parent
443a47b387
commit
abaeb8d609
@ -0,0 +1,19 @@ |
||||
package client |
||||
|
||||
import "hexmap-server/actions/syncable" |
||||
|
||||
type Hello struct { |
||||
Version int `json:"version"` |
||||
} |
||||
|
||||
type Refresh struct { |
||||
} |
||||
|
||||
type SentAction struct { |
||||
Id int `json:"id"` |
||||
Action syncable.Action `json:"action"` |
||||
} |
||||
|
||||
type SentActions struct { |
||||
Nested []SentAction `json:"nested"` |
||||
} |
@ -0,0 +1 @@ |
||||
package server |
@ -0,0 +1,18 @@ |
||||
package syncable |
||||
|
||||
import ( |
||||
"errors" |
||||
"hexmap-server/state" |
||||
) |
||||
|
||||
var ErrorNoOp error = errors.New("action's effects were already applied, or it's an empty action") |
||||
|
||||
// Action is the interface for actions that can be shared.
|
||||
type Action interface { |
||||
// Apply causes the action's effects to be applied to s, mutating it in place.
|
||||
// All syncable.Actions must conform to the standard that if an action can't be correctly applied, or if it would
|
||||
// have no effect, it returns an error without changing s.
|
||||
// If an action can be correctly applied but would have no effect, it should return ErrorNoOp.
|
||||
// If an action is correctly applied and has an effect, it should return nil.
|
||||
Apply(s *state.Synced) error |
||||
} |
@ -0,0 +1,5 @@ |
||||
module hexmap-server |
||||
|
||||
go 1.16 |
||||
|
||||
require github.com/rs/xid v1.3.0 // indirect |
@ -0,0 +1,2 @@ |
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= |
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= |
@ -0,0 +1 @@ |
||||
package persistence |
@ -0,0 +1,9 @@ |
||||
package state |
||||
|
||||
// StorageCoordinates gives the coordinates of a cell in a form optimized for storage.
|
||||
type StorageCoordinates struct { |
||||
// Line is the index from 0 to Lines - 1 of the HexLine in the HexLayer.
|
||||
Line uint8 `json:"line"` |
||||
// Cell is the index from 0 to CellsPerLine - 1 of the HexCell in the HexLine.
|
||||
Cell uint8 `json:"cell"` |
||||
} |
@ -0,0 +1,104 @@ |
||||
package state |
||||
|
||||
import "fmt" |
||||
|
||||
// HexColor is an internal representation of a hexadecimal color string.
|
||||
// It can take one of these formats:
|
||||
// #RRGGBBAA
|
||||
// #RRGGBB
|
||||
// #RGBA
|
||||
// #RGB
|
||||
// When marshaling, it will always choose the most efficient format, but any format can be used when unmarshaling.
|
||||
type HexColor struct { |
||||
// R is the red component of the color.
|
||||
R uint8 |
||||
// G is the green component of the color.
|
||||
G uint8 |
||||
// B is the blue component of the color.
|
||||
B uint8 |
||||
// A is the alpha component of the color.
|
||||
A uint8 |
||||
} |
||||
|
||||
// HexColorFromString decodes a hexadecimal string into a hex color.
|
||||
func HexColorFromString(text string) (HexColor, error) { |
||||
var hex HexColor |
||||
if err := (&hex).UnmarshalText([]byte(text)); err != nil { |
||||
return hex, err |
||||
} |
||||
return hex, nil |
||||
} |
||||
|
||||
func (h *HexColor) UnmarshalText(text []byte) error { |
||||
var count, expected int |
||||
var short bool |
||||
var scanErr error |
||||
switch len(text) { |
||||
case 9: |
||||
// Long form with alpha: #RRGGBBAA
|
||||
expected = 4 |
||||
short = false |
||||
count, scanErr = fmt.Sscanf(string(text), "#%02X%02X%02X%02X", &h.R, &h.G, &h.B, &h.A) |
||||
case 7: |
||||
// Long form: #RRGGBB
|
||||
expected = 3 |
||||
h.A = 0xFF |
||||
count, scanErr = fmt.Sscanf(string(text), "#%02X%02X%02X", &h.R, &h.G, &h.B) |
||||
case 5: |
||||
// Short form with alpha: #RGBA
|
||||
expected = 4 |
||||
short = true |
||||
count, scanErr = fmt.Sscanf(string(text), "#%01X%01X%01X%01X", &h.R, &h.G, &h.B, &h.A) |
||||
case 4: |
||||
// Short form: #RGB
|
||||
expected = 3 |
||||
h.A = 0xF |
||||
short = true |
||||
count, scanErr = fmt.Sscanf(string(text), "#%01X%01X%01X", &h.R, &h.G, &h.B) |
||||
default: |
||||
return fmt.Errorf("can't decode %s as HexColor: wrong length", text) |
||||
} |
||||
if scanErr != nil { |
||||
return scanErr |
||||
} |
||||
if count != expected { |
||||
return fmt.Errorf("can't decode %s as HexColor: missing components", text) |
||||
} |
||||
if short { |
||||
h.R *= 0x11 |
||||
h.G *= 0x11 |
||||
h.B *= 0x11 |
||||
h.A *= 0x11 |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// MarshalText marshals the HexColor into a small string.
|
||||
func (h HexColor) MarshalText() (text []byte, err error) { |
||||
return []byte(h.String()), nil |
||||
} |
||||
|
||||
// String prints the HexColor as an abbreviated notation.
|
||||
// Specifically, short form is used when possible (i.e., when all components are evenly divisible by 0x11).
|
||||
// The alpha component is left out if it's 0xFF.
|
||||
func (h HexColor) String() string { |
||||
if h.R%0x11 == 0 && h.G%0x11 == 0 && h.B%0x11 == 0 && h.A%0x11 == 0 { |
||||
// Short form works.
|
||||
if h.A == 0xFF { |
||||
// It's great when it's easy!
|
||||
return fmt.Sprintf("#%01X%01X%01X", h.R/0x11, h.G/0x11, h.B/0x11) |
||||
} else { |
||||
// Just need to add the alpha.
|
||||
return fmt.Sprintf("#%01X%01X%01X%01X", h.R/0x11, h.G/0x11, h.B/0x11, h.A/0x11) |
||||
} |
||||
} else { |
||||
// Gotta use long form.
|
||||
if h.A == 0xFF { |
||||
// Can skip the alpha channel, though.
|
||||
return fmt.Sprintf("#%02X%02X%02X", h.R, h.G, h.B) |
||||
} else { |
||||
// Doing things the hard way.
|
||||
return fmt.Sprintf("#%02X%02X%02X%02X", h.R, h.G, h.B, h.A) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,83 @@ |
||||
package state |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/rs/xid" |
||||
) |
||||
|
||||
// HexMapRepresentation combines HexOrientation and LineParity to represent a map's display mode.
|
||||
type HexMapRepresentation struct { |
||||
Orientation HexOrientation `json:"orientation"` |
||||
IndentedLines LineParity `json:"indentedLines"` |
||||
} |
||||
|
||||
// HexCell contains data for a single cell of the map.
|
||||
type HexCell struct { |
||||
// Color contains the color of the cell, in hex notation.
|
||||
Color HexColor `json:"color"` |
||||
} |
||||
|
||||
// HexLine is a line of cells which are adjacent by flat sides in a vertical or horizontal direction.
|
||||
type HexLine []HexCell |
||||
|
||||
// Copy creates a deep copy of this HexLine.
|
||||
func (l HexLine) Copy() HexLine { |
||||
duplicate := make(HexLine, len(l)) |
||||
for index, value := range l { |
||||
duplicate[index] = value |
||||
} |
||||
return duplicate |
||||
} |
||||
|
||||
// HexLayer is a two-dimensional plane of cells which are arranged into lines.
|
||||
type HexLayer []HexLine |
||||
|
||||
// GetCellAt returns a reference to the cell at the given coordinates.
|
||||
func (l HexLayer) GetCellAt(c StorageCoordinates) (*HexCell, error) { |
||||
if int(c.Line) > len(l) { |
||||
return nil, fmt.Errorf("line %d out of bounds (%d)", c.Line, len(l)) |
||||
} |
||||
line := l[c.Line] |
||||
if int(c.Cell) > len(line) { |
||||
return nil, fmt.Errorf("cell %d out of bounds (%d)", c.Cell, len(line)) |
||||
} |
||||
return &(line[c.Cell]), nil |
||||
} |
||||
|
||||
// Copy creates a deep copy of this HexLayer.
|
||||
func (l HexLayer) Copy() HexLayer { |
||||
duplicate := make(HexLayer, len(l)) |
||||
for index, value := range l { |
||||
duplicate[index] = value.Copy() |
||||
} |
||||
return duplicate |
||||
} |
||||
|
||||
// HexMap contains the data for a map instance.
|
||||
type HexMap struct { |
||||
// Xid is the unique ID of the HexMap, used to encourage clients not to blindly interact with a different map.
|
||||
Xid xid.ID `json:"xid"` |
||||
// Lines is the rough number of rows (in PointyTop orientation) or columns (in FlatTop orientation) in the map.
|
||||
// Because different lines will be staggered, it's somewhat hard to see.
|
||||
Lines uint8 `json:"lines"` |
||||
// CellsPerLine is the rough number of columns (in PointyTop orientation) or rows (in FlatTop orientation).
|
||||
// This is the number of cells joined together, flat-edge to flat-edge, in each line.
|
||||
CellsPerLine uint8 `json:"cellsPerLine"` |
||||
// DisplayMode is the orientation and line parity used to display the map.
|
||||
DisplayMode HexMapRepresentation `json:"displayMode"` |
||||
// LineCells contains the actual map data.
|
||||
// LineCells itself is a slice with Lines elements, each of which is a line;
|
||||
// each of those lines is a slice of CellsPerLine cells.
|
||||
LineCells HexLayer `json:"lineCells"` |
||||
} |
||||
|
||||
// Copy creates a deep copy of this HexMap.
|
||||
func (m HexMap) Copy() HexMap { |
||||
return HexMap{ |
||||
Xid: m.Xid, |
||||
Lines: m.Lines, |
||||
CellsPerLine: m.CellsPerLine, |
||||
DisplayMode: m.DisplayMode, |
||||
LineCells: m.LineCells.Copy(), |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
package state |
||||
|
||||
import "fmt" |
||||
|
||||
// HexOrientation is the enum for the direction hexes are facing.
|
||||
type HexOrientation uint8 |
||||
|
||||
const ( |
||||
// PointyTop indicates hexes that have a pair of sides on either side in the horizontal direction,
|
||||
// and points on the top and bottom in the vertical direction.
|
||||
PointyTop HexOrientation = 1 |
||||
// FlatTop indicates hexes that have a pair of points on either side in the horizontal direction,
|
||||
// and sides on the top and bottom in the vertical direction.
|
||||
FlatTop HexOrientation = 2 |
||||
) |
||||
|
||||
// UnmarshalText unmarshals from the equivalent Javascript constant name.
|
||||
func (o *HexOrientation) UnmarshalText(text []byte) error { |
||||
switch string(text) { |
||||
case "POINTY_TOP": |
||||
*o = PointyTop |
||||
return nil |
||||
case "FLAT_TOP": |
||||
*o = FlatTop |
||||
return nil |
||||
default: |
||||
return fmt.Errorf("can't unmarshal unknown HexOrientation %s", text) |
||||
} |
||||
} |
||||
|
||||
// MarshalText marshals into the equivalent JavaScript constant name.
|
||||
func (o HexOrientation) MarshalText() (text []byte, err error) { |
||||
switch o { |
||||
case PointyTop, FlatTop: |
||||
return []byte(o.String()), nil |
||||
default: |
||||
return nil, fmt.Errorf("can't marshal unknown HexOrientation %d", o) |
||||
} |
||||
} |
||||
|
||||
// String returns the equivalent JavaScript constant name.
|
||||
func (o HexOrientation) String() string { |
||||
switch o { |
||||
case PointyTop: |
||||
return "POINTY_TOP" |
||||
case FlatTop: |
||||
return "FLAT_TOP" |
||||
default: |
||||
return fmt.Sprintf("[unknown HexOrientation %d]", o) |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
package state |
||||
|
||||
import "fmt" |
||||
|
||||
// LineParity indicates whether odd or even lines are indented.
|
||||
type LineParity uint8 |
||||
|
||||
const ( |
||||
// OddLines indicates that odd lines - 1, 3, 5... - are indented by 1/2 cell.
|
||||
OddLines LineParity = 1 |
||||
// EvenLines indicates that even lines - 0, 2, 4... - are indented by 1/2 cell.
|
||||
EvenLines LineParity = 2 |
||||
) |
||||
|
||||
// UnmarshalText unmarshals from the equivalent Javascript constant name.
|
||||
func (o *LineParity) UnmarshalText(text []byte) error { |
||||
switch string(text) { |
||||
case "ODD": |
||||
*o = OddLines |
||||
return nil |
||||
case "EVEN": |
||||
*o = EvenLines |
||||
return nil |
||||
default: |
||||
return fmt.Errorf("can't unmarshal unknown LineParity %s", text) |
||||
} |
||||
} |
||||
|
||||
// MarshalText marshals into the equivalent JavaScript constant name.
|
||||
func (o LineParity) MarshalText() (text []byte, err error) { |
||||
switch o { |
||||
case OddLines, EvenLines: |
||||
return []byte(o.String()), nil |
||||
default: |
||||
return nil, fmt.Errorf("can't marshal unknown LineParity %d", o) |
||||
} |
||||
} |
||||
|
||||
// String returns the equivalent JavaScript constant name.
|
||||
func (o LineParity) String() string { |
||||
switch o { |
||||
case OddLines: |
||||
return "ODD_LINES" |
||||
case EvenLines: |
||||
return "EVEN_LINES" |
||||
default: |
||||
return fmt.Sprintf("[unknown LineParity %d]", o) |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
package state |
||||
|
||||
// Synced contains all state that is synced between the server and its clients.
|
||||
type Synced struct { |
||||
Map HexMap `json:"map"` |
||||
User UserData `json:"user"` |
||||
} |
||||
|
||||
// Copy creates a deep copy of this Synced instance.
|
||||
func (s Synced) Copy() Synced { |
||||
return Synced{ |
||||
Map: s.Map.Copy(), |
||||
User: s.User.Copy(), |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
package state |
||||
|
||||
// UserData contains data about clients that is synced between client and server.
|
||||
// Unlike the map, UserData is not persisted to disk, and all UserData is lost on shutdown.
|
||||
type UserData struct { |
||||
ActiveColor HexColor `json:"active_color"` |
||||
} |
||||
|
||||
// Copy creates a deep copy of this UserData.
|
||||
func (u UserData) Copy() UserData { |
||||
return UserData{ |
||||
ActiveColor: u.ActiveColor, |
||||
} |
||||
} |
Loading…
Reference in new issue