Compare commits
3 Commits
41f882190f
...
a83c9688bb
Author | SHA1 | Date |
---|---|---|
Mari | a83c9688bb | 3 years ago |
Mari | fe5fc0657f | 3 years ago |
Mari | f5f3427656 | 3 years ago |
@ -0,0 +1 @@ |
||||
/buildtools |
@ -0,0 +1,8 @@ |
||||
# Default ignored files |
||||
/shelf/ |
||||
/workspace.xml |
||||
# Datasource local storage ignored files |
||||
/dataSources/ |
||||
/dataSources.local.xml |
||||
# Editor-based HTTP Client requests |
||||
/httpRequests/ |
@ -0,0 +1,21 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<module type="WEB_MODULE" version="4"> |
||||
<component name="Go" enabled="true"> |
||||
<buildTags> |
||||
<option name="customFlags"> |
||||
<array> |
||||
<option value="mage" /> |
||||
</array> |
||||
</option> |
||||
</buildTags> |
||||
</component> |
||||
<component name="NewModuleRootManager"> |
||||
<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" /> |
||||
</component> |
||||
</module> |
@ -0,0 +1,8 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="ProjectModuleManager"> |
||||
<modules> |
||||
<module fileurl="file://$PROJECT_DIR$/.idea/hexmap.iml" filepath="$PROJECT_DIR$/.idea/hexmap.iml" /> |
||||
</modules> |
||||
</component> |
||||
</project> |
@ -0,0 +1,17 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="ProtobufLanguageSettings"> |
||||
<option name="autoConfigEnabled" value="false" /> |
||||
<option name="importPathEntries"> |
||||
<list> |
||||
<ImportPathEntry> |
||||
<option name="location" value="jar://$APPLICATION_PLUGINS_DIR$/protobuf-editor.jar!/include" /> |
||||
</ImportPathEntry> |
||||
<ImportPathEntry> |
||||
<option name="location" value="file://$PROJECT_DIR$/proto" /> |
||||
</ImportPathEntry> |
||||
</list> |
||||
</option> |
||||
<option name="descriptorPath" value="google/protobuf/descriptor.proto" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,6 @@ |
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="VcsDirectoryMappings"> |
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> |
||||
</component> |
||||
</project> |
@ -0,0 +1,41 @@ |
||||
package build |
||||
|
||||
import ( |
||||
"context" |
||||
"github.com/magefile/mage/mg" |
||||
"os" |
||||
"os/exec" |
||||
"path/filepath" |
||||
) |
||||
|
||||
const ToolsDir = "buildtools" |
||||
|
||||
func HasExecutableInTools(name string) (bool, error) { |
||||
info, err := os.Stat(filepath.Join(ToolsDir, name)) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return false, nil |
||||
} else { |
||||
return false, err |
||||
} |
||||
} |
||||
if info.Mode()&0100 != 0 { |
||||
return true, nil |
||||
} else { |
||||
return false, nil |
||||
} |
||||
} |
||||
|
||||
func InstallGoExecutable(ctx context.Context, packageNameWithVersion string) error { |
||||
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.Stderr = os.Stderr |
||||
if mg.Verbose() { |
||||
cmd.Stdout = os.Stdout |
||||
} |
||||
return cmd.Run() |
||||
} |
@ -0,0 +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" |
||||
) |
||||
|
||||
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 |
||||
|
||||
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
|
@ -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) |
||||
} |
@ -0,0 +1,12 @@ |
||||
module git.reya.zone/reya/hexmap |
||||
|
||||
go 1.16 |
||||
|
||||
require ( |
||||
github.com/gorilla/websocket v1.4.2 |
||||
github.com/magefile/mage v1.11.0 |
||||
github.com/rs/xid v1.3.0 |
||||
go.uber.org/zap v1.18.1 |
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a // indirect |
||||
google.golang.org/protobuf v1.27.1 |
||||
) |
@ -0,0 +1,12 @@ |
||||
#!/bin/bash |
||||
|
||||
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" "$@" |
@ -0,0 +1,32 @@ |
||||
// +build mage
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"git.reya.zone/reya/hexmap/proto" |
||||
|
||||
// mage:import server
|
||||
"git.reya.zone/reya/hexmap/server" |
||||
"github.com/magefile/mage/mg" |
||||
|
||||
// mage:import 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 |
||||
} |
@ -0,0 +1,27 @@ |
||||
syntax = "proto3"; |
||||
|
||||
import "coords.proto"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/action"; |
||||
|
||||
message CellSetColorPB { |
||||
fixed32 color = 1; |
||||
StorageCoordinatesPB at = 2; |
||||
} |
||||
|
||||
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; |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
syntax = "proto3"; |
||||
|
||||
import "action.proto"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/websocket"; |
||||
|
||||
message ClientHelloPB { |
||||
uint32 version = 1; |
||||
} |
||||
|
||||
message ClientRefreshPB { |
||||
} |
||||
|
||||
message ClientActPB { |
||||
message IDed { |
||||
uint32 id = 1; |
||||
ClientActionPB action = 2; |
||||
} |
||||
repeated IDed actions = 1; |
||||
} |
||||
|
||||
message ClientCommandPB { |
||||
oneof command { |
||||
ClientHelloPB hello = 1; |
||||
ClientRefreshPB refresh = 2; |
||||
ClientActPB act = 3; |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
syntax = "proto3"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/state"; |
||||
|
||||
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() |
||||
} |
@ -0,0 +1,37 @@ |
||||
syntax = "proto3"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/state"; |
||||
|
||||
message HexCellPB { |
||||
fixed32 color = 1; |
||||
} |
||||
|
||||
message HexLinePB { |
||||
repeated HexCellPB cells = 1; |
||||
} |
||||
|
||||
message HexLayerPB { |
||||
repeated HexLinePB lines = 1; |
||||
} |
||||
|
||||
message HexMapPB { |
||||
message Layout { |
||||
enum Orientation { |
||||
UNKNOWN_ORIENTATION = 0; |
||||
POINTY_TOP = 1; |
||||
FLAT_TOP = 2; |
||||
} |
||||
enum LineParity { |
||||
UNKNOWN_LINE = 0; |
||||
ODD = 1; |
||||
EVEN = 2; |
||||
} |
||||
Orientation orientation = 1; |
||||
LineParity indented_lines = 2; |
||||
} |
||||
bytes xid = 1; |
||||
uint32 lines = 2; |
||||
uint32 cells_per_line = 3; |
||||
Layout layout = 4; |
||||
HexLayerPB layer = 5; |
||||
} |
@ -0,0 +1,38 @@ |
||||
syntax = "proto3"; |
||||
|
||||
import "action.proto"; |
||||
import "state.proto"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/websocket"; |
||||
|
||||
message ServerHelloPB { |
||||
uint32 version = 1; |
||||
SyncableStatePB state = 2; |
||||
} |
||||
|
||||
message ServerRefreshPB { |
||||
SyncableStatePB state = 1; |
||||
} |
||||
|
||||
message ServerOKPB { |
||||
repeated uint32 ids = 1; |
||||
} |
||||
|
||||
message ServerFailedPB { |
||||
repeated uint32 ids = 1; |
||||
string error = 2; |
||||
} |
||||
|
||||
message ServerActPB { |
||||
repeated ServerActionPB 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; |
||||
} |
@ -0,0 +1,7 @@ |
||||
syntax = "proto3"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/state"; |
||||
|
||||
message UserStatePB { |
||||
fixed32 color = 1; |
||||
} |
@ -0,0 +1,2 @@ |
||||
*.pb.go |
||||
proto/ |
@ -0,0 +1,112 @@ |
||||
package action |
||||
|
||||
import ( |
||||
"errors" |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
var ( |
||||
// ErrNoOp is returned when an action has no effect.
|
||||
ErrNoOp = errors.New("action's effects were already applied, or it's an empty action") |
||||
// ErrNoTransparentColors is returned when a user tries to set their active color or a cell color to transparent.
|
||||
// Transparent here is defined as having an alpha component of less than 15 (0xF).
|
||||
ErrNoTransparentColors = errors.New("transparent colors not allowed") |
||||
) |
||||
|
||||
// Action is the interface for actions that can be shared between clients, or between the server and a client.
|
||||
type Action interface { |
||||
zapcore.ObjectMarshaler |
||||
// 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 |
||||
} |
||||
|
||||
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 ServerSlice []Server |
||||
|
||||
func (s ServerSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error { |
||||
var finalErr error = nil |
||||
for _, a := range s { |
||||
err := encoder.AppendObject(a) |
||||
if err != nil && finalErr == nil { |
||||
finalErr = err |
||||
} |
||||
} |
||||
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 SyncableType = "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() SyncableType { |
||||
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,43 +0,0 @@ |
||||
package action |
||||
|
||||
import ( |
||||
"errors" |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
var ( |
||||
// ErrNoOp is returned when an action has no effect.
|
||||
ErrNoOp = errors.New("action's effects were already applied, or it's an empty action") |
||||
// ErrNoTransparentColors is returned when a user tries to set their active color or a cell color to transparent.
|
||||
// Transparent here is defined as having an alpha component of less than 15 (0xF).
|
||||
ErrNoTransparentColors = errors.New("transparent colors not allowed") |
||||
) |
||||
|
||||
type SyncableType string |
||||
|
||||
// Syncable is the interface for actions that can be shared between clients.
|
||||
type Syncable interface { |
||||
zapcore.ObjectMarshaler |
||||
// Type gives the Javascript type that is sent over the wire.
|
||||
Type() SyncableType |
||||
// Apply causes the action's effects to be applied to s, mutating it in place.
|
||||
// All syncable.Actions must conform to the standard that if an action can't be correctly applied, or if it would
|
||||
// have no effect, it returns an error without changing s.
|
||||
// If an action can be correctly applied but would have no effect, it should return ErrNoOp.
|
||||
// If an action is correctly applied and has an effect, it should return nil.
|
||||
Apply(s *state.Synced) error |
||||
} |
||||
|
||||
type SyncableSlice []Syncable |
||||
|
||||
func (s SyncableSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error { |
||||
var finalErr error = nil |
||||
for _, a := range s { |
||||
err := encoder.AppendObject(a) |
||||
if err != nil && finalErr == nil { |
||||
finalErr = err |
||||
} |
||||
} |
||||
return finalErr |
||||
} |
@ -1,37 +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) Type() SyncableType { |
||||
return UserActiveColorType |
||||
} |
||||
|
||||
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 |
||||
} |
@ -1,9 +0,0 @@ |
||||
module git.reya.zone/reya/hexmap/server |
||||
|
||||
go 1.16 |
||||
|
||||
require ( |
||||
github.com/gorilla/websocket v1.4.2 |
||||
github.com/rs/xid v1.3.0 |
||||
go.uber.org/zap v1.18.1 |
||||
) |
@ -0,0 +1,68 @@ |
||||
package server |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"git.reya.zone/reya/hexmap/build" |
||||
"git.reya.zone/reya/hexmap/proto" |
||||
"github.com/magefile/mage/mg" |
||||
"io/fs" |
||||
"os" |
||||
"path/filepath" |
||||
"strings" |
||||
) |
||||
|
||||
// TODO: GoGet
|
||||
// TODO: Build
|
||||
// TODO: Clean
|
||||
|
||||
type Protobuf mg.Namespace |
||||
|
||||
func (Protobuf) InstallGoPlugin(ctx context.Context) error { |
||||
alreadyDone, err := build.HasExecutableInTools("protoc-gen-go") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if alreadyDone { |
||||
return nil |
||||
} |
||||
return build.InstallGoExecutable(ctx, "google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1") |
||||
} |
||||
|
||||
func (Protobuf) InstallPlugins(ctx context.Context) { |
||||
mg.CtxDeps(ctx, Protobuf.InstallGoPlugin) |
||||
} |
||||
|
||||
func ProtocFlags() ([]string, error) { |
||||
buildPath, err := filepath.Abs(filepath.Join(build.ToolsDir, "protoc-gen-go")) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
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 { |
||||
return filepath.WalkDir("server", func(path string, d fs.DirEntry, dirErr error) error { |
||||
if ctx.Err() != nil { |
||||
return ctx.Err() |
||||
} |
||||
if dirErr != nil { |
||||
return dirErr |
||||
} |
||||
if strings.HasSuffix(path, ".pb.go") && !d.IsDir() { |
||||
if mg.Verbose() { |
||||
fmt.Printf("Removing generated protobuf code at %s\n", path) |
||||
} |
||||
err := os.Remove(path) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
}) |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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(), |
||||
} |
||||
} |
@ -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(), |
||||
} |
||||
} |
@ -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(), |
||||
} |
||||
} |
@ -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,14 +1,118 @@ |
||||
package websocket |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/gorilla/websocket" |
||||
"go.uber.org/zap" |
||||
"google.golang.org/protobuf/proto" |
||||
"time" |
||||
) |
||||
|
||||
// Todo: Listen for pongs and extend the read deadline every time you get one
|
||||
|
||||
type reader struct { |
||||
conn *websocket.Conn |
||||
channel chan ClientMessage |
||||
readNotifications chan<- time.Duration |
||||
// conn is the connection to the client read from by the reader.
|
||||
conn *websocket.Conn |
||||
// channel is the channel that the reader sends messages it has received on.
|
||||
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.
|
||||
logger *zap.Logger |
||||
} |
||||
|
||||
// InvalidMessageType is placed in ClientMalformed when the type of the WebSocket message was incorrect.
|
||||
type InvalidMessageType struct { |
||||
MessageType int |
||||
} |
||||
|
||||
func (i InvalidMessageType) Error() string { |
||||
return fmt.Sprintf("invalid message type %d", i.MessageType) |
||||
} |
||||
|
||||
// act contains the main read loop of the reader.
|
||||
func (r *reader) act() { |
||||
defer r.shutdown() |
||||
// Our first deadline starts immediately, so let the writer know.
|
||||
r.updateDeadlines() |
||||
r.conn.SetPongHandler(func(appData string) error { |
||||
r.logger.Debug("Received pong, extending read deadline") |
||||
r.updateDeadlines() |
||||
if appData != PingData { |
||||
r.logger.Warn("Got unexpected data in the pong", zap.String("appData", appData)) |
||||
} |
||||
return nil |
||||
}) |
||||
for { |
||||
messageType, messageData, err := r.conn.ReadMessage() |
||||
if err != nil { |
||||
var closure SocketClosed |
||||
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} |
||||
} 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} |
||||
} else { |
||||
r.logger.Error("Error while reading message, shutting down", zap.Error(err)) |
||||
closure = SocketClosed{Error: err} |
||||
} |
||||
r.logger.Debug("Sending close message to reader", zap.Object("closeMessage", closure)) |
||||
r.channel <- closure |
||||
// We must exit now - errors from this method are permanent, after all.
|
||||
// We'll do the shutdown we deferred.
|
||||
return |
||||
} |
||||
r.updateDeadlines() |
||||
r.channel <- r.parseCommand(messageType, messageData) |
||||
} |
||||
} |
||||
|
||||
// parseCommand attempts to parse the incoming message
|
||||
func (r *reader) parseCommand(socketType int, data []byte) ClientCommand { |
||||
if socketType != websocket.BinaryMessage { |
||||
err := &InvalidMessageType{ |
||||
MessageType: socketType, |
||||
} |
||||
r.logger.Error("Received command with unknown WebSocket message type", zap.Error(err)) |
||||
return ClientMalformed{ |
||||
Error: err, |
||||
} |
||||
} |
||||
r.logger.Debug("Received command, parsing") |
||||
var cmdPb ClientCommandPB |
||||
err := proto.Unmarshal(data, &cmdPb) |
||||
if err != nil { |
||||
return ClientMalformed{ |
||||
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.
|
||||
func (r *reader) updateDeadlines() { |
||||
receivedAt := time.Now() |
||||
r.logger.Debug("Alerting writer to extend ping timer", zap.Time("receivedAt", receivedAt)) |
||||
r.readNotifications <- receivedAt |
||||
newDeadline := receivedAt.Add(ReadTimeLimit) |
||||
r.logger.Debug("Extending read deadline", zap.Time("newDeadline", newDeadline)) |
||||
err := r.conn.SetReadDeadline(newDeadline) |
||||
if err != nil { |
||||
r.logger.Error("Error while extending read deadline", zap.Error(err)) |
||||
} |
||||
r.logger.Debug("Read deadline extended") |
||||
} |
||||
|
||||
// shutdown closes all resources associated with the reader (the channel and readNotifications) but leaves its conn running.
|
||||
func (r *reader) shutdown() { |
||||
close(r.channel) |
||||
r.channel = nil |
||||
close(r.readNotifications) |
||||
r.readNotifications = nil |
||||
r.conn = nil |
||||
} |
||||
|
@ -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 |
||||
} |
Loading…
Reference in new issue