say: move the copy deck into a package memory can import (V-506)
The summaries family is spoken by internal/memory, internal/morning and internal/tasks. internal/phraser already imports internal/memory, so the deck cannot stay in phraser without a cycle. internal/say is a leaf: embed, json, math/rand, strings, sync. The four phraser families keep their files and their floors and now call say.Load, *say.Deck, Text, Matches, Variants, RequirePlaceholder and RegisterFloor. No copy changed and no behaviour changed.
This commit is contained in:
@@ -16,6 +16,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed ack_ru_v1.json
|
||||
@@ -65,7 +67,7 @@ var ackKeys = []string{
|
||||
|
||||
// ackFloor — the literal each key falls back to when the file is unusable.
|
||||
// These are the exact strings that lived in Go before this file existed.
|
||||
var ackFloor = registerFloor(map[string]string{
|
||||
var ackFloor = say.RegisterFloor(map[string]string{
|
||||
AckFact: "записала факт.",
|
||||
AckFactKey: "отметила: {key}",
|
||||
AckFactValue: "отметила: {key} = {value}",
|
||||
@@ -93,12 +95,12 @@ var ackFloor = registerFloor(map[string]string{
|
||||
})
|
||||
|
||||
// Acks picks a hand-written Russian acknowledgement. Safe for concurrent use.
|
||||
type Acks struct{ d *deck }
|
||||
type Acks struct{ d *say.Deck }
|
||||
|
||||
// LoadAcks reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadAcks(src rand.Source) (*Acks, error) {
|
||||
d, err := loadDeck(ackJSON, AckSchemaVersion, ackKeys, ackFloor, src)
|
||||
d, err := say.Load(ackJSON, AckSchemaVersion, ackKeys, ackFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,7 +111,7 @@ func LoadAcks(src rand.Source) (*Acks, error) {
|
||||
{AckFactKey, "{key}"}, {AckFactValue, "{key}"}, {AckFactValue, "{value}"},
|
||||
{AckAct, "{fn}"}, {AckTask, "{text}"}, {AckTaskUrgent, "{text}"},
|
||||
} {
|
||||
if err := d.requirePlaceholder(req.key, req.ph); err != nil {
|
||||
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -117,7 +119,7 @@ func LoadAcks(src rand.Source) (*Acks, error) {
|
||||
}
|
||||
|
||||
// deck reads through a nil *Acks, which is the unloadable-file case.
|
||||
func (a *Acks) deck() *deck {
|
||||
func (a *Acks) deck() *say.Deck {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -127,11 +129,11 @@ func (a *Acks) deck() *deck {
|
||||
// Say returns one line for key, with his data filled into the frame. Pass nil
|
||||
// when the entry takes none.
|
||||
func (a *Acks) Say(key string, vars map[string]string) string {
|
||||
return a.deck().text(key, vars)
|
||||
return a.deck().Text(key, vars)
|
||||
}
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (a *Acks) Variants() []string { return a.deck().variants() }
|
||||
func (a *Acks) Variants() []string { return a.deck().Variants() }
|
||||
|
||||
var (
|
||||
ackOnce sync.Once
|
||||
@@ -158,5 +160,5 @@ func Ack(key string, vars map[string]string) string { return DefaultAcks().Say(k
|
||||
// IsAck reports whether text is a line key could have produced. For the daemon
|
||||
// tests, which can no longer compare against one literal.
|
||||
func IsAck(key string, vars map[string]string, text string) bool {
|
||||
return DefaultAcks().deck().matches(key, vars, text)
|
||||
return DefaultAcks().deck().Matches(key, vars, text)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed acts_ru_v1.json
|
||||
@@ -74,7 +76,7 @@ var actKeys = []string{
|
||||
|
||||
// actFloor — the literal each key falls back to when the file is unusable.
|
||||
// These are the exact strings that lived in Go before this file existed.
|
||||
var actFloor = registerFloor(map[string]string{
|
||||
var actFloor = say.RegisterFloor(map[string]string{
|
||||
ActDone: "готово.",
|
||||
ActDoneOut: "готово: {out}",
|
||||
ActDoneEntity: "команда выполнена для {name}.",
|
||||
@@ -112,12 +114,12 @@ var actFloor = registerFloor(map[string]string{
|
||||
})
|
||||
|
||||
// Acts picks a hand-written Russian act reply. Safe for concurrent use.
|
||||
type Acts struct{ d *deck }
|
||||
type Acts struct{ d *say.Deck }
|
||||
|
||||
// LoadActs reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadActs(src rand.Source) (*Acts, error) {
|
||||
d, err := loadDeck(actJSON, ActSchemaVersion, actKeys, actFloor, src)
|
||||
d, err := say.Load(actJSON, ActSchemaVersion, actKeys, actFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,7 +135,7 @@ func LoadActs(src rand.Source) (*Acts, error) {
|
||||
{AttentionNoneEntity, "{name}"}, {AttentionListEntity, "{name}"},
|
||||
{AttentionListEntity, "{items}"}, {AttentionFailEntity, "{name}"},
|
||||
} {
|
||||
if err := d.requirePlaceholder(req.key, req.ph); err != nil {
|
||||
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -141,7 +143,7 @@ func LoadActs(src rand.Source) (*Acts, error) {
|
||||
}
|
||||
|
||||
// deck reads through a nil *Acts, which is the unloadable-file case.
|
||||
func (a *Acts) deck() *deck {
|
||||
func (a *Acts) deck() *say.Deck {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -150,11 +152,11 @@ func (a *Acts) deck() *deck {
|
||||
|
||||
// Say returns one line for key, with the names filled into the frame.
|
||||
func (a *Acts) Say(key string, vars map[string]string) string {
|
||||
return a.deck().text(key, vars)
|
||||
return a.deck().Text(key, vars)
|
||||
}
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (a *Acts) Variants() []string { return a.deck().variants() }
|
||||
func (a *Acts) Variants() []string { return a.deck().Variants() }
|
||||
|
||||
var (
|
||||
actOnce sync.Once
|
||||
@@ -180,5 +182,5 @@ func A(key string, vars map[string]string) string { return DefaultActs().Say(key
|
||||
|
||||
// IsA reports whether text is a line key could have produced, for the tests.
|
||||
func IsA(key string, vars map[string]string, text string) bool {
|
||||
return DefaultActs().deck().matches(key, vars, text)
|
||||
return DefaultActs().deck().Matches(key, vars, text)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
)
|
||||
|
||||
// TestFallbackPersona scores every line in every hand-written line family on the persona checks the nudges already pass. These lines are
|
||||
// heard out loud and they live in a JSON file now, so a reworded variant that
|
||||
// says "рад" or "вы" would otherwise reach him with nothing in between.
|
||||
// TestFallbackPersona scores every line in every hand-written family on the
|
||||
// persona checks the nudges already pass. These lines are heard out loud and
|
||||
// they live in a JSON file now, so a reworded variant that says "рад" or "вы"
|
||||
// would otherwise reach him with nothing in between.
|
||||
//
|
||||
// Only the persona checks run. Mood and topic belong to a nudge, and these are
|
||||
// not nudges.
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// fallbacks_ru_v1.json break Go tests, which is the coupling this file removed.
|
||||
func isFallback(t *testing.T, key, sources, got string) bool {
|
||||
t.Helper()
|
||||
return DefaultFallbacks().deck().matches(key, map[string]string{"sources": sources}, got)
|
||||
return DefaultFallbacks().deck().Matches(key, map[string]string{"sources": sources}, got)
|
||||
}
|
||||
|
||||
// A dead server must be distinguishable from bad phrasing. Both PhraseChat and
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed fallbacks_ru_v1.json
|
||||
@@ -39,7 +41,7 @@ var fbKeys = []string{fbChat, fbQueryUnknown, fbQuerySources, fbWorldGap}
|
||||
|
||||
// hardFloor — the literal each key falls back to when the file is unusable.
|
||||
// These are the exact strings that lived in Go before this file existed.
|
||||
var hardFloor = registerFloor(map[string]string{
|
||||
var hardFloor = say.RegisterFloor(map[string]string{
|
||||
fbChat: "даже не знаю, что сказать.",
|
||||
fbQueryUnknown: "не знаю.",
|
||||
fbQuerySources: "вот что я нашла: {sources}",
|
||||
@@ -47,24 +49,24 @@ var hardFloor = registerFloor(map[string]string{
|
||||
})
|
||||
|
||||
// Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use.
|
||||
type Fallbacks struct{ d *deck }
|
||||
type Fallbacks struct{ d *say.Deck }
|
||||
|
||||
// LoadFallbacks reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadFallbacks(src rand.Source) (*Fallbacks, error) {
|
||||
d, err := loadDeck(fallbackJSON, FallbackSchemaVersion, fbKeys, hardFloor, src)
|
||||
d, err := say.Load(fallbackJSON, FallbackSchemaVersion, fbKeys, hardFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// query_sources is the one entry whose whole job is to read something back.
|
||||
if err := d.requirePlaceholder(fbQuerySources, "{sources}"); err != nil {
|
||||
if err := d.RequirePlaceholder(fbQuerySources, "{sources}"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Fallbacks{d: d}, nil
|
||||
}
|
||||
|
||||
// deck reads through a nil *Fallbacks, which is the unloadable-file case.
|
||||
func (f *Fallbacks) deck() *deck {
|
||||
func (f *Fallbacks) deck() *say.Deck {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -72,23 +74,23 @@ func (f *Fallbacks) deck() *deck {
|
||||
}
|
||||
|
||||
// Chat — nothing usable came back on the chat path.
|
||||
func (f *Fallbacks) Chat() string { return f.deck().text(fbChat, nil) }
|
||||
func (f *Fallbacks) Chat() string { return f.deck().Text(fbChat, nil) }
|
||||
|
||||
// Unknown — a question she cannot answer and will not guess at.
|
||||
func (f *Fallbacks) Unknown() string { return f.deck().text(fbQueryUnknown, nil) }
|
||||
func (f *Fallbacks) Unknown() string { return f.deck().Text(fbQueryUnknown, nil) }
|
||||
|
||||
// FromSources — read back what she was handed, because phrasing it failed.
|
||||
func (f *Fallbacks) FromSources(sources string) string {
|
||||
return f.deck().text(fbQuerySources, map[string]string{"sources": sources})
|
||||
return f.deck().Text(fbQuerySources, map[string]string{"sources": sources})
|
||||
}
|
||||
|
||||
// WorldGap — the world model is the one configured to answer and it is not
|
||||
// answering. Fixed wording: it names a specific gap, and a variant set here
|
||||
// would let "the big model is asleep" drift into "I don't know".
|
||||
func (f *Fallbacks) WorldGap() string { return f.deck().text(fbWorldGap, nil) }
|
||||
func (f *Fallbacks) WorldGap() string { return f.deck().Text(fbWorldGap, nil) }
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (f *Fallbacks) Variants() []string { return f.deck().variants() }
|
||||
func (f *Fallbacks) Variants() []string { return f.deck().Variants() }
|
||||
|
||||
// The process-wide instance. Package-level because these lines are needed on
|
||||
// paths that have no phraser to hand — cmd/mavend names the world gap without
|
||||
@@ -129,10 +131,10 @@ func WorldGap() string { return DefaultFallbacks().WorldGap() }
|
||||
// IsUnknownFallback reports whether text is one of her "I do not know" lines.
|
||||
// The daemon tests read it to tell an answer from a shrug.
|
||||
func IsUnknownFallback(text string) bool {
|
||||
return DefaultFallbacks().deck().matches(fbQueryUnknown, nil, text)
|
||||
return DefaultFallbacks().deck().Matches(fbQueryUnknown, nil, text)
|
||||
}
|
||||
|
||||
// IsSourcesFallback reports whether text is sources read back verbatim.
|
||||
func IsSourcesFallback(text, sources string) bool {
|
||||
return DefaultFallbacks().deck().matches(fbQuerySources, map[string]string{"sources": sources}, text)
|
||||
return DefaultFallbacks().deck().Matches(fbQuerySources, map[string]string{"sources": sources}, text)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed query_ru_v1.json
|
||||
@@ -65,7 +67,7 @@ var queryKeys = []string{
|
||||
|
||||
// queryFloor — the literal each key falls back to when the file is unusable.
|
||||
// These are the exact strings that lived in Go before this file existed.
|
||||
var queryFloor = registerFloor(map[string]string{
|
||||
var queryFloor = say.RegisterFloor(map[string]string{
|
||||
QueryUnknown: "не знаю.",
|
||||
QueryOtherDay: "про другой день так не отвечу — спроси целиком.",
|
||||
QueryPersonalNone: "не знаю — не нашла у тебя такой записи.",
|
||||
@@ -95,12 +97,12 @@ var queryFloor = registerFloor(map[string]string{
|
||||
})
|
||||
|
||||
// Queries picks a hand-written Russian query line. Safe for concurrent use.
|
||||
type Queries struct{ d *deck }
|
||||
type Queries struct{ d *say.Deck }
|
||||
|
||||
// LoadQueries reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadQueries(src rand.Source) (*Queries, error) {
|
||||
d, err := loadDeck(queryJSON, QuerySchemaVersion, queryKeys, queryFloor, src)
|
||||
d, err := say.Load(queryJSON, QuerySchemaVersion, queryKeys, queryFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -111,7 +113,7 @@ func LoadQueries(src rand.Source) (*Queries, error) {
|
||||
{QueryFound, "{text}"}, {QueryPageText, "{text}"}, {QueryFeedsNew, "{items}"},
|
||||
{QueryWeatherNow, "{location}"}, {QueryWeatherNow, "{temp}"}, {QueryWeatherNow, "{condition}"},
|
||||
} {
|
||||
if err := d.requirePlaceholder(req.key, req.ph); err != nil {
|
||||
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -119,7 +121,7 @@ func LoadQueries(src rand.Source) (*Queries, error) {
|
||||
}
|
||||
|
||||
// deck reads through a nil *Queries, which is the unloadable-file case.
|
||||
func (q *Queries) deck() *deck {
|
||||
func (q *Queries) deck() *say.Deck {
|
||||
if q == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -128,11 +130,11 @@ func (q *Queries) deck() *deck {
|
||||
|
||||
// Say returns one line for key, with the values filled into the frame.
|
||||
func (q *Queries) Say(key string, vars map[string]string) string {
|
||||
return q.deck().text(key, vars)
|
||||
return q.deck().Text(key, vars)
|
||||
}
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (q *Queries) Variants() []string { return q.deck().variants() }
|
||||
func (q *Queries) Variants() []string { return q.deck().Variants() }
|
||||
|
||||
var (
|
||||
queryOnce sync.Once
|
||||
@@ -158,5 +160,5 @@ func Q(key string, vars map[string]string) string { return DefaultQueries().Say(
|
||||
|
||||
// IsQ reports whether text is a line key could have produced, for the tests.
|
||||
func IsQ(key string, vars map[string]string, text string) bool {
|
||||
return DefaultQueries().deck().matches(key, vars, text)
|
||||
return DefaultQueries().deck().Matches(key, vars, text)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package phraser
|
||||
package say
|
||||
|
||||
// deck — the mechanics every family of hand-written Russian lines shares.
|
||||
// Package say holds the mechanics every family of hand-written Russian lines
|
||||
// shares. The families themselves live next to the code that speaks them.
|
||||
//
|
||||
// Deck — the mechanics every family of hand-written Russian lines shares.
|
||||
//
|
||||
// A family is one embedded JSON file: schema-versioned, several variants per
|
||||
// entry, never the same variant twice running, and a hard floor of Go literals
|
||||
@@ -20,28 +23,28 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// deckEntry — one line she can say, in as many wordings as the file gives.
|
||||
type deckEntry struct {
|
||||
// Entry — one line she can say, in as many wordings as the file gives.
|
||||
type Entry struct {
|
||||
// Fixed — one variant, never picked between. For wording that must not
|
||||
// drift from turn to turn, like a phrase naming one specific gap.
|
||||
Fixed bool `json:"fixed"`
|
||||
Variants []string `json:"variants"`
|
||||
}
|
||||
|
||||
type deckFile struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Name string `json:"name"`
|
||||
Notes []string `json:"notes"`
|
||||
Entries map[string]deckEntry `json:"entries"`
|
||||
type file struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Name string `json:"name"`
|
||||
Notes []string `json:"notes"`
|
||||
Entries map[string]Entry `json:"entries"`
|
||||
}
|
||||
|
||||
// deck picks a line. Safe for concurrent use. A nil *deck answers from the
|
||||
// deck picks a line. Safe for concurrent use. A nil *Deck answers from the
|
||||
// floor, which is what an unloadable file leaves behind.
|
||||
type deck struct {
|
||||
type Deck struct {
|
||||
mu sync.Mutex
|
||||
rnd *rand.Rand
|
||||
last map[string]string
|
||||
file deckFile
|
||||
file file
|
||||
keys []string
|
||||
floor map[string]string
|
||||
}
|
||||
@@ -49,8 +52,8 @@ type deck struct {
|
||||
// loadDeck parses raw, checks the version and every required key, and seeds the
|
||||
// picker. Pass a source to make the picking reproducible in tests; nil seeds
|
||||
// from the clock.
|
||||
func loadDeck(raw []byte, version int, keys []string, floor map[string]string, src rand.Source) (*deck, error) {
|
||||
var f deckFile
|
||||
func Load(raw []byte, version int, keys []string, floor map[string]string, src rand.Source) (*Deck, error) {
|
||||
var f file
|
||||
if err := json.Unmarshal(raw, &f); err != nil {
|
||||
return nil, fmt.Errorf("parse: %w", err)
|
||||
}
|
||||
@@ -69,13 +72,13 @@ func loadDeck(raw []byte, version int, keys []string, floor map[string]string, s
|
||||
if src == nil {
|
||||
src = rand.NewSource(time.Now().UnixNano())
|
||||
}
|
||||
return &deck{rnd: rand.New(src), last: map[string]string{}, file: f, keys: keys, floor: floor}, nil
|
||||
return &Deck{rnd: rand.New(src), last: map[string]string{}, file: f, keys: keys, floor: floor}, nil
|
||||
}
|
||||
|
||||
// requirePlaceholder fails the load when a variant of key does not use ph. For
|
||||
// an entry whose whole job is to read something back, a variant without the
|
||||
// placeholder silently drops it.
|
||||
func (d *deck) requirePlaceholder(key, ph string) error {
|
||||
func (d *Deck) RequirePlaceholder(key, ph string) error {
|
||||
for _, v := range d.file.Entries[key].Variants {
|
||||
if !strings.Contains(v, ph) {
|
||||
return fmt.Errorf("%q variant %q does not use %s", key, v, ph)
|
||||
@@ -86,7 +89,7 @@ func (d *deck) requirePlaceholder(key, ph string) error {
|
||||
|
||||
// text returns one variant for key with the placeholders filled in. A nil
|
||||
// receiver answers from the floor, so no caller checks whether the file loaded.
|
||||
func (d *deck) text(key string, vars map[string]string) string {
|
||||
func (d *Deck) Text(key string, vars map[string]string) string {
|
||||
tmpl := ""
|
||||
if d != nil {
|
||||
if e, ok := d.file.Entries[key]; ok && len(e.Variants) > 0 {
|
||||
@@ -101,7 +104,7 @@ func (d *deck) text(key string, vars map[string]string) string {
|
||||
|
||||
// matches reports whether text is a line key could have produced. A caller that
|
||||
// has to recognise one of these lines cannot compare against a literal any more.
|
||||
func (d *deck) matches(key string, vars map[string]string, text string) bool {
|
||||
func (d *Deck) Matches(key string, vars map[string]string, text string) bool {
|
||||
if fill(floorOf(d, key), vars) == text {
|
||||
return true
|
||||
}
|
||||
@@ -118,7 +121,7 @@ func (d *deck) matches(key string, vars map[string]string, text string) bool {
|
||||
|
||||
// variants returns every line the file can produce, in key order, for the
|
||||
// persona scorer. Stable order so a failure names the same variant twice.
|
||||
func (d *deck) variants() []string {
|
||||
func (d *Deck) Variants() []string {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -130,7 +133,7 @@ func (d *deck) variants() []string {
|
||||
}
|
||||
|
||||
// pick chooses at random, skipping whatever this entry said last time.
|
||||
func (d *deck) pick(key string, e deckEntry) string {
|
||||
func (d *Deck) pick(key string, e Entry) string {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
@@ -153,7 +156,7 @@ func (d *deck) pick(key string, e deckEntry) string {
|
||||
|
||||
// floorOf reads the Go literal behind key, and works on a nil deck because that
|
||||
// is exactly the case it exists for. The per-family map is the source of truth.
|
||||
func floorOf(d *deck, key string) string {
|
||||
func floorOf(d *Deck, key string) string {
|
||||
if d != nil && d.floor != nil {
|
||||
return d.floor[key]
|
||||
}
|
||||
@@ -164,7 +167,7 @@ func floorOf(d *deck, key string) string {
|
||||
// finds them. Families register at init; the keys are namespaced by family.
|
||||
var deckFloors = map[string]string{}
|
||||
|
||||
func registerFloor(floor map[string]string) map[string]string {
|
||||
func RegisterFloor(floor map[string]string) map[string]string {
|
||||
for k, v := range floor {
|
||||
deckFloors[k] = v
|
||||
}
|
||||
Reference in New Issue
Block a user