battlemap - maps_structure.go
1 package battlemap
2
3 import (
4 "encoding/json"
5 "io"
6 "strconv"
7
8 "vimagination.zapto.org/memio"
9 "vimagination.zapto.org/rwcount"
10 )
11
12 type levelMap struct {
13 Width uint64 `json:"width"`
14 Height uint64 `json:"height"`
15 StartX uint64 `json:"startX"`
16 StartY uint64 `json:"startY"`
17 GridType uint8 `json:"gridType"`
18 GridSize uint64 `json:"gridSize"`
19 GridStroke uint64 `json:"gridStroke"`
20 GridColour colour `json:"gridColour"`
21 GridDistance uint64 `json:"gridDistance"`
22 GridDiagonal bool `json:"gridDiagonal"`
23 Light colour `json:"lightColour"`
24 MaskOpaque bool `json:"baseOpaque"`
25 Mask [][]uint64 `json:"masks"`
26 Data map[string]json.RawMessage `json:"data"`
27 layer
28 layers map[string]struct{}
29 tokens map[uint64]layerToken
30 walls map[uint64]layerWall
31 lastTokenID, lastWallID uint64
32 JSON, UserJSON memio.Buffer `json:"-"`
33 }
34
35 func (l *levelMap) ReadFrom(r io.Reader) (int64, error) {
36 l.tokens = make(map[uint64]layerToken)
37 l.walls = make(map[uint64]layerWall)
38 sr := rwcount.Reader{Reader: r}
39 err := json.NewDecoder(&sr).Decode(l)
40 if sr.Err != nil {
41 return sr.Count, sr.Err
42 } else if err != nil {
43 return sr.Count, err
44 }
45 l.layers = make(map[string]struct{})
46 if err = l.validate(); err != nil {
47 return sr.Count, err
48 }
49 if _, ok := l.layers["Grid"]; !ok {
50 l.Layers = append(l.Layers, &layer{Name: "Grid"})
51 l.layers["Grid"] = struct{}{}
52 }
53 if _, ok := l.layers["Light"]; !ok {
54 l.Layers = append(l.Layers, &layer{Name: "Light"})
55 l.layers["Light"] = struct{}{}
56 }
57 l.writeJSON()
58 return sr.Count, nil
59 }
60
61 func (l *levelMap) writeJSON() {
62 l.JSON = l.JSON[:0]
63 l.JSON = strconv.AppendUint(append(l.JSON[:0], "{\"width\":"...), l.Width, 10)
64 l.JSON = strconv.AppendUint(append(l.JSON, ",\"height\":"...), l.Height, 10)
65 l.JSON = strconv.AppendUint(append(l.JSON, ",\"startX\":"...), l.StartX, 10)
66 l.JSON = strconv.AppendUint(append(l.JSON, ",\"startY\":"...), l.StartY, 10)
67 l.JSON = strconv.AppendUint(append(l.JSON, ",\"gridDistance\":"...), l.GridDistance, 10)
68 l.JSON = strconv.AppendBool(append(l.JSON, ",\"gridDiagonal\":"...), l.GridDiagonal)
69 l.JSON = appendNum(append(l.JSON, ",\"gridType\":"...), l.GridType)
70 l.JSON = strconv.AppendUint(append(l.JSON, ",\"gridSize\":"...), l.GridSize, 10)
71 l.JSON = strconv.AppendUint(append(l.JSON, ",\"gridStroke\":"...), l.GridStroke, 10)
72 l.JSON = l.GridColour.appendTo(append(l.JSON, ",\"gridColour\":"...))
73 l.JSON = l.Light.appendTo(append(l.JSON, ",\"lightColour\":"...))
74 l.JSON = strconv.AppendBool(append(l.JSON, ",\"baseOpaque\":"...), l.MaskOpaque)
75 l.JSON = append(l.JSON, ",\"masks\":["...)
76 for n, m := range l.Mask {
77 if n > 0 {
78 l.JSON = append(l.JSON, ',')
79 }
80 l.JSON = append(l.JSON, '[')
81 for o, i := range m {
82 if o > 0 {
83 l.JSON = append(l.JSON, ',')
84 }
85 l.JSON = strconv.AppendUint(l.JSON, i, 10)
86 }
87 l.JSON = append(l.JSON, ']')
88 }
89 l.JSON = append(l.JSON, ']')
90 l.JSON = append(l.JSON, ",\"data\":{"...)
91 first := true
92 for k, v := range l.Data {
93 if !first {
94 l.JSON = append(l.JSON, ',')
95 } else {
96 first = false
97 }
98 l.JSON = append(append(appendString(l.JSON, k), ':'), v...)
99 }
100 l.JSON = append(l.JSON, '}')
101 l.UserJSON = append(l.layer.appendTo(append(l.UserJSON[:0], l.JSON...), false, true), '}')
102 l.JSON = append(l.layer.appendTo(l.JSON, false, false), '}')
103 }
104
105 func (l *levelMap) WriteTo(w io.Writer) (int64, error) {
106 l.writeJSON()
107 n, err := w.Write(l.JSON)
108 return int64(n), err
109 }
110
111 func (l *levelMap) validate() error {
112 return l.layer.validate(l, true)
113 }
114
115 type layer struct {
116 Name string `json:"name"`
117 Hidden bool `json:"hidden"`
118 Locked bool `json:"locked"`
119 Tokens []*token `json:"tokens"`
120 Walls []*wall `json:"walls"`
121 Layers []*layer `json:"children"`
122 }
123
124 func (l *layer) validate(lm *levelMap, first bool) error {
125 if _, ok := lm.layers[l.Name]; ok {
126 return ErrDuplicateLayer
127 }
128 lm.layers[l.Name] = struct{}{}
129 if l.Name == "Grid" || l.Name == "Light" {
130 if l.Tokens != nil || l.Layers != nil {
131 return ErrInvalidLayer
132 }
133 } else if l.Layers != nil && l.Tokens != nil || l.Tokens == nil && l.Layers == nil || l.Name == "Light" && l.Layers != nil {
134 return ErrInvalidLayer
135 }
136 for _, layer := range l.Layers {
137 if !first && (l.Name == "Grid" || l.Name == "Light") {
138 return ErrInvalidLayer
139 }
140 if err := layer.validate(lm, false); err != nil {
141 return err
142 }
143 }
144 for _, token := range l.Tokens {
145 if err := token.validate(true); err != nil {
146 return err
147 }
148 if _, ok := lm.tokens[token.ID]; ok {
149 return ErrDuplicateTokenID
150 }
151 if token.ID > lm.lastTokenID {
152 lm.lastTokenID = token.ID
153 }
154 lm.tokens[token.ID] = layerToken{l, token}
155 }
156 for _, wall := range l.Walls {
157 lm.lastWallID++
158 wall.ID = lm.lastWallID
159 lm.walls[lm.lastWallID] = layerWall{l, wall}
160 }
161 return nil
162 }
163
164 func (l *layer) appendTo(p []byte, full, user bool) []byte {
165 if full {
166 p = appendString(append(p, "\"name\":"...), l.Name)
167 p = strconv.AppendBool(append(p, ",\"hidden\":"...), l.Hidden)
168 p = strconv.AppendBool(append(p, ",\"locked\":"...), l.Locked)
169 if l.Layers == nil && l.Name != "Grid" && l.Name != "Light" {
170 p = append(p, ",\"walls\":["...)
171 for n, w := range l.Walls {
172 if n > 0 {
173 p = append(p, ',')
174 }
175 p = w.appendTo(p)
176 }
177 p = append(p, ']')
178 }
179 }
180 if l.Layers != nil {
181 p = append(p, ",\"children\":["...)
182 for n, l := range l.Layers {
183 if n > 0 {
184 p = append(p, ',')
185 }
186 p = append(l.appendTo(append(p, '{'), true, user), '}')
187 }
188 } else if l.Name != "Grid" && l.Name != "Light" {
189 p = append(p, ",\"tokens\":["...)
190 for n, t := range l.Tokens {
191 if n > 0 {
192 p = append(p, ',')
193 }
194 p = t.appendTo(p, user)
195 }
196 } else {
197 return p
198 }
199 return append(p, ']')
200 }
201
202 type layerToken struct {
203 *layer
204 *token
205 }
206
207 type lightColours [][]colour
208
209 func (lc lightColours) appendTo(p []byte) []byte {
210 p = append(p, '[')
211 for n, cs := range lc {
212 if n > 0 {
213 p = append(p, ',')
214 }
215 p = append(p, '[')
216 for n, c := range cs {
217 if n > 0 {
218 p = append(p, ',')
219 }
220 p = c.appendTo(p)
221 }
222 p = append(p, ']')
223 }
224 p = append(p, ']')
225 return p
226 }
227
228 type lightData []uint64
229
230 func (ld lightData) appendTo(p []byte) []byte {
231 p = append(p, '[')
232 for n, l := range ld {
233 if n > 0 {
234 p = append(p, ',')
235 }
236 p = strconv.AppendUint(p, l, 10)
237 }
238 p = append(p, ']')
239 return p
240 }
241
242 type token struct {
243 ID uint64 `json:"id"`
244 Source uint64 `json:"src"`
245 coords
246 Width uint64 `json:"width"`
247 Height uint64 `json:"height"`
248 PatternWidth uint64 `json:"patternWidth"`
249 PatternHeight uint64 `json:"patternHeight"`
250 TokenData map[string]keystoreData `json:"tokenData"`
251 Rotation uint8 `json:"rotation"`
252 Flip bool `json:"flip"`
253 Flop bool `json:"flop"`
254 Snap bool `json:"snap"`
255 LightColours lightColours `json:"lightColours"`
256 LightStages lightData `json:"lightStages"`
257 LightTimings lightData `json:"lightTimings"`
258 TokenType tokenType `json:"tokenType"`
259 IsEllipse bool `json:"isEllipse"`
260 StrokeWidth uint8 `json:"strokeWidth"`
261 Fill colour `json:"fill"`
262 Fills []fill `json:"fills"`
263 FillType fillType `json:"fillType"`
264 Stroke colour `json:"stroke"`
265 Points []coords `json:"points"`
266 }
267
268 type coords struct {
269 X int64 `json:"x"`
270 Y int64 `json:"y"`
271 }
272
273 type tokenType uint8
274
275 const (
276 tokenImage tokenType = iota
277 tokenShape
278 tokenDrawing
279 )
280
281 type fillType uint8
282
283 const (
284 fillColour fillType = iota
285 fillRadial
286 fillGradient
287 )
288
289 func (t *token) appendTo(p []byte, user bool) []byte {
290 p = strconv.AppendUint(append(p, "{\"id\":"...), t.ID, 10)
291 p = appendNum(append(p, ",\"tokenType\":"...), uint8(t.TokenType))
292 p = strconv.AppendInt(append(p, ",\"x\":"...), t.X, 10)
293 p = strconv.AppendInt(append(p, ",\"y\":"...), t.Y, 10)
294 p = strconv.AppendUint(append(p, ",\"width\":"...), t.Width, 10)
295 p = strconv.AppendUint(append(p, ",\"height\":"...), t.Height, 10)
296 p = appendNum(append(p, ",\"rotation\":"...), t.Rotation)
297 p = strconv.AppendBool(append(p, ",\"snap\":"...), t.Snap)
298 p = t.LightColours.appendTo(append(p, ",\"lightColours\":"...))
299 p = t.LightStages.appendTo(append(p, ",\"lightStages\":"...))
300 p = t.LightTimings.appendTo(append(p, ",\"lightTimings\":"...))
301 switch t.TokenType {
302 case tokenImage:
303 p = strconv.AppendUint(append(p, ",\"src\":"...), t.Source, 10)
304 p = strconv.AppendBool(append(p, ",\"flip\":"...), t.Flip)
305 p = strconv.AppendBool(append(p, ",\"flop\":"...), t.Flop)
306 p = strconv.AppendUint(append(p, ",\"patternWidth\":"...), t.PatternWidth, 10)
307 p = strconv.AppendUint(append(p, ",\"patternHeight\":"...), t.PatternHeight, 10)
308 case tokenDrawing:
309 p = append(p, ",\"points\":["...)
310 for n, coords := range t.Points {
311 if n > 0 {
312 p = append(p, ',')
313 }
314 p = strconv.AppendInt(append(p, "{\"x\":"...), coords.X, 10)
315 p = strconv.AppendInt(append(p, ",\"y\":"...), coords.Y, 10)
316 p = append(p, '}')
317 }
318 p = append(p, ']')
319 fallthrough
320 case tokenShape:
321 if t.IsEllipse {
322 p = append(p, ",\"isEllipse\":true"...)
323 }
324 p = t.Fill.appendTo(append(p, ",\"fill\":"...))
325 if t.FillType != fillColour {
326 p = append(p, ",\"fills\":["...)
327 for n, f := range t.Fills {
328 if n > 0 {
329 p = append(p, ',')
330 }
331 p = f.appendTo(p)
332 }
333 p = append(p, ']')
334 }
335 p = t.Stroke.appendTo(append(p, ",\"stroke\":"...))
336 p = appendNum(append(p, ",\"strokeWidth\":"...), t.StrokeWidth)
337 p = appendNum(append(p, ",\"fillType\":"...), uint8(t.FillType))
338 p = append(p, ",\"fills\":["...)
339 for n, f := range t.Fills {
340 if n > 0 {
341 p = append(p, ',')
342 }
343 p = appendNum(append(p, "{\"pos\":"...), f.Pos)
344 p = f.Colour.appendTo(append(p, ",\"colour\":"...))
345 p = append(p, '}')
346 }
347 p = append(p, ']')
348 }
349 p = append(p, ",\"tokenData\":{"...)
350 first := true
351 for key, data := range t.TokenData {
352 if user && !data.User {
353 continue
354 }
355 if !first {
356 p = append(p, ',')
357 } else {
358 first = false
359 }
360 p = append(appendString(p, key), ':')
361 p = strconv.AppendBool(append(p, "{\"user\":"...), data.User)
362 p = append(append(p, ",\"data\":"...), data.Data...)
363 p = append(p, '}')
364
365 }
366 p = append(p, '}')
367 return append(p, '}')
368 }
369
370 func (t *token) validate(checkID bool) error {
371 if checkID && t.ID == 0 {
372 return ErrInvalidTokenID
373 }
374 if len(t.LightColours) != len(t.LightStages) {
375 return ErrInvalidLighting
376 }
377 for _, l := range t.LightColours {
378 if len(l) != len(t.LightTimings) {
379 return ErrInvalidLighting
380 }
381 }
382 switch t.TokenType {
383 case tokenImage:
384 if t.FillType != 0 || t.Source == 0 || t.IsEllipse || !t.Fill.empty() || !t.Stroke.empty() || t.StrokeWidth > 0 || len(t.Points) > 0 || (t.PatternWidth > 0) != (t.PatternHeight > 0) {
385 return ErrInvalidToken
386 }
387 case tokenDrawing:
388 if len(t.Points) < 2 || t.IsEllipse {
389 return ErrInvalidToken
390 }
391 fallthrough
392 case tokenShape:
393 if t.Source != 0 || t.Flip || t.Flop || t.PatternWidth > 0 || t.PatternHeight > 0 {
394 return ErrInvalidToken
395 }
396 default:
397 return ErrInvalidToken
398 }
399 return nil
400 }
401
402 type layerWall struct {
403 *layer
404 *wall
405 }
406
407 type wall struct {
408 ID uint64 `json:"id"`
409 X1 int64 `json:"x1"`
410 Y1 int64 `json:"y1"`
411 X2 int64 `json:"x2"`
412 Y2 int64 `json:"y2"`
413 Colour colour `json:"colour"`
414 Scattering uint8 `json:"scattering"`
415 }
416
417 func (w wall) appendTo(p []byte) []byte {
418 p = strconv.AppendUint(append(p, "{\"id\":"...), w.ID, 10)
419 p = strconv.AppendInt(append(p, ",\"x1\":"...), w.X1, 10)
420 p = strconv.AppendInt(append(p, ",\"y1\":"...), w.Y1, 10)
421 p = strconv.AppendInt(append(p, ",\"x2\":"...), w.X2, 10)
422 p = strconv.AppendInt(append(p, ",\"y2\":"...), w.Y2, 10)
423 p = w.Colour.appendTo(append(p, ",\"colour\":"...))
424 p = strconv.AppendUint(append(p, ",\"scattering\":"...), uint64(w.Scattering), 10)
425 return append(p, '}')
426 }
427
428 type colour struct {
429 R uint8 `json:"r"`
430 G uint8 `json:"g"`
431 B uint8 `json:"b"`
432 A uint8 `json:"a"`
433 }
434
435 func (c colour) appendTo(p []byte) []byte {
436 p = appendNum(append(p, "{\"r\":"...), c.R)
437 p = appendNum(append(p, ",\"g\":"...), c.G)
438 p = appendNum(append(p, ",\"b\":"...), c.B)
439 p = appendNum(append(p, ",\"a\":"...), c.A)
440 return append(p, '}')
441 }
442
443 func (c colour) empty() bool {
444 return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0
445 }
446
447 type fill struct {
448 Pos uint8 `json:"pos"`
449 Colour colour `json:"colour"`
450 }
451
452 func (f fill) appendTo(p []byte) []byte {
453 p = appendNum(append(p, "{\"pos\":"...), f.Pos)
454 p = f.Colour.appendTo(p)
455 return append(p, '}')
456 }
457
458 const hex = "0123456789abcdef"
459
460 func appendString(p []byte, s string) []byte {
461 last := 0
462 var char byte
463 p = append(p, '"')
464 for n, c := range s {
465 switch c {
466 case '"', '\\', '/':
467 char = byte(c)
468 case '\b':
469 char = 'b'
470 case '\f':
471 char = 'f'
472 case '\n':
473 char = 'n'
474 case '\r':
475 char = 'r'
476 case '\t':
477 char = 't'
478 default:
479 if c < 0x20 { // control characters
480 p = append(append(p, s[last:n]...), '\\', 'u', '0', '0', hex[c>>4], hex[c&0xf])
481 last = n + 1
482 }
483 continue
484 }
485 p = append(append(p, s[last:n]...), '\\', char)
486 last = n + 1
487 }
488 return append(append(p, s[last:]...), '"')
489 }
490
491 func appendNum(p []byte, n uint8) []byte {
492 if n >= 100 {
493 c := n / 100
494 n -= c * 100
495 p = append(p, '0'+c)
496 if n < 10 {
497 p = append(p, '0')
498 }
499 }
500 if n >= 10 {
501 c := n / 10
502 n -= c * 10
503 p = append(p, '0'+c)
504 }
505 return append(p, '0'+n)
506 }
507