-
Notifications
You must be signed in to change notification settings - Fork 1
/
history.go
181 lines (166 loc) · 4.09 KB
/
history.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package hexz
import (
"compress/gzip"
"encoding/gob"
"errors"
"fmt"
"io"
"os"
"path"
"strings"
"time"
)
type GameHistory struct {
Header *GameHistoryHeader
Entries []*GameHistoryEntry
}
type GameHistoryHeader struct {
GameId string
GameType GameType
PlayerNames []string
}
type GameHistoryEntry struct {
Timestamp time.Time // Will be added automatically by the .Write method if not specified.
EntryType string // One of {"move", "undo", "redo", "reset"}.
Move *MoveRequest
Board *BoardView
MoveScores *MoveScores
}
// gameHistoryRecord is the struct that is persisted on disk
// as a sequence of gobs.
type gameHistoryRecord struct {
// Only one of the fields will ever be populated.
Header *GameHistoryHeader
Entry *GameHistoryEntry
}
type HistoryWriter struct {
w io.WriteCloser
zw *gzip.Writer
enc *gob.Encoder
numRecords int // Number of records written so far
closed bool
}
func gameIdPath(historyDir, gameId string) string {
return path.Join(historyDir, gameIdFile(gameId))
}
// Returns a relative file path for the given gameId.
// Games are stored in subdirectories named after the first two uppercase(d) letters.
func gameIdFile(gameId string) string {
if gameId == "" {
gameId = "_"
}
if len(gameId) < 2 {
return fmt.Sprintf("%s/%s.ggz", strings.ToUpper(gameId), gameId)
}
dir := strings.ToUpper(gameId[:2])
return fmt.Sprintf("%s/%s.ggz", dir, gameId)
}
func NewHistoryWriter(historyDir, gameId string) (*HistoryWriter, error) {
p := gameIdPath(historyDir, gameId)
err := os.MkdirAll(path.Dir(p), 0755)
if err != nil {
return nil, err
}
f, err := os.Create(p)
if err != nil {
return nil, err
}
zw := gzip.NewWriter(f)
enc := gob.NewEncoder(zw)
return &HistoryWriter{
w: f,
zw: zw,
enc: enc,
}, nil
}
// Writes the given header to the writer's game history.
// This method must be called only once, and before any calls to Write.
// w may be a nil receiver, in which case this method does nothing.
func (w *HistoryWriter) WriteHeader(header *GameHistoryHeader) error {
if w == nil {
return nil
}
if w.numRecords != 0 {
return fmt.Errorf("header must be the first record written")
}
w.numRecords++
return w.enc.Encode(gameHistoryRecord{Header: header})
}
// Appends the given entry to the writer's game history.
// w may be a nil receiver, in which case this method does nothing.
func (w *HistoryWriter) Write(entry *GameHistoryEntry) error {
if w == nil {
return nil
}
if entry.Timestamp.IsZero() {
entry.Timestamp = time.Now()
}
w.numRecords++
return w.enc.Encode(gameHistoryRecord{Entry: entry})
}
func (w *HistoryWriter) Flush() error {
if w == nil {
return nil
}
if err := w.zw.Flush(); err != nil {
return err
}
// if f, ok := w.w.(*os.File); ok {
// // Sync is not strictly necessary, since .Write on an os.File is unbuffered.
// if err := f.Sync(); err != nil {
// return err
// }
// }
return nil
}
func (w *HistoryWriter) Close() error {
if w == nil {
return nil
}
if w.closed {
return nil
}
w.closed = true
if err := w.zw.Close(); err != nil {
return err
}
return w.w.Close()
}
func GameHistoryExists(historyDir string, gameId string) bool {
fi, err := os.Stat(gameIdPath(historyDir, gameId))
if err != nil {
return false
}
return fi.Mode().IsRegular() && fi.Size() > 0
}
func ReadGameHistory(historyDir string, gameId string) (*GameHistory, error) {
f, err := os.Open(gameIdPath(historyDir, gameId))
if err != nil {
return nil, err
}
defer f.Close()
r, err := gzip.NewReader(f)
if err != nil {
return nil, err
}
dec := gob.NewDecoder(r)
hist := &GameHistory{}
for {
var record gameHistoryRecord
err := dec.Decode(&record)
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
// Ignore ErrUnexpectedEOF as well, since that's what we'll get
// when we try to read a history file that's flushed, but not closed yet.
break
}
if err != nil {
return nil, err
}
if record.Header != nil {
hist.Header = record.Header
} else if record.Entry != nil {
hist.Entries = append(hist.Entries, record.Entry)
}
}
return hist, nil
}