-
+
+
{colorPickerElement}
-
);
diff --git a/client/src/actions/NetworkAction.ts b/client/src/actions/NetworkAction.ts
index 92a6dc3..2f041c9 100644
--- a/client/src/actions/NetworkAction.ts
+++ b/client/src/actions/NetworkAction.ts
@@ -60,11 +60,6 @@ export function isServerOKAction(action: AppAction): action is ServerOKAction {
return action.type === SERVER_OK
}
-export interface ActionFailure {
- readonly id: number
- readonly error: string
-}
-
export const SERVER_FAILED = "SERVER_FAILED"
/** Sent in response to the client's ClientNestedAction, if it fails and has not been applied to the server map. */
export interface ServerFailedAction extends BaseAction {
@@ -103,9 +98,6 @@ export function isServerActAction(action: AppAction): action is ServerActAction
}
export type SyncableAction = SendableAction
-export function isSyncableAction(action: AppAction): action is SyncableAction {
- return isSendableAction(action)
-}
export type ServerAction =
ServerHelloAction | ServerGoodbyeAction | ServerRefreshAction |
diff --git a/client/src/reducers/HexMapReducer.ts b/client/src/reducers/HexMapReducer.ts
index 32ad5d2..8c16c00 100644
--- a/client/src/reducers/HexMapReducer.ts
+++ b/client/src/reducers/HexMapReducer.ts
@@ -40,6 +40,4 @@ export function hexMapReducer(oldState: HexMap, action: MapAction): HexMap {
case CELL_REMOVE:
return replaceCell(oldState, action.at, null)
}
-}
-
-export type HexMapReducer = typeof hexMapReducer;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/client/src/state/Coordinates.ts b/client/src/state/Coordinates.ts
index 433098b..49e9784 100644
--- a/client/src/state/Coordinates.ts
+++ b/client/src/state/Coordinates.ts
@@ -1,23 +1,5 @@
-/** Cubic (3-dimensional) coordinates for algorithms. */
import {HexagonOrientation, HexMapRepresentation, LineParity} from "./HexMap";
-export interface CubicCoordinates {
- /** The cubic x-coordinate. */
- readonly x: number
- /** The cubic y-coordinate. */
- readonly y: number
- /** The cubic z-coordinate. */
- readonly z: number
-}
-
-/** Axial (2-dimensional cube variant) coordinates for display. */
-export interface AxialCoordinates {
- /** The axial x-coordinate. */
- readonly q: number
- /** The axial y-coordinate. */
- readonly r: number
-}
-
/** Staggered (storage) coordinates for accessing cell storage. */
export interface StorageCoordinates {
/** The index of the line within the map. */
diff --git a/client/src/ui/debug/ConsoleConnection.tsx b/client/src/ui/debug/ConsoleConnection.tsx
deleted file mode 100644
index 81b85a5..0000000
--- a/client/src/ui/debug/ConsoleConnection.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import {
- CLIENT_ACT,
- ClientAction,
- ClientActAction,
- isClientActAction,
- SendableAction,
- SentAction,
- SERVER_FAILED,
- SERVER_GOODBYE,
- SERVER_HELLO,
- SERVER_OK,
- SERVER_ACT,
- SERVER_SOCKET_STARTUP,
- ServerAction,
- SocketState
-} from "../../actions/NetworkAction";
-import {HexagonOrientation, HexMapRepresentation, initializeMap, LineParity} from "../../state/HexMap";
-import {ReactElement, useContext, useEffect, useRef, useState} from "react";
-import {DispatchContext} from "../context/DispatchContext";
-import {USER_ACTIVE_COLOR} from "../../actions/UserAction";
-import {CELL_COLOR} from "../../actions/CellAction";
-
-export enum OrientationConstants {
- ROWS = "ROWS",
- COLUMNS = "COLUMNS",
- EVEN_ROWS = "EVEN_ROWS",
- EVEN_COLUMNS = "EVEN_COLUMNS"
-}
-export function orientationFromString(string: string): HexMapRepresentation {
- const normalized = string.toUpperCase().trim()
- switch (normalized) {
- case OrientationConstants.ROWS:
- return { orientation: HexagonOrientation.POINTY_TOP, indentedLines: LineParity.ODD }
- case OrientationConstants.COLUMNS:
- return { orientation: HexagonOrientation.FLAT_TOP, indentedLines: LineParity.ODD }
- case OrientationConstants.EVEN_ROWS:
- return { orientation: HexagonOrientation.POINTY_TOP, indentedLines: LineParity.EVEN }
- case OrientationConstants.EVEN_COLUMNS:
- return { orientation: HexagonOrientation.FLAT_TOP, indentedLines: LineParity.EVEN }
- default:
- return { orientation: HexagonOrientation.POINTY_TOP, indentedLines: LineParity.ODD }
- }
-}
-
-/** Fake "connection" to a "server" that actually just goes back and forth with the console. */
-export class ConsoleConnection {
- public receivedMessages: ClientAction[] = []
- private readonly dispatch: (action: ServerAction) => void
-
- constructor(dispatch: (action: ServerAction) => void) {
- this.dispatch = dispatch
- }
-
- receive(action: ClientAction): void {
- this.receivedMessages.push(action)
- if (isClientActAction(action)) {
- console.log(`Received Sent action containing: ${action.actions.map((value) => `${value.id}/${value.action.type}`).join(", ")}`)
- } else {
- console.log(`Received: ${action.type}`)
- }
- }
-
- public sendSocketConnecting(): void {
- this.dispatch({
- type: SERVER_SOCKET_STARTUP,
- state: SocketState.CONNECTING
- })
- }
-
- public sendSocketConnected(): void {
- this.dispatch({
- type: SERVER_SOCKET_STARTUP,
- state: SocketState.OPEN
- })
- }
-
- public sendHello({color = "#0000FF", displayMode = "ROWS", xid = "TotallyCoolXID", lines = 10, cells = 10}: {
- color?: string,
- displayMode?: string,
- xid?: string,
- lines?: number,
- cells?: number
- } = {}): void {
- this.dispatch({
- type: SERVER_HELLO,
- version: 1,
- state: {
- map: initializeMap({
- lines,
- cellsPerLine: cells,
- displayMode: orientationFromString(displayMode),
- xid
- }),
- user: {activeColor: color}
- }
- })
- }
-
- public sendOK(ids: readonly number[]): void {
- this.dispatch({
- type: SERVER_OK,
- ids
- })
- }
-
- public sendFailed(ids: readonly number[], error: string = "No thanks."): void {
- this.dispatch({
- type: SERVER_FAILED,
- ids,
- error
- })
- }
-
- public sendGoodbye({code = 1000, reason = "Okay, bye then!"}: { code?: number, reason?: string } = {}): void {
- this.dispatch({
- type: SERVER_GOODBYE,
- code,
- reason,
- currentTime: new Date()
- })
- }
-
- public sendColorChange(color: string = "#FF0000FF"): void {
- this.dispatch({
- type: SERVER_ACT,
- actions: [{
- type: USER_ACTIVE_COLOR,
- color
- }]
- })
- }
-
- public sendColorAtTile(color: string = "#FFFF00FF", line: number = 0, cell: number = 0): void {
- this.dispatch({
- type: SERVER_ACT,
- actions: [{
- type: CELL_COLOR,
- at: { line, cell },
- color
- }]
- })
- }
-}
-
-
-export function ConsoleConnector({specialMessage, pendingMessages, nextID}: {specialMessage: ClientAction|null, pendingMessages: readonly SendableAction[], nextID: number}): ReactElement {
- const dispatch = useContext(DispatchContext)
- const connector = useRef(new ConsoleConnection(dispatch || (() => null)))
- const [lastSpecialMessage, setLastSpecialMessage] = useState
(null)
- useEffect(() => {
- // @ts-ignore
- window.fakedServerConnection = connector.current
- }, []);
- useEffect(() => {
- if (dispatch !== null) {
- if (pendingMessages.length > 0) {
- const sentMessages: SentAction[] = pendingMessages.map((action, index) => {
- return { id: index + nextID, action }
- });
- const sentMessage: ClientActAction = {
- type: CLIENT_ACT,
- actions: sentMessages
- };
- connector.current.receive(sentMessage)
- dispatch(sentMessage)
- }
- }
- }, [nextID, dispatch, pendingMessages])
- useEffect(() => {
- if (specialMessage !== null && specialMessage !== lastSpecialMessage) {
- connector.current.receive(specialMessage);
- setLastSpecialMessage(specialMessage);
- }
- }, [specialMessage, lastSpecialMessage, setLastSpecialMessage])
- return Console connection active
-}
\ No newline at end of file
diff --git a/client/src/util/ArrayUtils.ts b/client/src/util/ArrayUtils.ts
index 6d4a943..929f3b4 100644
--- a/client/src/util/ArrayUtils.ts
+++ b/client/src/util/ArrayUtils.ts
@@ -1,7 +1,3 @@
-export function arrayShallowEqual(left: readonly T[], right: readonly T[]): boolean {
- return left.length === right.length && arrayShallowStartsWith(left, right)
-}
-
export function arrayShallowStartsWith(target: readonly T[], prefix: readonly T[]): boolean {
return target.length >= prefix.length && prefix.every((value, index) => target[index] === value)
}
\ No newline at end of file
diff --git a/mage.sh b/mage.sh
index 0895ffc..fb6b15e 100755
--- a/mage.sh
+++ b/mage.sh
@@ -1,12 +1,12 @@
#!/bin/bash
-MAINPATH=$(readlink -e ${BASH_SOURCE%/*})
+SCRIPTPATH=$(readlink -e ${BASH_SOURCE})
+MAINPATH=${SCRIPTPATH%/*}
BUILDTOOLSPATH=${MAINPATH}/buildtools
MAGEPATH=${BUILDTOOLSPATH}/mage
-
if [[ ! -x "$MAGEPATH" ]]; then
echo "go install-ing mage..."
GOBIN="$BUILDTOOLSPATH" go install github.com/magefile/mage@latest
fi
-exec "$MAGEPATH" -d "$MAINPATH" -w "$MAINPATH" "$@"
\ No newline at end of file
+exec "$MAGEPATH" -d "$MAINPATH" -w "$MAINPATH" "$@"
diff --git a/magefile.go b/magefile.go
index 2bc9c95..6b51256 100644
--- a/magefile.go
+++ b/magefile.go
@@ -3,8 +3,30 @@
package main
import (
+ "context"
+ "git.reya.zone/reya/hexmap/proto"
+
// mage:import server
- _ "git.reya.zone/reya/hexmap/server"
+ "git.reya.zone/reya/hexmap/server"
+ "github.com/magefile/mage/mg"
+
// mage:import client
- _ "git.reya.zone/reya/hexmap/client"
+ "git.reya.zone/reya/hexmap/client"
)
+
+type Protobuf mg.Namespace
+
+func (Protobuf) InstallPlugins(ctx context.Context) error {
+ mg.CtxDeps(ctx, server.Protobuf.InstallPlugins, client.Protobuf.InstallPlugins)
+ return nil
+}
+
+func (Protobuf) Build(ctx context.Context) error {
+ mg.SerialCtxDeps(ctx, Protobuf.Clean, Protobuf.InstallPlugins)
+ return proto.Compile(ctx, []proto.ProtocFlagsFunc{server.ProtocFlags, client.ProtocFlags})
+}
+
+func (Protobuf) Clean(ctx context.Context) error {
+ mg.CtxDeps(ctx, server.Protobuf.Clean, client.Protobuf.Clean)
+ return nil
+}
diff --git a/proto/action.proto b/proto/action.proto
index 73b20a5..52d6aca 100644
--- a/proto/action.proto
+++ b/proto/action.proto
@@ -4,11 +4,24 @@ import "coords.proto";
option go_package = "git.reya.zone/reya/hexmap/server/action";
-message CellSetColor {
+message CellSetColorPB {
fixed32 color = 1;
- StorageCoordinates at = 2;
+ StorageCoordinatesPB at = 2;
}
-message UserSetActiveColor {
+message UserSetActiveColorPB {
fixed32 color = 1;
+}
+
+message ClientActionPB {
+ oneof action {
+ CellSetColorPB cell_set_color = 1;
+ UserSetActiveColorPB user_set_active_color = 2;
+ }
+}
+
+message ServerActionPB {
+ oneof action {
+ ClientActionPB client = 1;
+ }
}
\ No newline at end of file
diff --git a/proto/client.proto b/proto/client.proto
index f51fcbe..f493448 100644
--- a/proto/client.proto
+++ b/proto/client.proto
@@ -4,28 +4,25 @@ import "action.proto";
option go_package = "git.reya.zone/reya/hexmap/server/websocket";
-message ClientHello {
+message ClientHelloPB {
uint32 version = 1;
}
-message ClientRefresh {
+message ClientRefreshPB {
}
-message ClientAct {
- message ClientAction {
+message ClientActPB {
+ message IDed {
uint32 id = 1;
- oneof action {
- CellSetColor cell_set_color = 2;
- UserSetActiveColor user_set_active_color = 3;
- }
+ ClientActionPB action = 2;
}
- repeated ClientAction actions = 1;
+ repeated IDed actions = 1;
}
-message ClientCommand {
+message ClientCommandPB {
oneof command {
- ClientHello hello = 1;
- ClientRefresh refresh = 2;
- ClientAct act = 3;
+ ClientHelloPB hello = 1;
+ ClientRefreshPB refresh = 2;
+ ClientActPB act = 3;
}
}
\ No newline at end of file
diff --git a/proto/coords.proto b/proto/coords.proto
index 8634b29..78d2410 100644
--- a/proto/coords.proto
+++ b/proto/coords.proto
@@ -2,7 +2,7 @@ syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
-message StorageCoordinates {
+message StorageCoordinatesPB {
uint32 line = 1;
uint32 cell = 2;
}
\ No newline at end of file
diff --git a/proto/magefile.go b/proto/magefile.go
new file mode 100644
index 0000000..96de9f6
--- /dev/null
+++ b/proto/magefile.go
@@ -0,0 +1,56 @@
+package proto
+
+import (
+ "context"
+ "github.com/magefile/mage/mg"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+func BaseProtocFlags() ([]string, error) {
+ return []string{"-I=proto"}, nil
+}
+
+type ProtocFlagsFunc func() ([]string, error)
+
+func Sources() ([]string, error) {
+ result := []string(nil)
+ err := filepath.WalkDir("proto", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if !d.IsDir() && strings.HasSuffix(path, ".proto") {
+ result = append(result, path)
+ }
+ return nil
+ })
+ return result, err
+}
+
+func Compile(ctx context.Context, withPlugins []ProtocFlagsFunc) error {
+ flags, err := BaseProtocFlags()
+ if err != nil {
+ return err
+ }
+ for _, flagsFunc := range withPlugins {
+ pluginFlags, err := flagsFunc()
+ if err != nil {
+ return err
+ }
+ flags = append(flags, pluginFlags...)
+ }
+ protoFiles, err := Sources()
+ if err != nil {
+ return err
+ }
+ args := append(flags, protoFiles...)
+ cmd := exec.CommandContext(ctx, "protoc", args...)
+ cmd.Stderr = os.Stderr
+ if mg.Verbose() {
+ cmd.Stdout = os.Stdout
+ }
+ return cmd.Run()
+}
diff --git a/proto/map.proto b/proto/map.proto
index 98ac842..0d89838 100644
--- a/proto/map.proto
+++ b/proto/map.proto
@@ -2,32 +2,36 @@ syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
-message HexCell {
+message HexCellPB {
fixed32 color = 1;
}
-message HexLine {
- repeated HexCell cells = 1;
+message HexLinePB {
+ repeated HexCellPB cells = 1;
}
-message HexMap {
+message HexLayerPB {
+ repeated HexLinePB lines = 1;
+}
+
+message HexMapPB {
message Layout {
enum Orientation {
UNKNOWN_ORIENTATION = 0;
- FLAT_TOP_ORIENTATION = 1;
- POINTY_TOP_ORIENTATION = 2;
+ POINTY_TOP = 1;
+ FLAT_TOP = 2;
}
enum LineParity {
- UNKNOWN_LINE_PARITY = 0;
- EVEN_LINE_PARITY = 1;
- ODD_LINE_PARITY = 2;
+ UNKNOWN_LINE = 0;
+ ODD = 1;
+ EVEN = 2;
}
Orientation orientation = 1;
- LineParity line_parity = 2;
+ LineParity indented_lines = 2;
}
- string xid = 1;
+ bytes xid = 1;
uint32 lines = 2;
uint32 cells_per_line = 3;
Layout layout = 4;
- repeated HexLine line_cells = 5;
+ HexLayerPB layer = 5;
}
diff --git a/proto/server.proto b/proto/server.proto
index 1a2f6f5..0cbb916 100644
--- a/proto/server.proto
+++ b/proto/server.proto
@@ -1,41 +1,38 @@
syntax = "proto3";
import "action.proto";
-import "map.proto";
-import "user.proto";
+import "state.proto";
option go_package = "git.reya.zone/reya/hexmap/server/websocket";
-message ServerState {
- HexMap map = 1;
- UserState user = 2;
-}
-
-message ServerHello {
+message ServerHelloPB {
uint32 version = 1;
- ServerState state = 2;
+ SyncableStatePB state = 2;
}
-message ServerRefresh {
- ServerState state = 1;
+message ServerRefreshPB {
+ SyncableStatePB state = 1;
}
-message ServerOK {
+message ServerOKPB {
repeated uint32 ids = 1;
}
-message ServerFailed {
+message ServerFailedPB {
repeated uint32 ids = 1;
string error = 2;
}
-message ServerAction {
- oneof action {
- CellSetColor cell_set_color = 1;
- UserSetActiveColor user_set_active_color = 2;
- }
+message ServerActPB {
+ repeated ServerActionPB actions = 1;
}
-message ServerAct {
- repeated ServerAction actions = 1;
+message ServerCommandPB {
+ oneof command {
+ ServerHelloPB hello = 1;
+ ServerRefreshPB refresh = 2;
+ ServerOKPB ok = 3;
+ ServerFailedPB failed = 4;
+ ServerActPB act = 5;
+ }
}
\ No newline at end of file
diff --git a/proto/state.proto b/proto/state.proto
new file mode 100644
index 0000000..f7320b7
--- /dev/null
+++ b/proto/state.proto
@@ -0,0 +1,11 @@
+syntax = "proto3";
+
+option go_package = "git.reya.zone/reya/hexmap/server/state";
+
+import "map.proto";
+import "user.proto";
+
+message SyncableStatePB {
+ HexMapPB map = 1;
+ UserStatePB user = 2;
+}
\ No newline at end of file
diff --git a/proto/user.proto b/proto/user.proto
index cc7e879..6939c98 100644
--- a/proto/user.proto
+++ b/proto/user.proto
@@ -2,6 +2,6 @@ syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
-message UserState {
+message UserStatePB {
fixed32 color = 1;
}
\ No newline at end of file
diff --git a/server/.gitignore b/server/.gitignore
index 9695e6e..8374028 100644
--- a/server/.gitignore
+++ b/server/.gitignore
@@ -1,2 +1,2 @@
*.pb.go
-*.zap.go
\ No newline at end of file
+proto/
\ No newline at end of file
diff --git a/server/action/action.go b/server/action/action.go
index e16a1ae..5ae920c 100644
--- a/server/action/action.go
+++ b/server/action/action.go
@@ -14,30 +14,38 @@ var (
ErrNoTransparentColors = errors.New("transparent colors not allowed")
)
-type Type string
-
// Action is the interface for actions that can be shared between clients, or between the server and a client.
type Action interface {
zapcore.ObjectMarshaler
- // Type gives the Javascript type that is sent over the wire.
- Type() Type
// Apply causes the action's effects to be applied to s, mutating it in place.
// All 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 ErrNoOp.
// If an action is correctly applied and has an effect, it should return nil.
Apply(s *state.Synced) error
- // fromJSONMap causes the action's state to be overwritten by data from the given map.
- fromJSONMap(data map[string] interface{}) error
}
-type parseAction struct {
- Type string `json:"type"`
+type Client interface {
+ Server
+ // ToClientPB converts the action into a client action protocol buffer.
+ ToClientPB() *ClientActionPB
+}
+
+type Server interface {
+ Action
+ // ToServerPB converts the action into a server action protocol buffer.
+ ToServerPB() *ServerActionPB
+}
+
+func serverPBFromClient(c Client) *ServerActionPB {
+ return &ServerActionPB{
+ Action: &ServerActionPB_Client{Client: c.ToClientPB()},
+ }
}
-type Slice []Action
+type ServerSlice []Server
-func (s Slice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
+func (s ServerSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
var finalErr error = nil
for _, a := range s {
err := encoder.AppendObject(a)
@@ -47,3 +55,58 @@ func (s Slice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
}
return finalErr
}
+
+// CellColor is the action sent when a cell of the map has been colored a different color.
+type CellColor struct {
+ // At is the location of the cell in storage coordinates.
+ At state.StorageCoordinates `json:"at"`
+ // Color is the color the cell has been changed to.
+ Color state.Color `json:"color"`
+}
+
+func (c CellColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
+ encoder.AddString("type", "CellColor")
+ err := encoder.AddObject("at", c.At)
+ encoder.AddString("color", c.Color.String())
+ return err
+}
+
+// Apply sets the target cell's color, or returns ErrNoOp if it can't.
+func (c CellColor) Apply(s *state.Synced) error {
+ if c.Color.A < 0xF {
+ return ErrNoTransparentColors
+ }
+ cell, err := s.Map.Layer.GetCellAt(c.At)
+ if err != nil {
+ return err
+ }
+ if cell.Color == c.Color {
+ return ErrNoOp
+ }
+ cell.Color = c.Color
+ return nil
+}
+
+// UserActiveColor is the action sent when the user's current color, the one being painted with, changes.
+type UserActiveColor struct {
+ // Color is the color that is now active.
+ Color state.Color `json:"color"`
+}
+
+func (c UserActiveColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
+ encoder.AddString("type", "UserActiveColor")
+ encoder.AddString("color", c.Color.String())
+ return nil
+}
+
+// Apply sets the user's active color, or returns ErrNoOp if it can't.
+func (c UserActiveColor) Apply(s *state.Synced) error {
+ if c.Color.A < 0xF {
+ return ErrNoTransparentColors
+ }
+ if s.User.ActiveColor == c.Color {
+ return ErrNoOp
+ }
+ s.User.ActiveColor = c.Color
+ return nil
+}
diff --git a/server/action/action.pbconv.go b/server/action/action.pbconv.go
new file mode 100644
index 0000000..68e5afb
--- /dev/null
+++ b/server/action/action.pbconv.go
@@ -0,0 +1,75 @@
+package action
+
+import "git.reya.zone/reya/hexmap/server/state"
+
+func (x *ServerActionPB) ToGo() (Server, error) {
+ if x == nil {
+ return nil, nil
+ }
+ switch action := x.Action.(type) {
+ case *ServerActionPB_Client:
+ return action.Client.ToGo()
+ default:
+ panic("A case was missed in ServerActionPB.ToGo!")
+ }
+}
+
+func (x *ClientActionPB) ToGo() (Client, error) {
+ if x == nil {
+ return nil, nil
+ }
+ switch action := x.Action.(type) {
+ case *ClientActionPB_CellSetColor:
+ return action.CellSetColor.ToGo()
+ case *ClientActionPB_UserSetActiveColor:
+ return action.UserSetActiveColor.ToGo(), nil
+ default:
+ panic("A case was missed in ClientActionPB.ToGo!")
+ }
+}
+
+func (x *CellSetColorPB) ToGo() (*CellColor, error) {
+ at, err := x.At.ToGo()
+ if err != nil {
+ return nil, err
+ }
+ return &CellColor{
+ At: at,
+ Color: state.ColorFromInt(x.Color),
+ }, nil
+}
+
+func (c CellColor) ToServerPB() *ServerActionPB {
+ return serverPBFromClient(c)
+}
+
+func (c CellColor) ToClientPB() *ClientActionPB {
+ return &ClientActionPB{
+ Action: &ClientActionPB_CellSetColor{
+ CellSetColor: &CellSetColorPB{
+ Color: c.Color.ToInt(),
+ At: c.At.ToPB(),
+ },
+ },
+ }
+}
+
+func (x *UserSetActiveColorPB) ToGo() *UserActiveColor {
+ return &UserActiveColor{
+ Color: state.ColorFromInt(x.Color),
+ }
+}
+
+func (c UserActiveColor) ToServerPB() *ServerActionPB {
+ return serverPBFromClient(c)
+}
+
+func (c UserActiveColor) ToClientPB() *ClientActionPB {
+ return &ClientActionPB{
+ Action: &ClientActionPB_UserSetActiveColor{
+ UserSetActiveColor: &UserSetActiveColorPB{
+ Color: c.Color.ToInt(),
+ },
+ },
+ }
+}
diff --git a/server/action/map.go b/server/action/map.go
deleted file mode 100644
index 76fed37..0000000
--- a/server/action/map.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package action
-
-import (
- "git.reya.zone/reya/hexmap/server/state"
- "go.uber.org/zap/zapcore"
-)
-
-const (
- CellColorType Type = "CELL_COLOR"
-)
-
-// CellColor is the action sent when a cell of the map has been colored a different color.
-type CellColor struct {
- // At is the location of the cell in storage coordinates.
- At state.StorageCoordinates `json:"at"`
- // Color is the color the cell has been changed to.
- Color state.HexColor `json:"color"`
-}
-
-func (c CellColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(CellColorType))
- err := encoder.AddObject("at", c.At)
- encoder.AddString("color", c.Color.String())
- return err
-}
-
-func (c CellColor) Type() Type {
- return CellColorType
-}
-
-// Apply sets the target cell's color, or returns ErrNoOp if it can't.
-func (c CellColor) Apply(s *state.Synced) error {
- if c.Color.A < 0xF {
- return ErrNoTransparentColors
- }
- cell, err := s.Map.LineCells.GetCellAt(c.At)
- if err != nil {
- return err
- }
- if cell.Color == c.Color {
- return ErrNoOp
- }
- cell.Color = c.Color
- return nil
-}
diff --git a/server/action/user.go b/server/action/user.go
deleted file mode 100644
index 2058b6f..0000000
--- a/server/action/user.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package action
-
-import (
- "git.reya.zone/reya/hexmap/server/state"
- "go.uber.org/zap/zapcore"
-)
-
-const (
- UserActiveColorType = "USER_ACTIVE_COLOR"
-)
-
-// UserActiveColor is the action sent when the user's current color, the one being painted with, changes.
-type UserActiveColor struct {
- // Color is the color that is now active.
- Color state.HexColor `json:"color"`
-}
-
-func (c UserActiveColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("color", c.Color.String())
- return nil
-}
-
-// Apply sets the user's active color, or returns ErrNoOp if it can't.
-func (c UserActiveColor) Apply(s *state.Synced) error {
- if c.Color.A < 0xF {
- return ErrNoTransparentColors
- }
- if s.User.ActiveColor == c.Color {
- return ErrNoOp
- }
- s.User.ActiveColor = c.Color
- return nil
-}
\ No newline at end of file
diff --git a/server/magefile.go b/server/magefile.go
index 15a365d..fcdcf89 100644
--- a/server/magefile.go
+++ b/server/magefile.go
@@ -4,11 +4,10 @@ import (
"context"
"fmt"
"git.reya.zone/reya/hexmap/build"
+ "git.reya.zone/reya/hexmap/proto"
"github.com/magefile/mage/mg"
"io/fs"
"os"
- "os/exec"
- "path"
"path/filepath"
"strings"
)
@@ -20,7 +19,7 @@ import (
type Protobuf mg.Namespace
func (Protobuf) InstallGoPlugin(ctx context.Context) error {
- alreadyDone, err := build.HasExecutableInBuildtools("protoc-gen-go")
+ alreadyDone, err := build.HasExecutableInTools("protoc-gen-go")
if err != nil {
return err
}
@@ -34,19 +33,17 @@ func (Protobuf) InstallPlugins(ctx context.Context) {
mg.CtxDeps(ctx, Protobuf.InstallGoPlugin)
}
-func (Protobuf) Build(ctx context.Context) error {
- mg.CtxDeps(ctx, Protobuf.Clean, Protobuf.InstallPlugins)
- tooldir, err := build.GetBuildToolsDir()
+func ProtocFlags() ([]string, error) {
+ buildPath, err := filepath.Abs(filepath.Join(build.ToolsDir, "protoc-gen-go"))
if err != nil {
- return err
- }
- goPluginPathFlag := fmt.Sprintf("--plugin=%s", path.Join(tooldir, "protoc-gen-go"))
- cmd := exec.CommandContext(ctx, "protoc", goPluginPathFlag, "-I=proto", "--go_out=.", "--go_opt=module=git.reya.zone/reya/hexmap", "proto/action.proto", "proto/client.proto", "proto/coords.proto", "proto/map.proto", "proto/server.proto", "proto/user.proto")
- cmd.Stderr = os.Stderr
- if mg.Verbose() {
- cmd.Stdout = os.Stdout
+ return nil, err
}
- return cmd.Run()
+ return []string{"--plugin=" + buildPath, "--go_out=.", "--go_opt=module=git.reya.zone/reya/hexmap"}, nil
+}
+
+func (Protobuf) Build(ctx context.Context) error {
+ mg.SerialCtxDeps(ctx, Protobuf.Clean, Protobuf.InstallPlugins)
+ return proto.Compile(ctx, []proto.ProtocFlagsFunc{ProtocFlags})
}
func (Protobuf) Clean(ctx context.Context) error {
diff --git a/server/room/actor.go b/server/room/actor.go
index 94d3057..7809484 100644
--- a/server/room/actor.go
+++ b/server/room/actor.go
@@ -24,7 +24,7 @@ func (r *room) act() {
case RefreshRequest:
r.sendRefresh(msg.id)
case ApplyRequest:
- msgLogger.Debug("Received action to apply from client", zap.Int("actionId", msg.action.ID))
+ msgLogger.Debug("Received action to apply from client", zap.Uint32("actionId", msg.action.ID))
result := r.applyAction(msg.action.Action)
if result != nil {
r.broadcastAction(client, msg.action.ID, msg.action.Action)
@@ -126,8 +126,8 @@ func (r *room) applyAction(action action.Action) error {
}
// broadcastAction sends an action to everyone other than the original client which requested it.
-func (r *room) broadcastAction(originalClientID xid.ID, originalActionID int, action action.Action) {
- logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Int("actionID", originalActionID), zap.Object("action", action))
+func (r *room) broadcastAction(originalClientID xid.ID, originalActionID uint32, action action.Action) {
+ logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Uint32("actionID", originalActionID), zap.Object("action", action))
broadcast := ActionBroadcast{
id: r.id,
originalClientID: originalClientID,
@@ -144,8 +144,8 @@ func (r *room) broadcastAction(originalClientID xid.ID, originalActionID int, ac
}
// acknowledgeAction sends a response to the original client which requested an action.
-func (r *room) acknowledgeAction(id xid.ID, actionID int, error error) {
- logger := r.logger.With(zap.Stringer("id", id), zap.Int("actionId", actionID), zap.Error(error))
+func (r *room) acknowledgeAction(id xid.ID, actionID uint32, error error) {
+ logger := r.logger.With(zap.Stringer("id", id), zap.Uint32("actionId", actionID), zap.Error(error))
logger.Debug("Responding to client with the status of its action")
client, ok := r.clients[id]
if !ok {
diff --git a/server/room/client.go b/server/room/client.go
index d226986..faf9af1 100644
--- a/server/room/client.go
+++ b/server/room/client.go
@@ -110,7 +110,7 @@ func (c *Client) NewClient(opts NewClientOptions) *Client {
return newClientForRoom(c.roomId, c.outgoingChannel, opts)
}
-// The message created by Refresh causes the client to request a fresh copy of the state.
+// Refresh creates a message which causes the client to request a fresh copy of the state.
func (c *Client) Refresh() RefreshRequest {
if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent")
@@ -120,7 +120,7 @@ func (c *Client) Refresh() RefreshRequest {
}
}
-// The message created by Leave causes the local client to signal that it is shutting down.
+// Leave creates a message which causes the local client to signal that it is shutting down.
// It is important to Leave to avoid dangling clients having messages sent to nothing.
// After sending Leave, the client must confirm that it has been removed by waiting for a LeaveResponse, accompanied by
// the closing of the Client's IncomingChannel if it was a private channel.
@@ -135,7 +135,7 @@ func (c *Client) Leave() LeaveRequest {
}
}
-// The message created by Stop causes the local client to signal that it is shutting down.
+// Stop creates a message which causes the local client to signal that it is shutting down.
// It is important to Stop when the room needs to be shut down.
// After sending Stop, the client must confirm that it has been removed by waiting for a ShutdownRequest, which should
// be handled normally.
diff --git a/server/room/clientmessage.go b/server/room/clientmessage.go
index 4254db6..062dae9 100644
--- a/server/room/clientmessage.go
+++ b/server/room/clientmessage.go
@@ -9,7 +9,7 @@ import (
// ClientMessage marks messages coming from clients to the room.
type ClientMessage interface {
zapcore.ObjectMarshaler
- // SourceID is the id of the client sending the message.
+ // ClientID is the id of the client sending the message.
ClientID() xid.ID
}
diff --git a/server/room/message.go b/server/room/message.go
index 35e575b..9595d64 100644
--- a/server/room/message.go
+++ b/server/room/message.go
@@ -63,7 +63,7 @@ func (r RefreshResponse) CurrentState() *state.Synced {
type ApplyResponse struct {
id xid.ID
// actionID is the ID of the action that completed or failed.
- actionID int
+ actionID uint32
// result is nil if the action was completed, or an error if it failed.
result error
}
@@ -71,7 +71,7 @@ type ApplyResponse struct {
func (a ApplyResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ApplyResponse")
encoder.AddString("id", a.id.String())
- encoder.AddInt("actionId", a.actionID)
+ encoder.AddUint32("actionId", a.actionID)
encoder.AddBool("success", a.result == nil)
if a.result != nil {
encoder.AddString("failure", a.result.Error())
@@ -99,7 +99,7 @@ type ActionBroadcast struct {
// originalClientID is the client that sent the action in the first place.
originalClientID xid.ID
// originalActionID is the ID that the client that sent the action sent.
- originalActionID int
+ originalActionID uint32
// action is the action that succeeded.
action action.Action
}
@@ -108,7 +108,7 @@ func (a ActionBroadcast) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ActionBroadcast")
encoder.AddString("id", a.id.String())
encoder.AddString("originalClientId", a.originalClientID.String())
- encoder.AddInt("originalActionId", a.originalActionID)
+ encoder.AddUint32("originalActionId", a.originalActionID)
return encoder.AddObject("action", a.action)
}
diff --git a/server/state/color.go b/server/state/color.go
new file mode 100644
index 0000000..fdab1cb
--- /dev/null
+++ b/server/state/color.go
@@ -0,0 +1,46 @@
+package state
+
+import "fmt"
+
+// Color 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 Color 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
+}
+
+// String prints the Color 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 (c Color) String() string {
+ if c.R%0x11 == 0 && c.G%0x11 == 0 && c.B%0x11 == 0 && c.A%0x11 == 0 {
+ // Short form works.
+ if c.A == 0xFF {
+ // It's great when it's easy!
+ return fmt.Sprintf("#%01X%01X%01X", c.R/0x11, c.G/0x11, c.B/0x11)
+ } else {
+ // Just need to add the alpha.
+ return fmt.Sprintf("#%01X%01X%01X%01X", c.R/0x11, c.G/0x11, c.B/0x11, c.A/0x11)
+ }
+ } else {
+ // Gotta use long form.
+ if c.A == 0xFF {
+ // Can skip the alpha channel, though.
+ return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
+ } else {
+ // Doing things the hard way.
+ return fmt.Sprintf("#%02X%02X%02X%02X", c.R, c.G, c.B, c.A)
+ }
+ }
+}
diff --git a/server/state/color.pbconv.go b/server/state/color.pbconv.go
new file mode 100644
index 0000000..4549633
--- /dev/null
+++ b/server/state/color.pbconv.go
@@ -0,0 +1,16 @@
+package state
+
+// ColorFromInt decodes a packed uint32 into a hex color.
+func ColorFromInt(value uint32) Color {
+ return Color{
+ R: uint8((value >> 24) & 0xFF),
+ G: uint8((value >> 16) & 0xFF),
+ B: uint8((value >> 8) & 0xFF),
+ A: uint8((value >> 0) & 0xFF),
+ }
+}
+
+// ToInt packs a hex color into a uint32.
+func (c Color) ToInt() uint32 {
+ return uint32(c.R)<<24 | uint32(c.G)<<16 | uint32(c.B)<<8 | uint32(c.A)
+}
diff --git a/server/state/coordinates.go b/server/state/coords.go
similarity index 93%
rename from server/state/coordinates.go
rename to server/state/coords.go
index 6ca05e1..a22a28b 100644
--- a/server/state/coordinates.go
+++ b/server/state/coords.go
@@ -1,6 +1,8 @@
package state
-import "go.uber.org/zap/zapcore"
+import (
+ "go.uber.org/zap/zapcore"
+)
// StorageCoordinates gives the coordinates of a cell in a form optimized for storage.
type StorageCoordinates struct {
diff --git a/server/state/coords.pbconv.go b/server/state/coords.pbconv.go
new file mode 100644
index 0000000..a8e8f47
--- /dev/null
+++ b/server/state/coords.pbconv.go
@@ -0,0 +1,25 @@
+package state
+
+import (
+ "errors"
+ "math"
+)
+
+var ErrOutOfBounds = errors.New("coordinate was out of bounds")
+
+func (x *StorageCoordinatesPB) ToGo() (StorageCoordinates, error) {
+ if x.Line > math.MaxUint8 || x.Cell > math.MaxUint8 {
+ return StorageCoordinates{}, ErrOutOfBounds
+ }
+ return StorageCoordinates{
+ Line: uint8(x.Line),
+ Cell: uint8(x.Cell),
+ }, nil
+}
+
+func (s StorageCoordinates) ToPB() *StorageCoordinatesPB {
+ return &StorageCoordinatesPB{
+ Line: uint32(s.Line),
+ Cell: uint32(s.Cell),
+ }
+}
diff --git a/server/state/hexcolor.go b/server/state/hexcolor.go
deleted file mode 100644
index 9f2a5c9..0000000
--- a/server/state/hexcolor.go
+++ /dev/null
@@ -1,104 +0,0 @@
-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)
- }
- }
-}
diff --git a/server/state/hexorientation.go b/server/state/hexorientation.go
deleted file mode 100644
index 60c3449..0000000
--- a/server/state/hexorientation.go
+++ /dev/null
@@ -1,51 +0,0 @@
-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)
- }
-}
diff --git a/server/state/lineparity.go b/server/state/lineparity.go
deleted file mode 100644
index 0f08027..0000000
--- a/server/state/lineparity.go
+++ /dev/null
@@ -1,49 +0,0 @@
-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)
- }
-}
diff --git a/server/state/hexmap.go b/server/state/map.go
similarity index 58%
rename from server/state/hexmap.go
rename to server/state/map.go
index 24e7291..fa1a2ba 100644
--- a/server/state/hexmap.go
+++ b/server/state/map.go
@@ -6,22 +6,72 @@ import (
"go.uber.org/zap/zapcore"
)
-// HexMapRepresentation combines HexOrientation and LineParity to represent a map's display mode.
-type HexMapRepresentation struct {
+// HexOrientation is the enum for the direction hexes are facing.
+type HexOrientation uint8
+
+const (
+ // UnknownOrientation indicates that an invalid orientation was specified.
+ UnknownOrientation HexOrientation = 0
+ // 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
+)
+
+// 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)
+ }
+}
+
+// LineParity indicates whether odd or even lines are indented.
+type LineParity uint8
+
+const (
+ // UnknownParity indicates that parity was not specified or unknown.
+ UnknownParity LineParity = 0
+ // 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
+)
+
+// 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)
+ }
+}
+
+// Layout combines HexOrientation and LineParity to represent a map's display mode.
+type Layout struct {
Orientation HexOrientation `json:"orientation"`
IndentedLines LineParity `json:"indentedLines"`
}
-func (h HexMapRepresentation) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("orientation", h.Orientation.String())
- encoder.AddString("indentedLines", h.IndentedLines.String())
+func (l Layout) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
+ encoder.AddString("orientation", l.Orientation.String())
+ encoder.AddString("indentedLines", l.IndentedLines.String())
return nil
}
// 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"`
+ Color Color `json:"color"`
}
func (h HexCell) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
@@ -98,20 +148,20 @@ type HexMap struct {
// 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;
+ // Layout is the orientation and line parity used to display the map.
+ Layout Layout `json:"displayMode"`
+ // Layer contains the actual map data.
+ // Layer 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"`
+ Layer HexLayer `json:"lineCells"`
}
func (m HexMap) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("id", m.XID.String())
encoder.AddUint8("lines", m.Lines)
encoder.AddUint8("cellsPerLine", m.CellsPerLine)
- displayModeErr := encoder.AddObject("displayMode", m.DisplayMode)
- lineCellsErr := encoder.AddArray("lineCells", m.LineCells)
+ displayModeErr := encoder.AddObject("displayMode", m.Layout)
+ lineCellsErr := encoder.AddArray("lineCells", m.Layer)
if displayModeErr != nil {
return displayModeErr
} else {
@@ -125,7 +175,7 @@ func (m HexMap) Copy() HexMap {
XID: m.XID,
Lines: m.Lines,
CellsPerLine: m.CellsPerLine,
- DisplayMode: m.DisplayMode,
- LineCells: m.LineCells.Copy(),
+ Layout: m.Layout,
+ Layer: m.Layer.Copy(),
}
}
diff --git a/server/state/map.pbconv.go b/server/state/map.pbconv.go
new file mode 100644
index 0000000..2afdab2
--- /dev/null
+++ b/server/state/map.pbconv.go
@@ -0,0 +1,142 @@
+package state
+
+import (
+ "github.com/rs/xid"
+ "math"
+)
+
+func (x HexMapPB_Layout_Orientation) ToGo() HexOrientation {
+ switch x {
+ case HexMapPB_Layout_POINTY_TOP:
+ return PointyTop
+ case HexMapPB_Layout_FLAT_TOP:
+ return FlatTop
+ default:
+ return UnknownOrientation
+ }
+}
+
+func (o HexOrientation) ToPB() HexMapPB_Layout_Orientation {
+ switch o {
+ case PointyTop:
+ return HexMapPB_Layout_POINTY_TOP
+ case FlatTop:
+ return HexMapPB_Layout_FLAT_TOP
+ default:
+ return HexMapPB_Layout_UNKNOWN_ORIENTATION
+ }
+}
+
+func (x HexMapPB_Layout_LineParity) ToGo() LineParity {
+ switch x {
+ case HexMapPB_Layout_EVEN:
+ return EvenLines
+ case HexMapPB_Layout_ODD:
+ return OddLines
+ default:
+ return UnknownParity
+ }
+}
+
+func (o LineParity) ToPB() HexMapPB_Layout_LineParity {
+ switch o {
+ case OddLines:
+ return HexMapPB_Layout_ODD
+ case EvenLines:
+ return HexMapPB_Layout_EVEN
+ default:
+ return HexMapPB_Layout_UNKNOWN_LINE
+ }
+}
+
+func (x *HexMapPB_Layout) ToGo() Layout {
+ return Layout{
+ Orientation: x.Orientation.ToGo(),
+ IndentedLines: x.IndentedLines.ToGo(),
+ }
+}
+
+func (l Layout) ToPB() *HexMapPB_Layout {
+ return &HexMapPB_Layout{
+ Orientation: l.Orientation.ToPB(),
+ IndentedLines: l.IndentedLines.ToPB(),
+ }
+}
+
+func (x *HexCellPB) ToGo() HexCell {
+ return HexCell{
+ Color: ColorFromInt(x.Color),
+ }
+}
+
+func (h HexCell) ToPB() *HexCellPB {
+ return &HexCellPB{
+ Color: h.Color.ToInt(),
+ }
+}
+
+func (x *HexLinePB) ToGo() HexLine {
+ r := make(HexLine, len(x.Cells))
+ for index, cell := range x.Cells {
+ r[index] = cell.ToGo()
+ }
+ return r
+}
+
+func (l HexLine) ToPB() *HexLinePB {
+ cells := make([]*HexCellPB, len(l))
+ for index, cell := range l {
+ cells[index] = cell.ToPB()
+ }
+ return &HexLinePB{
+ Cells: cells,
+ }
+}
+
+func (x *HexLayerPB) ToGo() HexLayer {
+ r := make(HexLayer, len(x.Lines))
+ for index, line := range x.Lines {
+ r[index] = line.ToGo()
+ }
+ return r
+}
+
+func (l HexLayer) ToPB() *HexLayerPB {
+ lines := make([]*HexLinePB, len(l))
+ for index, line := range l {
+ lines[index] = line.ToPB()
+ }
+ return &HexLayerPB{
+ Lines: lines,
+ }
+}
+
+func (x *HexMapPB) ToGo() (HexMap, error) {
+ pbId, err := xid.FromBytes(x.Xid)
+ if err != nil {
+ return HexMap{}, err
+ }
+ if x.Lines > math.MaxUint8 {
+ return HexMap{}, ErrOutOfBounds
+ }
+ if x.CellsPerLine > math.MaxUint8 {
+ return HexMap{}, ErrOutOfBounds
+ }
+ return HexMap{
+ XID: pbId,
+ Lines: uint8(x.Lines),
+ CellsPerLine: uint8(x.CellsPerLine),
+ Layout: x.Layout.ToGo(),
+ Layer: x.Layer.ToGo(),
+ }, nil
+}
+
+func (m HexMap) ToPB() *HexMapPB {
+ return &HexMapPB{
+ Xid: m.XID.Bytes(),
+ Lines: uint32(m.Lines),
+ CellsPerLine: uint32(m.CellsPerLine),
+ Layout: m.Layout.ToPB(),
+ Layer: m.Layer.ToPB(),
+ }
+}
diff --git a/server/state/synced.go b/server/state/state.go
similarity index 90%
rename from server/state/synced.go
rename to server/state/state.go
index 782c0a9..b28e69a 100644
--- a/server/state/synced.go
+++ b/server/state/state.go
@@ -6,8 +6,8 @@ import (
// Synced contains all state that is synced between the server and its clients.
type Synced struct {
- Map HexMap `json:"map"`
- User UserData `json:"user"`
+ Map HexMap `json:"map"`
+ User UserState `json:"user"`
}
func (s *Synced) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
diff --git a/server/state/state.pbconv.go b/server/state/state.pbconv.go
new file mode 100644
index 0000000..62c7f91
--- /dev/null
+++ b/server/state/state.pbconv.go
@@ -0,0 +1,20 @@
+package state
+
+func (x *SyncableStatePB) ToGo() (Synced, error) {
+ pbMap, err := x.Map.ToGo()
+ if err != nil {
+ return Synced{}, err
+ }
+ user := x.User.ToGo()
+ return Synced{
+ Map: pbMap,
+ User: user,
+ }, nil
+}
+
+func (s Synced) ToPB() *SyncableStatePB {
+ return &SyncableStatePB{
+ Map: s.Map.ToPB(),
+ User: s.User.ToPB(),
+ }
+}
diff --git a/server/state/user.go b/server/state/user.go
index 8724b6a..6a6d103 100644
--- a/server/state/user.go
+++ b/server/state/user.go
@@ -2,20 +2,20 @@ package state
import "go.uber.org/zap/zapcore"
-// 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:"activeColor"`
+// UserState contains data about clients that is synced between client and server.
+// Unlike the map, UserState is not persisted to disk, and all UserState is lost on shutdown.
+type UserState struct {
+ ActiveColor Color `json:"activeColor"`
}
-func (u UserData) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
+func (u UserState) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("activeColor", u.ActiveColor.String())
return nil
}
-// Copy creates a deep copy of this UserData.
-func (u UserData) Copy() UserData {
- return UserData{
+// Copy creates a deep copy of this UserState.
+func (u UserState) Copy() UserState {
+ return UserState{
ActiveColor: u.ActiveColor,
}
}
diff --git a/server/state/user.pbconv.go b/server/state/user.pbconv.go
new file mode 100644
index 0000000..be9a135
--- /dev/null
+++ b/server/state/user.pbconv.go
@@ -0,0 +1,13 @@
+package state
+
+func (x *UserStatePB) ToGo() UserState {
+ return UserState{
+ ActiveColor: ColorFromInt(x.Color),
+ }
+}
+
+func (u UserState) ToPB() *UserStatePB {
+ return &UserStatePB{
+ Color: u.ActiveColor.ToInt(),
+ }
+}
diff --git a/server/websocket/client.go b/server/websocket/client.go
index c66a6eb..653ce13 100644
--- a/server/websocket/client.go
+++ b/server/websocket/client.go
@@ -8,34 +8,22 @@ import (
// ClientCommandType is an enum type for the client's protocol messages.
type ClientCommandType string
-const (
- ClientHelloType ClientCommandType = "HELLO"
- ClientRefreshType ClientCommandType = "REFRESH"
- ClientActType ClientCommandType = "ACT"
- ClientGoodbyeType ClientCommandType = GoodbyeType
- ClientMalformedCommandType ClientCommandType = "(malformed command)"
-)
-
// ClientCommand s are those sent by the client.
type ClientCommand interface {
zapcore.ObjectMarshaler
- // ClientType gives the type constant that will be sent on or read from the wire.
- ClientType() ClientCommandType
+ // ToClientPB converts the command to a client protocol buffer which will be sent on the wire.
+ ToClientPB() *ClientCommandPB
}
// ClientHello is the command sent by the client when it first establishes the connection.
type ClientHello struct {
// Version is the protocol version the client is running.
- Version int `json:"version"`
-}
-
-func (c ClientHello) ClientType() ClientCommandType {
- return ClientHelloType
+ Version uint32 `json:"version"`
}
func (c ClientHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ClientHelloType))
- encoder.AddInt("version", c.Version)
+ encoder.AddString("type", "Hello")
+ encoder.AddUint32("version", c.Version)
return nil
}
@@ -43,25 +31,21 @@ func (c ClientHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
type ClientRefresh struct {
}
-func (c ClientRefresh) ClientType() ClientCommandType {
- return ClientRefreshType
-}
-
func (c ClientRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ClientRefreshType))
+ encoder.AddString("type", "Refresh")
return nil
}
// IDed contains a pair of ID and Action, as sent by the client.
type IDed struct {
// ID contains the arbitrary ID that was sent by the client, for identifying the action in future messages.
- ID int `json:"id"`
+ ID uint32 `json:"id"`
// Action contains the action that was actually being sent.
- Action action.Action `json:"action"`
+ Action action.Client `json:"action"`
}
func (i IDed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddInt("id", i.ID)
+ encoder.AddUint32("id", i.ID)
return encoder.AddObject("action", i.Action)
}
@@ -84,12 +68,8 @@ type ClientAct struct {
Actions IDPairs `json:"actions"`
}
-func (c ClientAct) ClientType() ClientCommandType {
- return ClientActType
-}
-
func (c ClientAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ClientActType))
+ encoder.AddString("type", "Act")
return encoder.AddArray("actions", c.Actions)
}
@@ -101,11 +81,7 @@ type ClientMalformed struct {
}
func (c ClientMalformed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ClientMalformedCommandType))
+ encoder.AddString("type", "(malformed command)")
encoder.AddString("error", c.Error.Error())
return nil
}
-
-func (c ClientMalformed) ClientType() ClientCommandType {
- return ClientMalformedCommandType
-}
diff --git a/server/websocket/client.pbconv.go b/server/websocket/client.pbconv.go
new file mode 100644
index 0000000..29b1cd6
--- /dev/null
+++ b/server/websocket/client.pbconv.go
@@ -0,0 +1,96 @@
+package websocket
+
+func (x *ClientCommandPB) ToGo() (ClientCommand, error) {
+ switch msg := x.Command.(type) {
+ case *ClientCommandPB_Hello:
+ return msg.Hello.ToGo(), nil
+ case *ClientCommandPB_Refresh:
+ return msg.Refresh.ToGo(), nil
+ case *ClientCommandPB_Act:
+ return msg.Act.ToGo()
+ default:
+ panic("A case was missed in ClientCommandPB.ToGo!")
+ }
+}
+
+func (x *ClientHelloPB) ToGo() ClientHello {
+ return ClientHello{
+ Version: x.Version,
+ }
+}
+
+func (c ClientHello) ToClientPB() *ClientCommandPB {
+ return &ClientCommandPB{
+ Command: &ClientCommandPB_Hello{
+ Hello: &ClientHelloPB{
+ Version: c.Version,
+ },
+ },
+ }
+}
+
+func (*ClientRefreshPB) ToGo() ClientRefresh {
+ return ClientRefresh{}
+}
+
+func (c ClientRefresh) ToClientPB() *ClientCommandPB {
+ return &ClientCommandPB{
+ Command: &ClientCommandPB_Refresh{
+ Refresh: &ClientRefreshPB{},
+ },
+ }
+}
+
+func (x *ClientActPB_IDed) ToGo() (IDed, error) {
+ action, err := x.Action.ToGo()
+ if err != nil {
+ return IDed{}, nil
+ }
+ return IDed{
+ ID: x.Id,
+ Action: action,
+ }, nil
+}
+
+func (i IDed) ToPB() *ClientActPB_IDed {
+ return &ClientActPB_IDed{
+ Id: i.ID,
+ Action: i.Action.ToClientPB(),
+ }
+}
+
+func (x *ClientActPB) ToGo() (ClientAct, error) {
+ actions := make(IDPairs, len(x.Actions))
+ for index, ided := range x.Actions {
+ action, err := ided.ToGo()
+ if err != nil {
+ return ClientAct{}, err
+ }
+ actions[index] = action
+ }
+ return ClientAct{
+ Actions: actions,
+ }, nil
+}
+
+func (c ClientAct) ToClientPB() *ClientCommandPB {
+ actions := make([]*ClientActPB_IDed, len(c.Actions))
+ for index, ided := range c.Actions {
+ actions[index] = ided.ToPB()
+ }
+ return &ClientCommandPB{
+ Command: &ClientCommandPB_Act{
+ Act: &ClientActPB{
+ Actions: actions,
+ },
+ },
+ }
+}
+
+func (ClientMalformed) ToClientPB() *ClientCommandPB {
+ return nil
+}
+
+func (SocketClosed) ToClientPB() *ClientCommandPB {
+ return nil
+}
diff --git a/server/websocket/reader.go b/server/websocket/reader.go
index a6d9a99..fc08ea5 100644
--- a/server/websocket/reader.go
+++ b/server/websocket/reader.go
@@ -1,19 +1,18 @@
package websocket
import (
- "bytes"
- "encoding/json"
"fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
+ "google.golang.org/protobuf/proto"
"time"
)
type reader struct {
// conn is the connection to the client read from by the reader.
- conn *websocket.Conn
+ conn *websocket.Conn
// channel is the channel that the reader sends messages it has received on.
- channel chan ClientCommand
+ channel chan ClientCommand
// readNotifications is the channel that alerts are sent to the writer on, to let it know to
readNotifications chan<- time.Time
// logger is the logger used to record the state of the reader, primarily in Debug level.
@@ -29,29 +28,6 @@ func (i InvalidMessageType) Error() string {
return fmt.Sprintf("invalid message type %d", i.MessageType)
}
-// InvalidCommandType is placed in ClientMalformed when the type of the command was unknown.
-type InvalidCommandType struct {
- CommandType ClientCommandType
-}
-
-func (i InvalidCommandType) Error() string {
- return fmt.Sprintf("invalid command type %s", i.CommandType)
-}
-
-// InvalidPayload is placed in ClientMalformed when the payload could not be parsed.
-type InvalidPayload struct {
- CommandType ClientCommandType
- Cause error
-}
-
-func (i InvalidPayload) Error() string {
- return fmt.Sprintf("command type %s had invalid payload: %s", i.CommandType, i.Cause)
-}
-
-func (i InvalidPayload) Unwrap() error {
- return i.Cause
-}
-
// act contains the main read loop of the reader.
func (r *reader) act() {
defer r.shutdown()
@@ -72,14 +48,14 @@ func (r *reader) act() {
if websocket.IsCloseError(err, StandardClientCloseTypes...) {
typedErr := err.(*websocket.CloseError)
r.logger.Debug("Received normal close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text))
- closure = SocketClosed { Code: typedErr.Code, Text: typedErr.Text }
+ closure = SocketClosed{Code: typedErr.Code, Text: typedErr.Text}
} else if websocket.IsUnexpectedCloseError(err, StandardClientCloseTypes...) {
typedErr := err.(*websocket.CloseError)
r.logger.Warn("Received unexpected close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text))
- closure = SocketClosed { Code: typedErr.Code, Text: typedErr.Text }
+ closure = SocketClosed{Code: typedErr.Code, Text: typedErr.Text}
} else {
r.logger.Error("Error while reading message, shutting down", zap.Error(err))
- closure = SocketClosed { Error: err }
+ closure = SocketClosed{Error: err}
}
r.logger.Debug("Sending close message to reader", zap.Object("closeMessage", closure))
r.channel <- closure
@@ -94,7 +70,7 @@ func (r *reader) act() {
// parseCommand attempts to parse the incoming message
func (r *reader) parseCommand(socketType int, data []byte) ClientCommand {
- if socketType != websocket.TextMessage {
+ if socketType != websocket.BinaryMessage {
err := &InvalidMessageType{
MessageType: socketType,
}
@@ -103,58 +79,19 @@ func (r *reader) parseCommand(socketType int, data []byte) ClientCommand {
Error: err,
}
}
- r.logger.Debug("Received command, parse")
- parts := bytes.SplitN(data, []byte(" "), 2)
- commandBytes := parts[0]
- payloadJson := parts[1]
- var command ClientCommandType
- if len(payloadJson) == 0 {
- // Since there's no payload, we expect the command to end with an exclamation point.
- if bytes.HasSuffix(commandBytes, []byte("!")) {
- command = ClientCommandType(bytes.TrimSuffix(commandBytes, []byte("!")))
- } else {
- r.logger.Warn("Received command not fitting the protocol: has no payload but no ! after command type")
- command = ClientCommandType(commandBytes)
- }
- } else {
- command = ClientCommandType(commandBytes)
- }
- switch command {
- case ClientHelloType:
- hello := &ClientHello{}
- err := json.Unmarshal(payloadJson, hello)
- if err != nil {
- return ClientMalformed{
- Error: &InvalidPayload{
- CommandType: command,
- Cause: err,
- },
- }
- }
- return hello
- case ClientRefreshType:
- if len(payloadJson) != 0 {
- r.logger.Warn("Received command not fitting the protocol: has payload for payloadless Refresh command")
- }
- refresh := &ClientRefresh{}
- return refresh
- case ClientActType:
- act := &ClientAct{}
- err := json.Unmarshal(payloadJson, act)
- if err != nil {
- return ClientMalformed{
- Error: &InvalidPayload{
- CommandType: command,
- Cause: err,
- },
- }
- }
- return act
- default:
+ r.logger.Debug("Received command, parsing")
+ var cmdPb ClientCommandPB
+ err := proto.Unmarshal(data, &cmdPb)
+ if err != nil {
return ClientMalformed{
- Error: InvalidCommandType{CommandType: command},
+ Error: err,
}
}
+ cmd, err := (&cmdPb).ToGo()
+ if err != nil {
+ return ClientMalformed{Error: err}
+ }
+ return cmd
}
// updateDeadlines extends the time limit for pongs, and instructs the writer to hold off on sending a ping for the next PingDelay.
@@ -178,4 +115,4 @@ func (r *reader) shutdown() {
close(r.readNotifications)
r.readNotifications = nil
r.conn = nil
-}
\ No newline at end of file
+}
diff --git a/server/websocket/server.go b/server/websocket/server.go
index 499e2a2..1a34d38 100644
--- a/server/websocket/server.go
+++ b/server/websocket/server.go
@@ -9,40 +9,27 @@ import (
// ServerMessageType is an enum type for the server's messages.
type ServerMessageType string
-const (
- ServerHelloType ServerMessageType = "HELLO"
- ServerRefreshType ServerMessageType = "REFRESH"
- ServerOKType ServerMessageType = "OK"
- ServerFailedType ServerMessageType = "FAILED"
- ServerActType ServerMessageType = "ACT"
- ServerGoodbyeType ServerMessageType = GoodbyeType
-)
-
// ServerCommand s are sent by the server to the client.
type ServerCommand interface {
zapcore.ObjectMarshaler
- // ServerType returns the type constant that will be sent on the wire.
- ServerType() ServerMessageType
+ // ToServerPB converts the command to a server protocol buffer which will be sent on the wire.
+ ToServerPB() *ServerCommandPB
}
// ServerHello is the command sent to establish the current state of the server when a new client connects.
type ServerHello struct {
// Version is the protocol version the server is running.
- Version int `json:"version"`
+ Version uint32 `json:"version"`
// State is the complete state of the server as of when the client joined.
State *state.Synced `json:"state"`
}
func (s ServerHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ServerHelloType))
- encoder.AddInt("version", s.Version)
+ encoder.AddString("type", "Hello")
+ encoder.AddUint32("version", s.Version)
return encoder.AddObject("state", s.State)
}
-func (s ServerHello) ServerType() ServerMessageType {
- return ServerHelloType
-}
-
// ServerRefresh is the command sent to reestablish the current state of the server in response to ClientRefresh.
type ServerRefresh struct {
// State is the complete state of the server as of when the corresponding ClientRefresh was processed.
@@ -50,19 +37,15 @@ type ServerRefresh struct {
}
func (s ServerRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ServerRefreshType))
+ encoder.AddString("type", "Refresh")
return encoder.AddObject("state", s.State)
}
-func (s ServerRefresh) ServerType() ServerMessageType {
- return ServerRefreshType
-}
-
-type IDSlice []int
+type IDSlice []uint32
func (i IDSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
for _, v := range i {
- encoder.AppendInt(v)
+ encoder.AppendUint32(v)
}
return nil
}
@@ -76,14 +59,10 @@ type ServerOK struct {
}
func (s ServerOK) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ServerOKType))
+ encoder.AddString("type", "OK")
return encoder.AddArray("ids", s.IDs)
}
-func (s ServerOK) ServerType() ServerMessageType {
- return ServerOKType
-}
-
// ServerFailed is the command sent when one or more client actions have been rejected.
type ServerFailed struct {
// IDs contains the IDs of the actions which were rejected, in the order they were rejected.
@@ -95,28 +74,20 @@ type ServerFailed struct {
}
func (s ServerFailed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ServerFailedType))
+ encoder.AddString("type", "Failed")
err := encoder.AddArray("ids", s.IDs)
encoder.AddString("error", s.Error)
return err
}
-func (s ServerFailed) ServerType() ServerMessageType {
- return ServerFailedType
-}
-
// ServerAct is the command sent when one or more client actions from other clients have been accepted and applied.
// The client's own actions will never be included in this command.
type ServerAct struct {
// Actions contains the actions that are now being applied.
- Actions action.Slice `json:"actions"`
+ Actions action.ServerSlice `json:"actions"`
}
func (s ServerAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
- encoder.AddString("type", string(ServerActType))
+ encoder.AddString("type", "Act")
return encoder.AddArray("actions", s.Actions)
}
-
-func (s ServerAct) ServerType() ServerMessageType {
- return ServerActType
-}
diff --git a/server/websocket/server.pbconv.go b/server/websocket/server.pbconv.go
new file mode 100644
index 0000000..0d7ee15
--- /dev/null
+++ b/server/websocket/server.pbconv.go
@@ -0,0 +1,136 @@
+package websocket
+
+import "git.reya.zone/reya/hexmap/server/action"
+
+func (x *ServerCommandPB) ToGo() (ServerCommand, error) {
+ switch msg := x.Command.(type) {
+ case *ServerCommandPB_Hello:
+ return msg.Hello.ToGo()
+ case *ServerCommandPB_Refresh:
+ return msg.Refresh.ToGo()
+ case *ServerCommandPB_Ok:
+ return msg.Ok.ToGo(), nil
+ case *ServerCommandPB_Failed:
+ return msg.Failed.ToGo(), nil
+ case *ServerCommandPB_Act:
+ return msg.Act.ToGo()
+ default:
+ panic("A case was missed in ServerCommandPB.ToGo!")
+ }
+}
+
+func (x *ServerHelloPB) ToGo() (ServerHello, error) {
+ state, err := x.State.ToGo()
+ if err != nil {
+ return ServerHello{}, err
+ }
+ return ServerHello{
+ Version: x.Version,
+ State: &state,
+ }, nil
+}
+
+func (s ServerHello) ToServerPB() *ServerCommandPB {
+ return &ServerCommandPB{
+ Command: &ServerCommandPB_Hello{
+ Hello: &ServerHelloPB{
+ Version: s.Version,
+ State: s.State.ToPB(),
+ },
+ },
+ }
+}
+
+func (x *ServerRefreshPB) ToGo() (ServerRefresh, error) {
+ state, err := x.State.ToGo()
+ if err != nil {
+ return ServerRefresh{}, err
+ }
+ return ServerRefresh{
+ State: &state,
+ }, nil
+}
+
+func (s ServerRefresh) ToServerPB() *ServerCommandPB {
+ return &ServerCommandPB{
+ Command: &ServerCommandPB_Refresh{
+ Refresh: &ServerRefreshPB{
+ State: s.State.ToPB(),
+ },
+ },
+ }
+}
+
+func (x *ServerOKPB) ToGo() ServerOK {
+ ids := make(IDSlice, len(x.Ids))
+ copy(ids, x.Ids)
+ return ServerOK{
+ IDs: ids,
+ }
+}
+
+func (s ServerOK) ToServerPB() *ServerCommandPB {
+ ids := make([]uint32, len(s.IDs))
+ copy(ids, s.IDs)
+ return &ServerCommandPB{
+ Command: &ServerCommandPB_Ok{
+ Ok: &ServerOKPB{
+ Ids: ids,
+ },
+ },
+ }
+}
+
+func (x *ServerFailedPB) ToGo() ServerFailed {
+ ids := make(IDSlice, len(x.Ids))
+ copy(ids, x.Ids)
+ return ServerFailed{
+ IDs: ids,
+ Error: x.Error,
+ }
+}
+
+func (s ServerFailed) ToServerPB() *ServerCommandPB {
+ ids := make([]uint32, len(s.IDs))
+ copy(ids, s.IDs)
+ return &ServerCommandPB{
+ Command: &ServerCommandPB_Failed{
+ Failed: &ServerFailedPB{
+ Ids: ids,
+ Error: s.Error,
+ },
+ },
+ }
+}
+
+func (x *ServerActPB) ToGo() (ServerAct, error) {
+ actions := make(action.ServerSlice, len(x.Actions))
+ for index, act := range x.Actions {
+ convertedAct, err := act.ToGo()
+ if err != nil {
+ return ServerAct{}, err
+ }
+ actions[index] = convertedAct
+ }
+ return ServerAct{
+ Actions: actions,
+ }, nil
+}
+
+func (s ServerAct) ToServerPB() *ServerCommandPB {
+ actions := make([]*action.ServerActionPB, len(s.Actions))
+ for index, act := range s.Actions {
+ actions[index] = act.ToServerPB()
+ }
+ return &ServerCommandPB{
+ Command: &ServerCommandPB_Act{
+ Act: &ServerActPB{
+ Actions: actions,
+ },
+ },
+ }
+}
+
+func (SocketClosed) ToServerPB() *ServerCommandPB {
+ return nil
+}
diff --git a/server/websocket/shared.go b/server/websocket/shared.go
index 6354d33..984935c 100644
--- a/server/websocket/shared.go
+++ b/server/websocket/shared.go
@@ -11,7 +11,7 @@ const (
GoodbyeType = "GOODBYE"
)
-var StandardClientCloseTypes = []int { websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure }
+var StandardClientCloseTypes = []int{websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure}
// SocketClosed is synthesized when a client closes the WebSocket connection, or sent to the write process to write a
// WebSocket close message.
@@ -37,10 +37,6 @@ func (c SocketClosed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return nil
}
-func (c SocketClosed) ClientType() ClientCommandType {
- return ClientGoodbyeType
-}
-
func (c SocketClosed) ServerType() ServerMessageType {
- return ServerGoodbyeType
+ return GoodbyeType
}
diff --git a/server/websocket/writer.go b/server/websocket/writer.go
index 5b294ee..874893a 100644
--- a/server/websocket/writer.go
+++ b/server/websocket/writer.go
@@ -1,10 +1,9 @@
package websocket
import (
- "encoding/json"
- "fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
+ "google.golang.org/protobuf/proto"
"io"
"time"
)
@@ -94,9 +93,15 @@ func (w *writer) act() {
// send actually transmits a ServerCommand to the client according to the protocol.
func (w *writer) send(msg ServerCommand) {
+ w.logger.Debug("Marshaling command as protobuf", zap.Object("msg", msg))
+ marshaled, err := proto.Marshal(msg.ToServerPB())
+ if err != nil {
+ w.logger.Error("Error while marshaling to protobuf", zap.Error(err))
+ return
+ }
writeDeadline := time.Now().Add(WriteTimeLimit)
w.logger.Debug("Setting deadline to write command", zap.Time("writeDeadline", writeDeadline))
- err := w.conn.SetWriteDeadline(writeDeadline)
+ err = w.conn.SetWriteDeadline(writeDeadline)
if err != nil {
w.logger.Error("Error while setting write deadline", zap.Time("writeDeadline", writeDeadline), zap.Object("msg", msg), zap.Error(err))
}
@@ -114,27 +119,12 @@ func (w *writer) send(msg ServerCommand) {
}
w.logger.Debug("Command sent")
}(writer)
- w.logger.Debug("Marshaling command to JSON", zap.Object("msg", msg))
- payload, err := json.Marshal(msg)
+ _, err = writer.Write(marshaled)
if err != nil {
- w.logger.Error("Error while rendering command payload to JSON", zap.Object("msg", msg), zap.Error(err))
+ w.logger.Error("Error while writing marshaled protobuf to connection", zap.Error(err))
return
}
- if len(payload) == 2 {
- // This is an empty JSON message. We can leave it out.
- w.logger.Debug("Empty payload, sending only command type", zap.String("type", string(msg.ServerType())))
- _, err = fmt.Fprintf(writer, "%s!", msg.ServerType())
- if err != nil {
- w.logger.Error("Error while writing no-payload command", zap.Error(err), zap.Object("msg", msg))
- }
- } else {
- // Because we need to send this, we put in a space instead of an exclamation mark.
- w.logger.Debug("Sending command with payload", zap.String("type", string(msg.ServerType())), zap.ByteString("payload", payload))
- _, err = fmt.Fprintf(writer, "%s %s", msg.ServerType(), payload)
- if err != nil {
- w.logger.Error("Error while writing command with payload", zap.Error(err), zap.Object("msg", msg))
- }
- }
+ // Deferred close happens now
}
// sendClose sends a close message on the websocket connection, but does not actually close the connection.
@@ -144,7 +134,7 @@ func (w *writer) sendClose(msg SocketClosed) {
close(w.channel)
w.channel = nil
w.logger.Debug("Writing close message")
- err := w.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(int(msg.Code), msg.Text), time.Now().Add(ControlTimeLimit))
+ err := w.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(msg.Code, msg.Text), time.Now().Add(ControlTimeLimit))
if err != nil {
w.logger.Warn("Error while sending close", zap.Error(err))
}