Finish implementing protobuf conversion routines for the server

main
Mari 3 years ago
parent fe5fc0657f
commit a83c9688bb
  1. 1
      .idea/hexmap.iml
  2. 24
      build/build.go
  3. 3
      client/.gitignore
  4. 103
      client/magefile.go
  5. 12
      client/src/App.css
  6. 6
      client/src/App.tsx
  7. 8
      client/src/actions/NetworkAction.ts
  8. 4
      client/src/reducers/HexMapReducer.ts
  9. 18
      client/src/state/Coordinates.ts
  10. 176
      client/src/ui/debug/ConsoleConnection.tsx
  11. 4
      client/src/util/ArrayUtils.ts
  12. 6
      mage.sh
  13. 26
      magefile.go
  14. 19
      proto/action.proto
  15. 23
      proto/client.proto
  16. 2
      proto/coords.proto
  17. 56
      proto/magefile.go
  18. 28
      proto/map.proto
  19. 37
      proto/server.proto
  20. 11
      proto/state.proto
  21. 2
      proto/user.proto
  22. 2
      server/.gitignore
  23. 83
      server/action/action.go
  24. 75
      server/action/action.pbconv.go
  25. 45
      server/action/map.go
  26. 33
      server/action/user.go
  27. 25
      server/magefile.go
  28. 10
      server/room/actor.go
  29. 6
      server/room/client.go
  30. 2
      server/room/clientmessage.go
  31. 8
      server/room/message.go
  32. 46
      server/state/color.go
  33. 16
      server/state/color.pbconv.go
  34. 4
      server/state/coords.go
  35. 25
      server/state/coords.pbconv.go
  36. 104
      server/state/hexcolor.go
  37. 51
      server/state/hexorientation.go
  38. 49
      server/state/lineparity.go
  39. 80
      server/state/map.go
  40. 142
      server/state/map.pbconv.go
  41. 4
      server/state/state.go
  42. 20
      server/state/state.pbconv.go
  43. 16
      server/state/user.go
  44. 13
      server/state/user.pbconv.go
  45. 46
      server/websocket/client.go
  46. 96
      server/websocket/client.pbconv.go
  47. 99
      server/websocket/reader.go
  48. 53
      server/websocket/server.go
  49. 136
      server/websocket/server.pbconv.go
  50. 8
      server/websocket/shared.go
  51. 34
      server/websocket/writer.go

@ -13,6 +13,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/mage" />
<excludeFolder url="file://$MODULE_DIR$/buildtools" />
<excludeFolder url="file://$MODULE_DIR$/client/src/proto" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

@ -8,20 +8,10 @@ import (
"path/filepath"
)
func GetBuildToolsDir() (string, error) {
basedir, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Join(basedir, "buildtools"), nil
}
const ToolsDir = "buildtools"
func HasExecutableInBuildtools(name string) (bool, error) {
tooldir, err := GetBuildToolsDir()
if err != nil {
return false, err
}
info, err := os.Stat(filepath.Join(tooldir, name))
func HasExecutableInTools(name string) (bool, error) {
info, err := os.Stat(filepath.Join(ToolsDir, name))
if err != nil {
if os.IsNotExist(err) {
return false, nil
@ -29,7 +19,7 @@ func HasExecutableInBuildtools(name string) (bool, error) {
return false, err
}
}
if info.Mode() & 0100 != 0 {
if info.Mode()&0100 != 0 {
return true, nil
} else {
return false, nil
@ -37,15 +27,15 @@ func HasExecutableInBuildtools(name string) (bool, error) {
}
func InstallGoExecutable(ctx context.Context, packageNameWithVersion string) error {
tooldir, err := GetBuildToolsDir()
tooldir, err := filepath.Abs(ToolsDir)
if err != nil {
return err
}
cmd := exec.CommandContext(ctx, "go", "install", packageNameWithVersion)
cmd.Env = append(os.Environ(), "GOBIN=" + tooldir)
cmd.Env = append(os.Environ(), "GOBIN="+tooldir)
cmd.Stderr = os.Stderr
if mg.Verbose() {
cmd.Stdout = os.Stdout
}
return cmd.Run()
}
}

3
client/.gitignore vendored

@ -11,6 +11,9 @@
# production
/build
# Generated protocol buffers files
/src/proto
# misc
.DS_Store
.env.local

@ -1,18 +1,109 @@
package client
import (
"context"
"git.reya.zone/reya/hexmap/build"
"git.reya.zone/reya/hexmap/proto"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/target"
"os"
"os/exec"
"path/filepath"
)
const ProtocPluginPath = "client/node_modules/.bin/protoc-gen-ts_proto"
func NPMInstall(ctx context.Context) error {
var packageLockOutOfDate, nodeModulesOutOfDate bool
var err error
if packageLockOutOfDate, err = target.Path("client/package-lock.json", "client/package-lock.json"); err != nil {
return err
}
if nodeModulesOutOfDate, err = target.Dir("client/node_modules", "client/package.json"); err != nil {
return err
}
if !(packageLockOutOfDate || nodeModulesOutOfDate) {
return nil
}
cmd := exec.CommandContext(ctx, "npm", "install")
cmd.Dir, err = filepath.Abs("client")
if err != nil {
return err
}
cmd.Stderr = os.Stderr
if mg.Verbose() {
cmd.Stdout = os.Stdout
}
return nil
}
func ProtocFlags() ([]string, error) {
buildPath, err := filepath.Abs(filepath.Join(build.ToolsDir, "protoc-gen-ts_proto"))
if err != nil {
return nil, err
}
return []string{
"--plugin=" + buildPath,
"--ts_proto_out=client/src/proto/",
"--ts_proto_opt=env=browser",
"--ts_proto_opt=esModuleInterop=true",
}, nil
}
type Protobuf mg.Namespace
// TODO: NPMInstall
// TODO: Protobuf:InstallTSPlugin
// TODO: Protobuf:InstallPlugins
// TODO: Protobuf:Build
// TODO: Protobuf:Clean
const TSPluginNPMLocation = "client/node_modules/.bin/protoc-gen-ts_proto"
func (Protobuf) InstallTSPlugin(ctx context.Context) error {
mg.CtxDeps(ctx, NPMInstall)
buildPath, err := filepath.Abs(filepath.Join(build.ToolsDir, "protoc-gen-ts_proto"))
if err != nil {
return err
}
sourcePath, err := filepath.Abs(TSPluginNPMLocation)
if err != nil {
return err
}
// Errors here just mean we move on to the next step - this could not exist or not be a link, we don't care.
// The important part is the later parts.
if linkPath, err := os.Readlink(buildPath); err == nil && linkPath == sourcePath {
return nil
}
// Remove whatever's in the way, if necessary.
err = os.Remove(buildPath)
if err != nil && !os.IsNotExist(err) {
return err
}
err = os.Symlink(sourcePath, buildPath)
if err != nil && !os.IsExist(err) {
return err
}
return nil
}
func (Protobuf) MakeProtoDir() error {
if err := os.Mkdir("client/src/proto", 0755); err != nil && !os.IsExist(err) {
return err
}
return nil
}
func (Protobuf) InstallPlugins(ctx context.Context) error {
mg.CtxDeps(ctx, Protobuf.InstallTSPlugin, Protobuf.MakeProtoDir)
return 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() error {
err := os.RemoveAll("client/src/proto")
if err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
// TODO: Build
// TODO: Clean
// TODO: Serve

@ -3,7 +3,7 @@ body {
background-color: #282c34;
}
.App, .scrollbox {
.App, .scrollBox {
position: absolute;
top: 0;
bottom: 0;
@ -11,11 +11,11 @@ body {
right: 0;
}
.scrollbox {
.scrollBox {
overflow-x: scroll;
overflow-y: scroll;
}
.centerbox {
.centerBox {
display: flex;
align-items: center;
justify-content: center;
@ -72,7 +72,7 @@ body {
.hexColorPicker {
width: 30vw;
}
.centerbox {
.centerBox {
padding-bottom: calc(2vh + (30vw * 126 / 855));
}
}
@ -80,7 +80,7 @@ body {
.hexColorPicker {
width: 300px;
}
.centerbox {
.centerBox {
padding-bottom: calc(2vh + (300px * 126 / 855));
}
}
@ -88,7 +88,7 @@ body {
.hexColorPicker {
width: 98vw;
}
.centerbox {
.centerBox {
padding-bottom: calc(2vh + (98vw * 126 / 855));
}
}

@ -7,7 +7,6 @@ import {ServerConnectionState} from "./state/NetworkState";
import HexMapRenderer from "./ui/HexMapRenderer";
import HexColorPicker from "./ui/HexColorPicker";
import {sizeFromLinesAndCells} from "./state/Coordinates";
import {ConsoleConnector} from "./ui/debug/ConsoleConnection";
function App() {
const [state, dispatch] = useReducer<AppStateReducer, null>(
@ -48,15 +47,14 @@ function App() {
return (
<div className="App">
<DispatchContext.Provider value={dispatch}>
<div className={"scrollbox"}>
<div className={"centerbox"}>
<div className={"scrollBox"}>
<div className={"centerBox"}>
<svg className={"map"} width={width} height={height} viewBox={`0 0 ${width} ${height}`} onContextMenu={(e) => e.preventDefault()}>
{mapElement}
</svg>
</div>
</div>
{colorPickerElement}
<ConsoleConnector specialMessage={state.network.specialMessage} pendingMessages={state.network.pendingActions} nextID={state.network.nextID} />
</DispatchContext.Provider>
</div>
);

@ -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 |

@ -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;
}

@ -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. */

@ -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<ClientAction|null>(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 <div className="consoleConnector">Console connection active</div>
}

@ -1,7 +1,3 @@
export function arrayShallowEqual<T>(left: readonly T[], right: readonly T[]): boolean {
return left.length === right.length && arrayShallowStartsWith(left, right)
}
export function arrayShallowStartsWith<T>(target: readonly T[], prefix: readonly T[]): boolean {
return target.length >= prefix.length && prefix.every((value, index) => target[index] === value)
}

@ -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" "$@"
exec "$MAGEPATH" -d "$MAINPATH" -w "$MAINPATH" "$@"

@ -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
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}

@ -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()
}

@ -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;
}

@ -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;
}
}

@ -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;
}

@ -2,6 +2,6 @@ syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
message UserState {
message UserStatePB {
fixed32 color = 1;
}

2
server/.gitignore vendored

@ -1,2 +1,2 @@
*.pb.go
*.zap.go
proto/

@ -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
}

@ -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(),
},
},
}
}

@ -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
}

@ -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
}

@ -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 {

@ -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 {

@ -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.

@ -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
}

@ -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)
}

@ -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)
}
}
}

@ -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)
}

@ -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 {

@ -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),
}
}

@ -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)
}
}
}

@ -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)
}
}

@ -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)
}
}

@ -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(),
}
}

@ -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(),
}
}

@ -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 {

@ -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(),
}
}

@ -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,
}
}

@ -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(),
}
}

@ -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
}

@ -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
}

@ -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
}
}

@ -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
}

@ -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
}

@ -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
}

@ -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))
}

Loading…
Cancel
Save