For now, the URL is hardcoded. We'll fix this later.main
parent
54b9112546
commit
34e64c7130
@ -0,0 +1,27 @@ |
|||||||
|
export function normalizeColor(hex: string): string { |
||||||
|
hex = hex.toUpperCase() |
||||||
|
if (hex.length === 4) { |
||||||
|
return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}FF` |
||||||
|
} |
||||||
|
if (hex.length === 5) { |
||||||
|
return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}${hex[4]}${hex[4]}` |
||||||
|
} |
||||||
|
if (hex.length === 7) { |
||||||
|
return `${hex}FF` |
||||||
|
} |
||||||
|
return hex |
||||||
|
} |
||||||
|
|
||||||
|
export function packHexColor(color: string): number { |
||||||
|
return parseInt(normalizeColor(color).substring(1), 16) |
||||||
|
} |
||||||
|
|
||||||
|
export function unpackHexColor(color: number): string { |
||||||
|
if (color > 2**32 || Math.floor(color) !== color) { |
||||||
|
throw Error("Packed color was too large or not an integer") |
||||||
|
} |
||||||
|
// this is 1 << 32, so it will produce a single hex digit above the others, even if the
|
||||||
|
// R/G/B is 0 - which we can then trim off and replace with our #
|
||||||
|
// a neat insight from https://stackoverflow.com/a/13397771
|
||||||
|
return "#" + ((color + 0x100000000).toString(16).substring(1)) |
||||||
|
} |
@ -1,6 +1,38 @@ |
|||||||
import {ClientCommand} from "../actions/ClientAction"; |
import {CLIENT_ACT, CLIENT_HELLO, CLIENT_REFRESH, ClientCommand, SentAction} from "../actions/ClientAction"; |
||||||
import {ClientCommandPB} from "../proto/client"; |
import {ClientActPB_IDed, ClientCommandPB} from "../proto/client"; |
||||||
|
import {sendableActionToPB} from "./SyncableActionToPb"; |
||||||
|
|
||||||
export function clientToPb(message: ClientCommand): ClientCommandPB { |
export function sentActionToPb(message: SentAction): ClientActPB_IDed { |
||||||
|
return { |
||||||
|
id: message.id, |
||||||
|
action: sendableActionToPB(message.action), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function clientToPb(message: ClientCommand): ClientCommandPB { |
||||||
|
switch (message.type) { |
||||||
|
case CLIENT_HELLO: |
||||||
|
return { |
||||||
|
hello: { |
||||||
|
version: message.version, |
||||||
|
}, |
||||||
|
refresh: undefined, |
||||||
|
act: undefined, |
||||||
|
} |
||||||
|
case CLIENT_REFRESH: |
||||||
|
return { |
||||||
|
refresh: { |
||||||
|
}, |
||||||
|
hello: undefined, |
||||||
|
act: undefined, |
||||||
|
} |
||||||
|
case CLIENT_ACT: |
||||||
|
return { |
||||||
|
act: { |
||||||
|
actions: message.actions.map(sentActionToPb) |
||||||
|
}, |
||||||
|
hello: undefined, |
||||||
|
refresh: undefined, |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
@ -0,0 +1,91 @@ |
|||||||
|
import {SyncableStatePB} from "../proto/state"; |
||||||
|
import {SyncedState} from "../state/SyncedState"; |
||||||
|
import {HexagonOrientation, HexCell, HexLayout, HexMap, LineParity} from "../state/HexMap"; |
||||||
|
import {UserStatePB} from "../proto/user"; |
||||||
|
import { |
||||||
|
HexCellPB, |
||||||
|
HexMapPB, |
||||||
|
HexMapPB_Layout, |
||||||
|
HexMapPB_Layout_LineParity, |
||||||
|
HexMapPB_Layout_Orientation |
||||||
|
} from "../proto/map"; |
||||||
|
import {UserState} from "../state/UserState"; |
||||||
|
import {unpackHexColor} from "../util/ColorUtils"; |
||||||
|
import {encode} from "base64-arraybuffer"; |
||||||
|
import {StorageCoordinatesPB} from "../proto/coords"; |
||||||
|
import {StorageCoordinates} from "../state/Coordinates"; |
||||||
|
|
||||||
|
export function storageCoordsFromPb(coords: StorageCoordinatesPB): StorageCoordinates { |
||||||
|
return { |
||||||
|
line: coords.line, |
||||||
|
cell: coords.cell, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function orientationFromPb(orientation: HexMapPB_Layout_Orientation): HexagonOrientation { |
||||||
|
switch (orientation) { |
||||||
|
case HexMapPB_Layout_Orientation.FLAT_TOP: |
||||||
|
return HexagonOrientation.FLAT_TOP |
||||||
|
case HexMapPB_Layout_Orientation.POINTY_TOP: |
||||||
|
return HexagonOrientation.POINTY_TOP |
||||||
|
case HexMapPB_Layout_Orientation.UNKNOWN_ORIENTATION: |
||||||
|
case HexMapPB_Layout_Orientation.UNRECOGNIZED: |
||||||
|
throw Error("unknown orientation") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function lineParityFromPb(parity: HexMapPB_Layout_LineParity): LineParity { |
||||||
|
switch (parity) { |
||||||
|
case HexMapPB_Layout_LineParity.EVEN: |
||||||
|
return LineParity.EVEN |
||||||
|
case HexMapPB_Layout_LineParity.ODD: |
||||||
|
return LineParity.ODD |
||||||
|
case HexMapPB_Layout_LineParity.UNKNOWN_LINE: |
||||||
|
case HexMapPB_Layout_LineParity.UNRECOGNIZED: |
||||||
|
throw Error("unknown line parity") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function layoutFromPb(layout: HexMapPB_Layout): HexLayout { |
||||||
|
return { |
||||||
|
orientation: orientationFromPb(layout.orientation), |
||||||
|
indentedLines: lineParityFromPb(layout.indentedLines), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function cellFromPb(cell: HexCellPB): HexCell { |
||||||
|
return { |
||||||
|
color: unpackHexColor(cell.color) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mapFromPb(map: HexMapPB): HexMap { |
||||||
|
if (!map.layout || !map.layer) { |
||||||
|
throw Error("HexMapPB did not have layout and layer"); |
||||||
|
} |
||||||
|
return { |
||||||
|
xid: encode(map.xid), |
||||||
|
lines: 0, |
||||||
|
cellsPerLine: 0, |
||||||
|
layout: layoutFromPb(map.layout), |
||||||
|
layer: map.layer.lines.map((line): HexCell[] => { |
||||||
|
return line.cells.map(cellFromPb) |
||||||
|
}), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function userFromPb(user: UserStatePB): UserState { |
||||||
|
return { |
||||||
|
activeColor: unpackHexColor(user.color) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function stateFromPb(state: SyncableStatePB): SyncedState { |
||||||
|
if (!state.map || !state.user) { |
||||||
|
throw Error("SyncableStatePB did not have map and user") |
||||||
|
} |
||||||
|
return { |
||||||
|
map: mapFromPb(state.map), |
||||||
|
user: userFromPb(state.user), |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
import {StorageCoordinates} from "../state/Coordinates"; |
||||||
|
import {StorageCoordinatesPB} from "../proto/coords"; |
||||||
|
|
||||||
|
export function storageCoordsToPb(storageCoords: StorageCoordinates): StorageCoordinatesPB { |
||||||
|
return { |
||||||
|
line: storageCoords.line, |
||||||
|
cell: storageCoords.cell, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
import {ServerCommandPB} from "../proto/server"; |
||||||
|
import { |
||||||
|
SERVER_ACT, |
||||||
|
SERVER_FAILED, |
||||||
|
SERVER_HELLO, |
||||||
|
SERVER_OK, |
||||||
|
SERVER_REFRESH, |
||||||
|
ServerCommand |
||||||
|
} from "../actions/ServerAction"; |
||||||
|
import {stateFromPb} from "./MapFromPb"; |
||||||
|
import {syncableActionFromPb} from "./SyncableActionFromPb"; |
||||||
|
|
||||||
|
export function serverFromPb(pb: ServerCommandPB): ServerCommand { |
||||||
|
if (pb.hello) { |
||||||
|
if (!pb.hello.state) { |
||||||
|
throw Error("No state for Server Hello") |
||||||
|
} |
||||||
|
return { |
||||||
|
type: SERVER_HELLO, |
||||||
|
version:pb.hello.version, |
||||||
|
state: stateFromPb(pb.hello.state), |
||||||
|
} |
||||||
|
} else if (pb.refresh) { |
||||||
|
if (!pb.refresh.state) { |
||||||
|
throw Error("No state for Server Refresh") |
||||||
|
} |
||||||
|
return { |
||||||
|
type: SERVER_REFRESH, |
||||||
|
state: stateFromPb(pb.refresh.state), |
||||||
|
} |
||||||
|
} else if (pb.ok) { |
||||||
|
return { |
||||||
|
type: SERVER_OK, |
||||||
|
ids: pb.ok.ids, |
||||||
|
} |
||||||
|
} else if (pb.failed) { |
||||||
|
return { |
||||||
|
type: SERVER_FAILED, |
||||||
|
ids: pb.failed.ids, |
||||||
|
error: pb.failed.error, |
||||||
|
} |
||||||
|
} else if (pb.act) { |
||||||
|
return { |
||||||
|
type: SERVER_ACT, |
||||||
|
actions: pb.act.actions.map(syncableActionFromPb) |
||||||
|
} |
||||||
|
} else { |
||||||
|
throw Error("No actual commands set on command") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
import {ClientActionPB, ServerActionPB} from "../proto/action"; |
||||||
|
import {SyncableAction} from "../actions/ServerAction"; |
||||||
|
import {SendableAction} from "../actions/ClientAction"; |
||||||
|
import {unpackHexColor} from "../util/ColorUtils"; |
||||||
|
import {USER_ACTIVE_COLOR} from "../actions/UserAction"; |
||||||
|
import {CELL_COLOR} from "../actions/CellAction"; |
||||||
|
import {storageCoordsFromPb} from "./MapFromPb"; |
||||||
|
|
||||||
|
function sendableActionFromPB(action: ClientActionPB): SendableAction { |
||||||
|
if (action.cellSetColor) { |
||||||
|
if (!action.cellSetColor.at) { |
||||||
|
throw Error("No location set in cellSetColor") |
||||||
|
} |
||||||
|
return { |
||||||
|
type: CELL_COLOR, |
||||||
|
at: storageCoordsFromPb(action.cellSetColor.at), |
||||||
|
color: unpackHexColor(action.cellSetColor.color), |
||||||
|
} |
||||||
|
} else if (action.userSetActiveColor) { |
||||||
|
return { |
||||||
|
type: USER_ACTIVE_COLOR, |
||||||
|
color: unpackHexColor(action.userSetActiveColor.color) |
||||||
|
} |
||||||
|
} else { |
||||||
|
throw Error("No action set in ClientAction") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function syncableActionFromPb(action: ServerActionPB): SyncableAction { |
||||||
|
if (action.client) { |
||||||
|
return sendableActionFromPB(action.client) |
||||||
|
} else { |
||||||
|
throw Error("No action set in ServerAction") |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
import {ClientActionPB} from "../proto/action"; |
||||||
|
import {SendableAction} from "../actions/ClientAction"; |
||||||
|
import {packHexColor} from "../util/ColorUtils"; |
||||||
|
import {storageCoordsToPb} from "./MapToPb"; |
||||||
|
import {USER_ACTIVE_COLOR} from "../actions/UserAction"; |
||||||
|
import {CELL_COLOR} from "../actions/CellAction"; |
||||||
|
|
||||||
|
export function sendableActionToPB(action: SendableAction): ClientActionPB { |
||||||
|
switch (action.type) { |
||||||
|
case CELL_COLOR: |
||||||
|
return { |
||||||
|
cellSetColor: { |
||||||
|
at: storageCoordsToPb(action.at), |
||||||
|
color: packHexColor(action.color), |
||||||
|
}, |
||||||
|
userSetActiveColor: undefined, |
||||||
|
} |
||||||
|
case USER_ACTIVE_COLOR: |
||||||
|
return { |
||||||
|
userSetActiveColor: { |
||||||
|
color: packHexColor(action.color) |
||||||
|
}, |
||||||
|
cellSetColor: undefined, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,71 @@ |
|||||||
|
import {ReactElement, useContext, useEffect, useRef, useState} from "react"; |
||||||
|
import { |
||||||
|
CLIENT_ACT, |
||||||
|
CLIENT_REFRESH, |
||||||
|
ClientActCommand, |
||||||
|
ClientGoodbyeAction, |
||||||
|
ClientHelloCommand, |
||||||
|
ClientRefreshCommand, |
||||||
|
isClientCommand, |
||||||
|
isClientGoodbyeCommand, |
||||||
|
SendableAction, |
||||||
|
SentAction |
||||||
|
} from "../actions/ClientAction"; |
||||||
|
import {DispatchContext} from "../ui/context/DispatchContext"; |
||||||
|
import {WebsocketTranslator} from "./WebsocketTranslator"; |
||||||
|
import {ServerConnectionState} from "../state/NetworkState"; |
||||||
|
import {SERVER_SOCKET_STARTUP, SocketState} from "../actions/ServerAction"; |
||||||
|
|
||||||
|
export function WebsocketReactAdapter({url, protocols = ["v1.hexmap.deliciousreya.net"], state, specialMessage, pendingMessages, nextID}: { |
||||||
|
url: string, |
||||||
|
protocols?: readonly string[], |
||||||
|
state: ServerConnectionState, |
||||||
|
specialMessage: ClientHelloCommand | ClientRefreshCommand | ClientGoodbyeAction | null, |
||||||
|
pendingMessages: readonly SendableAction[], |
||||||
|
nextID: number |
||||||
|
}): ReactElement { |
||||||
|
const dispatch = useContext(DispatchContext) |
||||||
|
if (dispatch === null) { |
||||||
|
throw Error("What the heck?! No dispatch?! I quit!") |
||||||
|
} |
||||||
|
const connector = useRef(new WebsocketTranslator({ |
||||||
|
url, |
||||||
|
protocols, |
||||||
|
onStartup: dispatch, |
||||||
|
onMessage: dispatch, |
||||||
|
onError: dispatch, |
||||||
|
onGoodbye: dispatch, |
||||||
|
})) |
||||||
|
useEffect(() => { |
||||||
|
connector.current.connect(); |
||||||
|
dispatch({ |
||||||
|
type: SERVER_SOCKET_STARTUP, |
||||||
|
state: SocketState.CONNECTING, |
||||||
|
}); |
||||||
|
}, [dispatch]) |
||||||
|
const [lastSpecialMessage, setLastSpecialMessage] = useState<ClientHelloCommand | ClientRefreshCommand | ClientGoodbyeAction | null>(null) |
||||||
|
useEffect(() => { |
||||||
|
if (state === ServerConnectionState.CONNECTED && pendingMessages.length > 0) { |
||||||
|
const sentMessages: SentAction[] = pendingMessages.map((action, index) => { |
||||||
|
return {id: index + nextID, action} |
||||||
|
}); |
||||||
|
const sentMessage: ClientActCommand = { |
||||||
|
type: CLIENT_ACT, |
||||||
|
actions: sentMessages, |
||||||
|
}; |
||||||
|
connector.current.send(sentMessage) |
||||||
|
dispatch(sentMessage) |
||||||
|
} |
||||||
|
}, [nextID, dispatch, pendingMessages, state]) |
||||||
|
useEffect(() => { |
||||||
|
if (state === ServerConnectionState.CONNECTED && specialMessage !== null && specialMessage !== lastSpecialMessage) { |
||||||
|
if (isClientCommand(specialMessage)) { |
||||||
|
connector.current.send(specialMessage); |
||||||
|
} else if (isClientGoodbyeCommand(specialMessage)) { |
||||||
|
connector.current.close(specialMessage.code, specialMessage.reason) |
||||||
|
} |
||||||
|
setLastSpecialMessage(specialMessage); |
||||||
|
} |
||||||
|
}, [specialMessage, lastSpecialMessage, setLastSpecialMessage, state]) |
||||||
|
return <div className="connectionState" onClick={() => dispatch ? dispatch({type: CLIENT_REFRESH}) : null}>{state}</div> |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
package state |
||||||
|
|
||||||
|
import "errors" |
||||||
|
|
||||||
|
var ErrOneofNotSet = errors.New("no value was given for a oneof") |
Loading…
Reference in new issue