moth

Monarch Of The Hill game server
git clone https://git.woozle.org/neale/moth.git

moth / cmd / mothd
Neale Pickett  ·  2021-10-25

mothballs.go

  1package main
  2
  3import (
  4	"archive/zip"
  5	"bufio"
  6	"fmt"
  7	"io"
  8	"log"
  9	"sort"
 10	"strconv"
 11	"strings"
 12	"sync"
 13	"time"
 14
 15	"github.com/spf13/afero"
 16	"github.com/spf13/afero/zipfs"
 17)
 18
 19type zipCategory struct {
 20	afero.Fs
 21	io.Closer
 22	mtime time.Time
 23}
 24
 25// Mothballs provides a collection of active mothball files (puzzle categories)
 26type Mothballs struct {
 27	afero.Fs
 28	categories   map[string]zipCategory
 29	categoryLock *sync.RWMutex
 30}
 31
 32// NewMothballs returns a new Mothballs structure backed by the provided directory
 33func NewMothballs(fs afero.Fs) *Mothballs {
 34	return &Mothballs{
 35		Fs:           fs,
 36		categories:   make(map[string]zipCategory),
 37		categoryLock: new(sync.RWMutex),
 38	}
 39}
 40
 41func (m *Mothballs) getCat(cat string) (zipCategory, bool) {
 42	m.categoryLock.RLock()
 43	defer m.categoryLock.RUnlock()
 44	ret, ok := m.categories[cat]
 45	return ret, ok
 46}
 47
 48// Open returns a ReadSeekCloser corresponding to the filename in a puzzle's category and points
 49func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekCloser, time.Time, error) {
 50	zc, ok := m.getCat(cat)
 51	if !ok {
 52		return nil, time.Time{}, fmt.Errorf("no such category: %s", cat)
 53	}
 54
 55	f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename))
 56	if err != nil {
 57		return nil, time.Time{}, err
 58	}
 59
 60	fInfo, err := f.Stat()
 61	return f, fInfo.ModTime(), err
 62}
 63
 64// Inventory returns the list of current categories
 65func (m *Mothballs) Inventory() []Category {
 66	m.categoryLock.RLock()
 67	defer m.categoryLock.RUnlock()
 68	categories := make([]Category, 0, 20)
 69	for cat, zfs := range m.categories {
 70		pointsList := make([]int, 0, 20)
 71		pf, err := zfs.Open("puzzles.txt")
 72		if err != nil {
 73			// No puzzles = no category
 74			continue
 75		}
 76		scanner := bufio.NewScanner(pf)
 77		for scanner.Scan() {
 78			line := scanner.Text()
 79			if pointval, err := strconv.Atoi(line); err != nil {
 80				log.Printf("Reading points for %s: %s", cat, err.Error())
 81			} else {
 82				pointsList = append(pointsList, pointval)
 83			}
 84		}
 85		sort.Ints(pointsList)
 86		categories = append(categories, Category{cat, pointsList})
 87	}
 88	return categories
 89}
 90
 91// CheckAnswer returns an error if the provided answer is in any way incorrect for the given category and points
 92func (m *Mothballs) CheckAnswer(cat string, points int, answer string) (bool, error) {
 93	zfs, ok := m.getCat(cat)
 94	if !ok {
 95		return false, fmt.Errorf("no such category: %s", cat)
 96	}
 97
 98	af, err := zfs.Open("answers.txt")
 99	if err != nil {
100		return false, fmt.Errorf("no answers.txt file")
101	}
102	defer af.Close()
103
104	needle := fmt.Sprintf("%d %s", points, answer)
105	scanner := bufio.NewScanner(af)
106	for scanner.Scan() {
107		if scanner.Text() == needle {
108			return true, nil
109		}
110	}
111
112	return false, nil
113}
114
115// refresh refreshes internal state.
116// It looks for changes to the directory listing, and caches any new mothballs.
117func (m *Mothballs) refresh() {
118	m.categoryLock.Lock()
119	defer m.categoryLock.Unlock()
120
121	// Any new categories?
122	files, err := afero.ReadDir(m.Fs, "/")
123	if err != nil {
124		log.Println("Error listing mothballs:", err)
125		return
126	}
127	found := make(map[string]bool)
128	for _, f := range files {
129		filename := f.Name()
130		if !strings.HasSuffix(filename, ".mb") {
131			continue
132		}
133		categoryName := strings.TrimSuffix(filename, ".mb")
134		found[categoryName] = true
135
136		reopen := false
137		if existingMothball, ok := m.categories[categoryName]; !ok {
138			reopen = true
139		} else if si, err := m.Fs.Stat(filename); err != nil {
140			log.Println(err)
141		} else if si.ModTime().After(existingMothball.mtime) {
142			existingMothball.Close()
143			delete(m.categories, categoryName)
144			reopen = true
145		}
146
147		if reopen {
148			f, err := m.Fs.Open(filename)
149			if err != nil {
150				log.Println(err)
151				continue
152			}
153
154			fi, err := f.Stat()
155			if err != nil {
156				f.Close()
157				log.Println(err)
158				continue
159			}
160
161			zrc, err := zip.NewReader(f, fi.Size())
162			if err != nil {
163				f.Close()
164				log.Println(err)
165				continue
166			}
167
168			m.categories[categoryName] = zipCategory{
169				Fs:     zipfs.New(zrc),
170				Closer: f,
171				mtime:  fi.ModTime(),
172			}
173
174			log.Println("Adding category:", categoryName)
175		}
176	}
177
178	// Delete anything in the list that wasn't found
179	for categoryName, zc := range m.categories {
180		if !found[categoryName] {
181			zc.Close()
182			delete(m.categories, categoryName)
183			log.Println("Removing category:", categoryName)
184		}
185	}
186}
187
188// Mothball just returns an error
189func (m *Mothballs) Mothball(cat string, w io.Writer) error {
190	return fmt.Errorf("refusing to repackage a compiled mothball")
191}
192
193// Maintain performs housekeeping for Mothballs.
194func (m *Mothballs) Maintain(updateInterval time.Duration) {
195	m.refresh()
196	for range time.NewTicker(updateInterval).C {
197		m.refresh()
198	}
199}