parent
fe5fc0657f
commit
a83c9688bb
@ -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
|
||||
|
@ -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" "$@" |
||||
|
@ -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() |
||||
} |
@ -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; |
||||
} |
@ -1,2 +1,2 @@ |
||||
*.pb.go |
||||
*.zap.go |
||||
proto/ |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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