parent
5e7aee5e76
commit
f4ed4a261a
@ -1 +1,24 @@ |
||||
/buildtools |
||||
# dependencies |
||||
/node_modules |
||||
/.pnp |
||||
.pnp.js |
||||
|
||||
# testing |
||||
/coverage |
||||
|
||||
# production |
||||
/build |
||||
|
||||
# Generated protocol buffers files |
||||
/src/proto |
||||
|
||||
# misc |
||||
.DS_Store |
||||
.env.local |
||||
.env.development.local |
||||
.env.test.local |
||||
.env.production.local |
||||
|
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
@ -1,41 +0,0 @@ |
||||
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() |
||||
} |
@ -1,26 +0,0 @@ |
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
||||
|
||||
# dependencies |
||||
/node_modules |
||||
/.pnp |
||||
.pnp.js |
||||
|
||||
# testing |
||||
/coverage |
||||
|
||||
# production |
||||
/build |
||||
|
||||
# Generated protocol buffers files |
||||
/src/proto |
||||
|
||||
# misc |
||||
.DS_Store |
||||
.env.local |
||||
.env.development.local |
||||
.env.test.local |
||||
.env.production.local |
||||
|
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
@ -1,109 +0,0 @@ |
||||
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,17 +0,0 @@ |
||||
import React from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import './index.css'; |
||||
import App from './App'; |
||||
import reportWebVitals from './reportWebVitals'; |
||||
|
||||
ReactDOM.render( |
||||
<React.StrictMode> |
||||
<App /> |
||||
</React.StrictMode>, |
||||
document.getElementById('root') |
||||
); |
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals(); |
@ -1,12 +0,0 @@ |
||||
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 |
||||
) |
@ -1,74 +0,0 @@ |
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= |
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= |
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= |
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= |
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= |
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= |
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
||||
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= |
||||
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= |
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= |
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= |
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= |
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= |
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= |
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= |
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= |
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= |
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= |
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= |
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= |
||||
go.uber.org/zap v1.18.1 h1:CSUJ2mjFszzEWt4CdKISEuChVIXGBn3lAPwkRGyVrc4= |
||||
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= |
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= |
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= |
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= |
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= |
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= |
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= |
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= |
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= |
||||
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= |
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= |
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= |
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= |
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= |
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= |
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= |
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= |
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
@ -1,16 +0,0 @@ |
||||
#!/bin/bash |
||||
|
||||
set -eux |
||||
|
||||
PATH=${GOROOT}/bin:${PATH} |
||||
SCRIPTPATH=$(readlink -e "${BASH_SOURCE[0]}") |
||||
MAINPATH=${SCRIPTPATH%/*} |
||||
BUILDTOOLSPATH=${MAINPATH}/buildtools |
||||
MAGEPATH=${BUILDTOOLSPATH}/mage |
||||
if [[ ! -x "$MAGEPATH" ]]; then |
||||
echo "go install-ing mage..." |
||||
mkdir -p "$BUILDTOOLSPATH" |
||||
GOBIN="$BUILDTOOLSPATH" go install github.com/magefile/mage@latest |
||||
fi |
||||
|
||||
exec "$MAGEPATH" -d "$MAINPATH" -w "$MAINPATH" "$@" |
@ -1,32 +0,0 @@ |
||||
// +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 |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,5 @@ |
||||
syntax = "proto3"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/state"; |
||||
|
||||
message StorageCoordinatesPB { |
||||
uint32 line = 1; |
||||
uint32 cell = 2; |
||||
|
@ -1,56 +0,0 @@ |
||||
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,7 +1,5 @@ |
||||
syntax = "proto3"; |
||||
|
||||
option go_package = "git.reya.zone/reya/hexmap/server/state"; |
||||
|
||||
message UserStatePB { |
||||
fixed32 color = 1; |
||||
} |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
@ -1,2 +0,0 @@ |
||||
*.pb.go |
||||
proto/ |
@ -1,125 +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") |
||||
) |
||||
|
||||
// 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 |
||||
} |
||||
|
||||
// IDed contains a pair of ID and Action, as sent by the client.
|
||||
type IDed struct { |
||||
// ID contains the arbitrary ID that was sent by the client, for identifying the action in future messages.
|
||||
ID uint32 `json:"id"` |
||||
// Action contains the action that was actually being sent.
|
||||
Action Client `json:"action"` |
||||
} |
||||
|
||||
func (i IDed) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddUint32("id", i.ID) |
||||
return encoder.AddObject("action", i.Action) |
||||
} |
@ -1,78 +0,0 @@ |
||||
package action |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
) |
||||
|
||||
func (x *ServerActionPB) ToGo() (Server, error) { |
||||
switch action := x.Action.(type) { |
||||
case nil: |
||||
return nil, state.ErrOneofNotSet |
||||
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 nil: |
||||
return nil, state.ErrOneofNotSet |
||||
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.ColorFromRGBA8888(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.ToRGBA8888(), |
||||
At: c.At.ToPB(), |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func (x *UserSetActiveColorPB) ToGo() *UserActiveColor { |
||||
return &UserActiveColor{ |
||||
Color: state.ColorFromRGBA8888(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.ToRGBA8888(), |
||||
}, |
||||
}, |
||||
} |
||||
} |
@ -1,240 +0,0 @@ |
||||
package action |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"google.golang.org/protobuf/runtime/protoimpl" |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func TestCellColor_ToClientPB(t *testing.T) { |
||||
type fields struct { |
||||
At state.StorageCoordinates |
||||
Color state.Color |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want *ClientActionPB |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := CellColor{ |
||||
At: tt.fields.At, |
||||
Color: tt.fields.Color, |
||||
} |
||||
if got := c.ToClientPB(); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToClientPB() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestCellColor_ToServerPB(t *testing.T) { |
||||
type fields struct { |
||||
At state.StorageCoordinates |
||||
Color state.Color |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want *ServerActionPB |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := CellColor{ |
||||
At: tt.fields.At, |
||||
Color: tt.fields.Color, |
||||
} |
||||
if got := c.ToServerPB(); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToServerPB() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestCellSetColorPB_ToGo(t *testing.T) { |
||||
type fields struct { |
||||
state protoimpl.MessageState |
||||
sizeCache protoimpl.SizeCache |
||||
unknownFields protoimpl.UnknownFields |
||||
Color uint32 |
||||
At *state.StorageCoordinatesPB |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want *CellColor |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
x := &CellSetColorPB{ |
||||
state: tt.fields.state, |
||||
sizeCache: tt.fields.sizeCache, |
||||
unknownFields: tt.fields.unknownFields, |
||||
Color: tt.fields.Color, |
||||
At: tt.fields.At, |
||||
} |
||||
got, err := x.ToGo() |
||||
if (err != nil) != tt.wantErr { |
||||
t.Errorf("ToGo() error = %v, wantErr %v", err, tt.wantErr) |
||||
return |
||||
} |
||||
if !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToGo() got = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestClientActionPB_ToGo(t *testing.T) { |
||||
type fields struct { |
||||
state protoimpl.MessageState |
||||
sizeCache protoimpl.SizeCache |
||||
unknownFields protoimpl.UnknownFields |
||||
Action isClientActionPB_Action |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want Client |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
x := &ClientActionPB{ |
||||
state: tt.fields.state, |
||||
sizeCache: tt.fields.sizeCache, |
||||
unknownFields: tt.fields.unknownFields, |
||||
Action: tt.fields.Action, |
||||
} |
||||
got, err := x.ToGo() |
||||
if (err != nil) != tt.wantErr { |
||||
t.Errorf("ToGo() error = %v, wantErr %v", err, tt.wantErr) |
||||
return |
||||
} |
||||
if !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToGo() got = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestServerActionPB_ToGo(t *testing.T) { |
||||
type fields struct { |
||||
state protoimpl.MessageState |
||||
sizeCache protoimpl.SizeCache |
||||
unknownFields protoimpl.UnknownFields |
||||
Action isServerActionPB_Action |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want Server |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
x := &ServerActionPB{ |
||||
state: tt.fields.state, |
||||
sizeCache: tt.fields.sizeCache, |
||||
unknownFields: tt.fields.unknownFields, |
||||
Action: tt.fields.Action, |
||||
} |
||||
got, err := x.ToGo() |
||||
if (err != nil) != tt.wantErr { |
||||
t.Errorf("ToGo() error = %v, wantErr %v", err, tt.wantErr) |
||||
return |
||||
} |
||||
if !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToGo() got = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestUserActiveColor_ToClientPB(t *testing.T) { |
||||
type fields struct { |
||||
Color state.Color |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want *ClientActionPB |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := UserActiveColor{ |
||||
Color: tt.fields.Color, |
||||
} |
||||
if got := c.ToClientPB(); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToClientPB() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestUserActiveColor_ToServerPB(t *testing.T) { |
||||
type fields struct { |
||||
Color state.Color |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want *ServerActionPB |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := UserActiveColor{ |
||||
Color: tt.fields.Color, |
||||
} |
||||
if got := c.ToServerPB(); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToServerPB() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestUserSetActiveColorPB_ToGo(t *testing.T) { |
||||
type fields struct { |
||||
state protoimpl.MessageState |
||||
sizeCache protoimpl.SizeCache |
||||
unknownFields protoimpl.UnknownFields |
||||
Color uint32 |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
want *UserActiveColor |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
x := &UserSetActiveColorPB{ |
||||
state: tt.fields.state, |
||||
sizeCache: tt.fields.sizeCache, |
||||
unknownFields: tt.fields.unknownFields, |
||||
Color: tt.fields.Color, |
||||
} |
||||
if got := x.ToGo(); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("ToGo() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -1,190 +0,0 @@ |
||||
package action |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"go.uber.org/zap/zapcore" |
||||
"reflect" |
||||
"testing" |
||||
) |
||||
|
||||
func TestCellColor_Apply(t *testing.T) { |
||||
type fields struct { |
||||
At state.StorageCoordinates |
||||
Color state.Color |
||||
} |
||||
type args struct { |
||||
s *state.Synced |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
args args |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := CellColor{ |
||||
At: tt.fields.At, |
||||
Color: tt.fields.Color, |
||||
} |
||||
if err := c.Apply(tt.args.s); (err != nil) != tt.wantErr { |
||||
t.Errorf("Apply() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestCellColor_MarshalLogObject(t *testing.T) { |
||||
type fields struct { |
||||
At state.StorageCoordinates |
||||
Color state.Color |
||||
} |
||||
type args struct { |
||||
encoder zapcore.ObjectEncoder |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
args args |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := CellColor{ |
||||
At: tt.fields.At, |
||||
Color: tt.fields.Color, |
||||
} |
||||
if err := c.MarshalLogObject(tt.args.encoder); (err != nil) != tt.wantErr { |
||||
t.Errorf("MarshalLogObject() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestIDed_MarshalLogObject(t *testing.T) { |
||||
type fields struct { |
||||
ID uint32 |
||||
Action Client |
||||
} |
||||
type args struct { |
||||
encoder zapcore.ObjectEncoder |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
args args |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
i := IDed{ |
||||
ID: tt.fields.ID, |
||||
Action: tt.fields.Action, |
||||
} |
||||
if err := i.MarshalLogObject(tt.args.encoder); (err != nil) != tt.wantErr { |
||||
t.Errorf("MarshalLogObject() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestServerSlice_MarshalLogArray(t *testing.T) { |
||||
type args struct { |
||||
encoder zapcore.ArrayEncoder |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
s ServerSlice |
||||
args args |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
if err := tt.s.MarshalLogArray(tt.args.encoder); (err != nil) != tt.wantErr { |
||||
t.Errorf("MarshalLogArray() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestUserActiveColor_Apply(t *testing.T) { |
||||
type fields struct { |
||||
Color state.Color |
||||
} |
||||
type args struct { |
||||
s *state.Synced |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
args args |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := UserActiveColor{ |
||||
Color: tt.fields.Color, |
||||
} |
||||
if err := c.Apply(tt.args.s); (err != nil) != tt.wantErr { |
||||
t.Errorf("Apply() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestUserActiveColor_MarshalLogObject(t *testing.T) { |
||||
type fields struct { |
||||
Color state.Color |
||||
} |
||||
type args struct { |
||||
encoder zapcore.ObjectEncoder |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
fields fields |
||||
args args |
||||
wantErr bool |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
c := UserActiveColor{ |
||||
Color: tt.fields.Color, |
||||
} |
||||
if err := c.MarshalLogObject(tt.args.encoder); (err != nil) != tt.wantErr { |
||||
t.Errorf("MarshalLogObject() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func Test_serverPBFromClient(t *testing.T) { |
||||
type args struct { |
||||
c Client |
||||
} |
||||
tests := []struct { |
||||
name string |
||||
args args |
||||
want *ServerActionPB |
||||
}{ |
||||
// TODO: Add test cases.
|
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
if got := serverPBFromClient(tt.args.c); !reflect.DeepEqual(got, tt.want) { |
||||
t.Errorf("serverPBFromClient() = %v, want %v", got, tt.want) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -1,193 +0,0 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"git.reya.zone/reya/hexmap/server/room" |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"git.reya.zone/reya/hexmap/server/ws" |
||||
"github.com/gorilla/websocket" |
||||
"go.uber.org/zap" |
||||
"google.golang.org/protobuf/proto" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"os" |
||||
"path/filepath" |
||||
"strconv" |
||||
"time" |
||||
) |
||||
|
||||
const SaveDir = "/home/reya/hexmaps" |
||||
|
||||
func save(m state.HexMap, l *zap.Logger) error { |
||||
filename := filepath.Join(SaveDir, "map."+strconv.FormatInt(time.Now().Unix(), 16)) |
||||
l.Debug("Saving to file", zap.String("filename", filename)) |
||||
marshaled, err := proto.Marshal(m.ToPB()) |
||||
l.Debug("Marshaled proto") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
l.Debug("Opening file") |
||||
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0x644) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
l.Debug("Writing to file") |
||||
_, err = file.Write(marshaled) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
l.Debug("Closing file") |
||||
err = file.Close() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
l.Info("Saved to file", zap.String("filename", filename)) |
||||
return nil |
||||
} |
||||
|
||||
func load(l *zap.Logger) (*state.HexMap, error) { |
||||
filename := filepath.Join(SaveDir, "map.LOAD") |
||||
l.Debug("Loading from file", zap.String("filename", filename)) |
||||
file, err := os.Open(filename) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
l.Debug("Reading file") |
||||
marshaled, err := ioutil.ReadAll(file) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
pb := &state.HexMapPB{} |
||||
l.Debug("Extracting protobuf from file") |
||||
err = proto.Unmarshal(marshaled, pb) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
l.Debug("Closing file") |
||||
err = file.Close() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
m, err := pb.ToGo() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
l.Info("Loaded from file", zap.String("filename", filename)) |
||||
return &m, nil |
||||
} |
||||
|
||||
func BackupMap(client *room.Client, l *zap.Logger) { |
||||
var err error |
||||
myState := &state.Synced{} |
||||
l.Info("Starting backup system") |
||||
for { |
||||
msg := <-client.IncomingChannel() |
||||
switch typedMsg := msg.(type) { |
||||
case *room.JoinResponse: |
||||
myState = typedMsg.CurrentState() |
||||
err := save(myState.Map, l) |
||||
if err != nil { |
||||
l.Error("Failed saving during join response", zap.Error(err)) |
||||
} |
||||
case *room.ActionBroadcast: |
||||
err = typedMsg.Action().Apply(myState) |
||||
if err == nil { |
||||
err = save(myState.Map, l) |
||||
if err != nil { |
||||
l.Error("Failed saving during action broadcast", zap.Error(err)) |
||||
} |
||||
} |
||||
case *room.ShutdownRequest: |
||||
client.OutgoingChannel() <- client.AcknowledgeShutdown() |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func ServeWS(logger *zap.Logger) (err error) { |
||||
m := http.NewServeMux() |
||||
httpLogger := logger.Named("HTTP") |
||||
hexes, err := load(logger) |
||||
if err != nil { |
||||
hexes = state.NewHexMap(state.Layout{ |
||||
Orientation: state.PointyTop, |
||||
IndentedLines: state.EvenLines, |
||||
}, 25, 10) |
||||
} |
||||
rm := room.New(room.NewOptions{ |
||||
BaseLogger: logger.Named("Room"), |
||||
StartingState: &state.Synced{ |
||||
Map: *hexes, |
||||
User: state.UserState{ |
||||
ActiveColor: state.Color{ |
||||
R: 0, |
||||
G: 0, |
||||
B: 0, |
||||
A: 255, |
||||
}, |
||||
}, |
||||
}, |
||||
StartingClientOptions: room.NewClientOptions{ |
||||
IncomingChannel: nil, |
||||
AcceptBroadcasts: true, |
||||
RequestStartingState: true, |
||||
}, |
||||
}) |
||||
go BackupMap(rm, logger.Named("BackupMap")) |
||||
m.Handle("/map", &ws.HTTPHandler{ |
||||
Upgrader: websocket.Upgrader{ |
||||
Subprotocols: []string{"v1.hexmap.deliciousreya.net"}, |
||||
CheckOrigin: func(r *http.Request) bool { |
||||
return r.Header.Get("Origin") == "https://hexmap.deliciousreya.net" |
||||
}, |
||||
}, |
||||
Logger: logger.Named("WS"), |
||||
Room: rm, |
||||
}) |
||||
srv := http.Server{ |
||||
Addr: "127.0.0.1:5238", |
||||
Handler: m, |
||||
ErrorLog: zap.NewStdLog(httpLogger), |
||||
} |
||||
m.HandleFunc("/exit", func(writer http.ResponseWriter, request *http.Request) { |
||||
// Some light dissuasion of accidental probing.
|
||||
// To keep good people out.
|
||||
if request.FormValue("superSecretPassword") != "Gesture/Retrial5/Untrained/Countable/Extrude/Jeep/Cheese/Carbon" { |
||||
writer.WriteHeader(403) |
||||
_, err = writer.Write([]byte("... What are you trying to pull?")) |
||||
return |
||||
} |
||||
writer.WriteHeader(200) |
||||
_, err := writer.Write([]byte("OK, shutting down, bye!")) |
||||
if err != nil { |
||||
logger.Warn("Error while writing goodbye response", zap.Error(err)) |
||||
} |
||||
time.AfterFunc(500*time.Millisecond, func() { |
||||
err := srv.Shutdown(context.Background()) |
||||
if err != nil { |
||||
logger.Error("Error while shutting down the server", zap.Error(err)) |
||||
} |
||||
}) |
||||
}) |
||||
err = srv.ListenAndServe() |
||||
if err != nil && err != http.ErrServerClosed { |
||||
return err |
||||
} |
||||
rm.OutgoingChannel() <- rm.Stop() |
||||
for { |
||||
msg := <-rm.IncomingChannel() |
||||
switch msg.(type) { |
||||
case *room.ShutdownRequest: |
||||
rm.OutgoingChannel() <- rm.AcknowledgeShutdown() |
||||
return nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
func main() { |
||||
logger, err := zap.NewDevelopment() |
||||
err = ServeWS(logger) |
||||
if err != nil { |
||||
logger.Fatal("Error while serving HTTP", zap.Error(err)) |
||||
} |
||||
} |
@ -1,68 +0,0 @@ |
||||
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 |
||||
}) |
||||
} |
@ -1 +0,0 @@ |
||||
package persistence |
@ -1,273 +0,0 @@ |
||||
package room |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/action" |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"github.com/rs/xid" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// act is the meat and potatoes of the room - it's responsible for actually running the room.
|
||||
// It will gracefully remove all clients before shutting down.
|
||||
func (r *room) act() { |
||||
defer r.gracefulShutdown() |
||||
r.logger.Info("Room starting up, listening for incoming actions") |
||||
for { |
||||
raw := <-r.incomingChannel |
||||
client := raw.ClientID() |
||||
msgLogger := r.logger.With(zap.Stringer("client", client)) |
||||
msgLogger.Debug("Message received, handling", zap.Object("message", raw)) |
||||
switch msg := raw.(type) { |
||||
case *JoinRequest: |
||||
r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel) |
||||
r.acknowledgeJoin(msg.id, msg.wantCurrentState) |
||||
case *RefreshRequest: |
||||
r.sendRefresh(msg.id) |
||||
case *ApplyRequest: |
||||
msgLogger.Debug("Received action to apply from client", zap.Uint32("actionId", msg.action.ID)) |
||||
result := r.applyAction(msg.action.Action) |
||||
if result == nil { |
||||
r.broadcastAction(client, msg.action.ID, msg.action.Action) |
||||
} |
||||
r.acknowledgeAction(client, msg.action.ID, result) |
||||
case *LeaveRequest: |
||||
// So long, then. We can close immediately here; they promised not to send any more messages after this
|
||||
// unless we were shutting down, which we're not.
|
||||
r.acknowledgeLeave(client) |
||||
r.closeClient(client) |
||||
case *StopRequest: |
||||
// As requested, we shut down. Our deferred gracefulShutdown will catch us as we fall.
|
||||
msgLogger.Info("Received StopRequest from client, shutting down") |
||||
return |
||||
case *ShutdownResponse: |
||||
// Uh... thank... you. I'm not... Never mind. I guess this means you're leaving?
|
||||
msgLogger.Error("Received unexpected ShutdownResponse from client while not shutting down") |
||||
r.closeClient(client) |
||||
default: |
||||
msgLogger.Warn("Ignoring unhandled message", zap.Object("message", msg)) |
||||
} |
||||
msgLogger.Debug("Message handled, resuming listening") |
||||
} |
||||
} |
||||
|
||||
// addClient records a client's presence in the client map.
|
||||
func (r *room) addClient(id xid.ID, returnChannel chan<- Message, broadcast bool, privateChannel bool) { |
||||
logger := r.logger.With(zap.Stringer("client", id)) |
||||
logger.Debug("Adding client") |
||||
if client, ok := r.clients[id]; ok { |
||||
if client.outgoingChannel == returnChannel { |
||||
if broadcast == client.broadcast && privateChannel == client.privateChannel { |
||||
logger.Warn("Already have client when adding client") |
||||
} else { |
||||
logger.Error("Already have client but with different settings when adding client") |
||||
} |
||||
} else { |
||||
logger.Error("Already have a different client with the same id when adding client") |
||||
} |
||||
return |
||||
} |
||||
r.clients[id] = internalClient{ |
||||
id: id, |
||||
outgoingChannel: returnChannel, |
||||
broadcast: broadcast, |
||||
privateChannel: privateChannel, |
||||
} |
||||
} |
||||
|
||||
// stateCopy creates and returns a fresh copy of the current state.
|
||||
// This avoids concurrent modification of the state while clients are reading it.
|
||||
func (r *room) stateCopy() *state.Synced { |
||||
s := r.currentState.Copy() |
||||
return &s |
||||
} |
||||
|
||||
// acknowledgeJoin composes and sends a JoinResponse to the given client.
|
||||
func (r *room) acknowledgeJoin(id xid.ID, includeState bool) { |
||||
logger := r.logger.With(zap.Stringer("client", id)) |
||||
client, ok := r.clients[id] |
||||
if !ok { |
||||
logger.Error("No such client when acknowledging join") |
||||
return |
||||
} |
||||
var s *state.Synced = nil |
||||
if includeState { |
||||
logger.Debug("Preparing state copy for client") |
||||
s = r.stateCopy() |
||||
} |
||||
logger.Debug("Sending JoinResponse to client") |
||||
client.outgoingChannel <- &JoinResponse{ |
||||
id: r.id, |
||||
currentState: s, |
||||
} |
||||
} |
||||
|
||||
// sendRefresh composes and sends a RefreshResponse to the given client.
|
||||
func (r *room) sendRefresh(id xid.ID) { |
||||
logger := r.logger.With(zap.Stringer("client", id)) |
||||
client, ok := r.clients[id] |
||||
if !ok { |
||||
logger.Error("No such client when sending refresh") |
||||
return |
||||
} |
||||
var s *state.Synced = nil |
||||
logger.Debug("Preparing state copy for client") |
||||
s = r.stateCopy() |
||||
logger.Debug("Sending RefreshResponse to client") |
||||
client.outgoingChannel <- &RefreshResponse{ |
||||
id: r.id, |
||||
currentState: s, |
||||
} |
||||
} |
||||
|
||||
// applyAction applies an action to the state and returns the result of it.
|
||||
func (r *room) applyAction(action action.Action) error { |
||||
r.logger.Debug("Applying action", zap.Object("action", action)) |
||||
return action.Apply(&r.currentState) |
||||
} |
||||
|
||||
// broadcastAction sends an action to everyone other than the original client which requested it.
|
||||
func (r *room) broadcastAction(originalClientID xid.ID, originalActionID uint32, action action.Server) { |
||||
logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Uint32("actionID", originalActionID), zap.Object("action", action)) |
||||
broadcast := ActionBroadcast{ |
||||
id: r.id, |
||||
originalClientID: originalClientID, |
||||
originalActionID: originalActionID, |
||||
action: action, |
||||
} |
||||
logger.Debug("Broadcasting action to all clients") |
||||
for id, client := range r.clients { |
||||
if id.Compare(originalClientID) != 0 { |
||||
logger.Debug("Sending ActionBroadcast to client", zap.Stringer("client", id)) |
||||
client.outgoingChannel <- &broadcast |
||||
} |
||||
} |
||||
} |
||||
|
||||
// acknowledgeAction sends a response to the original client which requested an action.
|
||||
func (r *room) acknowledgeAction(id xid.ID, actionID uint32, error error) { |
||||
logger := r.logger.With(zap.Stringer("id", id), zap.Uint32("actionId", actionID), zap.Error(error)) |
||||
logger.Debug("Responding to client with the status of its action") |
||||
client, ok := r.clients[id] |
||||
if !ok { |
||||
logger.Error("No such client when acknowledging action") |
||||
return |
||||
} |
||||
logger.Debug("Sending ApplyResponse to client") |
||||
client.outgoingChannel <- &ApplyResponse{ |
||||
id: id, |
||||
actionID: actionID, |
||||
result: error, |
||||
} |
||||
} |
||||
|
||||
// acknowledgeLeave causes the room to signal to the client that it has received and acknowledged the client's LeaveRequest,
|
||||
// and will not send any further messages.
|
||||
func (r *room) acknowledgeLeave(id xid.ID) { |
||||
logger := r.logger.With(zap.Stringer("client", id)) |
||||
logger.Debug("Acknowledging client's leave request") |
||||
client, ok := r.clients[id] |
||||
if !ok { |
||||
logger.Error("No such client when acknowledging leave request") |
||||
return |
||||
} |
||||
logger.Debug("Sending LeaveResponse to client") |
||||
client.outgoingChannel <- &LeaveResponse{id: r.id} |
||||
} |
||||
|
||||
// closeClient causes the room to remove the client with the given id from its clients.
|
||||
// This should only be used after a shutdown handshake between this client and the room has taken place.
|
||||
func (r *room) closeClient(id xid.ID) { |
||||
logger := r.logger.With(zap.Stringer("client", id)) |
||||
logger.Debug("Closing client") |
||||
client, ok := r.clients[id] |
||||
if !ok { |
||||
logger.Error("Attempted to close a client that didn't exist") |
||||
return |
||||
} |
||||
if client.privateChannel { |
||||
logger.Debug("Closing outgoingChannel, as client has a privateChannel") |
||||
close(client.outgoingChannel) |
||||
} |
||||
delete(r.clients, id) |
||||
} |
||||
|
||||
// gracefulShutdown causes the room to shut down cleanly, making sure that all clients have been removed.
|
||||
func (r *room) gracefulShutdown() { |
||||
defer r.finalShutdown() |
||||
if len(r.clients) == 0 { |
||||
// Nothing to do, we've already won.
|
||||
r.logger.Debug("No remaining clients, so just shutting down") |
||||
return |
||||
} |
||||
r.requestShutdown() |
||||
for len(r.clients) > 0 { |
||||
raw := <-r.incomingChannel |
||||
client := raw.ClientID() |
||||
msgLogger := r.logger.With(zap.Stringer("client", client)) |
||||
msgLogger.Debug("Post-shutdown message received, handling", zap.Object("message", raw)) |
||||
switch msg := raw.(type) { |
||||
case *JoinRequest: |
||||
// Don't you hate it when someone comes to the desk right as you're getting ready to pack up?
|
||||
// Can't ignore them - they have our channel, and they might be sending things to it. We have to add them
|
||||
// and then immediately send them a ShutdownRequest and wait for them to answer it.
|
||||
msgLogger.Debug("Received join request from client while shutting down") |
||||
r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel) |
||||
r.requestShutdownFrom(client) |
||||
case *RefreshRequest: |
||||
// Ugh, seriously, now? Fine. You can have this - you might be our friend the persistence actor.
|
||||
r.sendRefresh(client) |
||||
case *LeaveRequest: |
||||
// We sent them a shutdown already, so unfortunately, we can't close them immediately. We have to wait for
|
||||
// them to tell us they've heard that we're shutting down.
|
||||
msgLogger.Debug("Received leave request from client while shutting down") |
||||
r.acknowledgeLeave(client) |
||||
case *StopRequest: |
||||
// Yes. We're doing that. Check your inbox, I already sent you the shutdown.
|
||||
msgLogger.Debug("Received stop request from client while shutting down") |
||||
case *ShutdownResponse: |
||||
// The only way we would be getting one of these is if the client knows it doesn't have to send us anything
|
||||
// else. Therefore, we can remove them now.
|
||||
// Similarly, we know that they'll receive the LeaveResponse they need and shut down.
|
||||
// Like us, even if it sent a LeaveRequest before realizing we were shutting down, it would have gotten our
|
||||
// ShutdownRequest and sent this before it could read the LeaveResponse, but it will wait for the
|
||||
// LeaveResponse regardless.
|
||||
msgLogger.Debug("Received shutdown confirmation from client") |
||||
r.closeClient(client) |
||||
default: |
||||
msgLogger.Debug("Ignoring irrelevant message from client while shutting down", zap.Object("message", raw)) |
||||
} |
||||
msgLogger.Debug("Message handled, resuming listening and waiting to be safe to shut down", zap.Int("clientsLeft", len(r.clients))) |
||||
} |
||||
r.logger.Debug("All clients have acknowledged the ShutdownRequest, now shutting down") |
||||
} |
||||
|
||||
// requestShutdown produces a ShutdownRequest and sends it to all clients to indicate that the room is shutting down.
|
||||
func (r *room) requestShutdown() { |
||||
r.logger.Debug("Alerting clients that shutdown is in progress", zap.Int("clientsLeft", len(r.clients))) |
||||
for id := range r.clients { |
||||
r.requestShutdownFrom(id) |
||||
} |
||||
} |
||||
|
||||
// requestShutdownFrom produces a ShutdownRequest and sends it to the client with id to indicate that the room is
|
||||
// shutting down.
|
||||
func (r *room) requestShutdownFrom(id xid.ID) { |
||||
clientField := zap.Stringer("client", id) |
||||
r.logger.Debug("Alerting client that shutdown is in progress", clientField) |
||||
shutdown := ShutdownRequest{ |
||||
id: r.id, |
||||
} |
||||
client, ok := r.clients[id] |
||||
if !ok { |
||||
r.logger.Error("No such client when requesting shutdown from client", clientField) |
||||
} |
||||
client.outgoingChannel <- &shutdown |
||||
} |
||||
|
||||
// finalShutdown causes the room to do any final cleanup not involving its clients before stopping.
|
||||
// Use gracefulShutdown instead, which calls this once it's safe to do so.
|
||||
func (r *room) finalShutdown() { |
||||
r.logger.Debug("Closing incoming channel") |
||||
close(r.incomingChannel) |
||||
r.logger.Info("Shut down") |
||||
} |
@ -1,173 +0,0 @@ |
||||
package room |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/action" |
||||
"github.com/rs/xid" |
||||
) |
||||
|
||||
// internalClient is used by the room itself to track information about a client.
|
||||
type internalClient struct { |
||||
// id is the id that the client identifies itself with in all clientMessage instances it sends.
|
||||
id xid.ID |
||||
// outgoingChannel is a channel that the room can send messages to the client on.
|
||||
outgoingChannel chan<- Message |
||||
// privateChannel is true iff the room can close the outgoingChannel when the client and room have completed their
|
||||
// close handshake.
|
||||
privateChannel bool |
||||
// broadcast is true iff the client requested to be included on broadcasts on creation.
|
||||
broadcast bool |
||||
} |
||||
|
||||
type NewClientOptions struct { |
||||
// IncomingChannel is the channel to use as the room's channel to send messages to - the new Client's IncomingChannel.
|
||||
// If this is non-nil, the room will not automatically close the IncomingChannel after a shutdown is negotiated.
|
||||
// If this is nil, a new channel will be allocated on join and closed on shutdown.
|
||||
IncomingChannel chan Message |
||||
// If AcceptBroadcasts is true, the room will send all broadcasts originating from other clients to this client.
|
||||
AcceptBroadcasts bool |
||||
// If RequestStartingState is true, the room will send a copy of the current state as of when the JoinRequest was
|
||||
// received in the JoinResponse that will be the first message the Client receives.
|
||||
RequestStartingState bool |
||||
} |
||||
|
||||
// Client is the structure used by clients external to the Room package to communicate with the Room.
|
||||
// It is not expected to be parallel-safe; to run it in parallel, use the NewClient method and send the new client to
|
||||
// the new goroutine.
|
||||
type Client struct { |
||||
// id is the ClientID used by the client for all communications.
|
||||
id xid.ID |
||||
// roomId is the unique ID of the room (not its map).
|
||||
roomId xid.ID |
||||
// incomingChannel is the channel that this client receives messages on.
|
||||
incomingChannel <-chan Message |
||||
// outgoingChannel is the channel that this client sends messages on.
|
||||
// Becomes nil if the client has been completely shut down.
|
||||
outgoingChannel chan<- ClientMessage |
||||
// Once Leave or AcknowledgeShutdown have been triggered, this flag is set, preventing use of other messages.
|
||||
shuttingDown bool |
||||
} |
||||
|
||||
// ID is the ID used by this client to identify itself to the Room.
|
||||
func (c *Client) ID() xid.ID { |
||||
return c.id |
||||
} |
||||
|
||||
// RoomID is the ID used by the room to differentiate itself from other rooms.
|
||||
// It is not the map's internal ID.
|
||||
func (c *Client) RoomID() xid.ID { |
||||
return c.id |
||||
} |
||||
|
||||
// IncomingChannel is the channel the client can listen on for messages from the room.
|
||||
func (c *Client) IncomingChannel() <-chan Message { |
||||
return c.incomingChannel |
||||
} |
||||
|
||||
// OutgoingChannel is the channel the client can send messages to the room on.
|
||||
func (c *Client) OutgoingChannel() chan<- ClientMessage { |
||||
if c.outgoingChannel == nil { |
||||
panic("Already finished shutting down; no new messages should be sent") |
||||
} |
||||
return c.outgoingChannel |
||||
} |
||||
|
||||
// newClientForRoom uses the necessary parameters to create and join a Client for the given room.
|
||||
func newClientForRoom(roomId xid.ID, outgoingChannel chan<- ClientMessage, opts NewClientOptions) *Client { |
||||
var privateChannel bool |
||||
var incomingChannel chan Message |
||||
if opts.IncomingChannel != nil { |
||||
incomingChannel = opts.IncomingChannel |
||||
privateChannel = false |
||||
} else { |
||||
incomingChannel = make(chan Message, 1) |
||||
privateChannel = true |
||||
} |
||||
result := Client{ |
||||
id: xid.New(), |
||||
roomId: roomId, |
||||
incomingChannel: incomingChannel, |
||||
outgoingChannel: outgoingChannel, |
||||
shuttingDown: false, |
||||
} |
||||
result.outgoingChannel <- &JoinRequest{ |
||||
id: result.id, |
||||
returnChannel: incomingChannel, |
||||
privateChannel: privateChannel, |
||||
broadcast: opts.AcceptBroadcasts, |
||||
wantCurrentState: opts.RequestStartingState, |
||||
} |
||||
return &result |
||||
} |
||||
|
||||
// NewClient creates a new client belonging to the same room as this client with a random ID.
|
||||
// The new client will be automatically joined to the channel.
|
||||
func (c *Client) NewClient(opts NewClientOptions) *Client { |
||||
if c.shuttingDown { |
||||
panic("Already started shutting down; no new messages should be sent") |
||||
} |
||||
return newClientForRoom(c.roomId, c.outgoingChannel, opts) |
||||
} |
||||
|
||||
// Refresh creates a message which causes the client to request a fresh copy of the state.
|
||||
func (c *Client) Refresh() *RefreshRequest { |
||||
if c.shuttingDown { |
||||
panic("Already started shutting down; no new messages should be sent") |
||||
} |
||||
return &RefreshRequest{ |
||||
id: c.id, |
||||
} |
||||
} |
||||
|
||||
func (c *Client) Apply(a action.IDed) *ApplyRequest { |
||||
if c.shuttingDown { |
||||
panic("Already started shutting down; no new messages should be sent") |
||||
} |
||||
return &ApplyRequest{ |
||||
id: c.id, |
||||
action: a, |
||||
} |
||||
} |
||||
|
||||
// Leave creates a message which causes the local client to signal that it is shutting down.
|
||||
// It is important to Leave to avoid dangling clients having messages sent to nothing.
|
||||
// After sending Leave, the client must confirm that it has been removed by waiting for a LeaveResponse, accompanied by
|
||||
// the closing of the Client's IncomingChannel if it was a private channel.
|
||||
// No further messages should be sent after Leave except AcknowledgeShutdown if Leave and requestShutdown crossed paths in midair.
|
||||
func (c *Client) Leave() *LeaveRequest { |
||||
if c.shuttingDown { |
||||
panic("Already started shutting down; no new messages should be sent") |
||||
} |
||||
c.shuttingDown = true |
||||
return &LeaveRequest{ |
||||
id: c.id, |
||||
} |
||||
} |
||||
|
||||
// Stop creates a message which causes the local client to signal that it is shutting down.
|
||||
// It is important to Stop when the room needs to be shut down.
|
||||
// After sending Stop, the client must confirm that it has been removed by waiting for a ShutdownRequest, which should
|
||||
// be handled normally.
|
||||
// No further messages should be sent after Stop except AcknowledgeShutdown.
|
||||
func (c *Client) Stop() *StopRequest { |
||||
if c.shuttingDown { |
||||
panic("Already started shutting down; no new messages should be sent") |
||||
} |
||||
c.shuttingDown = true |
||||
return &StopRequest{ |
||||
id: c.id, |
||||
} |
||||
} |
||||
|
||||
// AcknowledgeShutdown causes the local client to signal that it has acknowledged that the room is shutting down.
|
||||
// No further messages can be sent after AcknowledgeShutdown; attempting to do so will block forever, as the
|
||||
// OutgoingChannel has become nil.
|
||||
func (c *Client) AcknowledgeShutdown() *ShutdownResponse { |
||||
if c.outgoingChannel == nil { |
||||
panic("Already finished shutting down; no new messages should be sent") |
||||
} |
||||
c.shuttingDown = true |
||||
c.outgoingChannel = nil |
||||
return &ShutdownResponse{ |
||||
id: c.id, |
||||
} |
||||
} |
@ -1,124 +0,0 @@ |
||||
package room |
||||
|
||||
import ( |
||||
action2 "git.reya.zone/reya/hexmap/server/action" |
||||
"github.com/rs/xid" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
// ClientMessage marks messages coming from clients to the room.
|
||||
type ClientMessage interface { |
||||
zapcore.ObjectMarshaler |
||||
// ClientID is the id of the client sending the message.
|
||||
ClientID() xid.ID |
||||
} |
||||
|
||||
// JoinRequest is the message sent on the room's IncomingChannel by a new client joining the room.
|
||||
type JoinRequest struct { |
||||
// id is the SourceID the client will use to identify itself in future messages.
|
||||
id xid.ID |
||||
// returnChannel is a buffered channel the client is ready to receive messages from the room on.
|
||||
// This becomes the Client's OutgoingChannel.
|
||||
returnChannel chan<- Message |
||||
// privateChannel is true iff the room can close returnChannel after completing a shutdown handshake.
|
||||
// This permits extra safety by causing channels that somehow leak into other contexts to become noticeable by
|
||||
// causing panics.
|
||||
privateChannel bool |
||||
// broadcast is true iff the room should send action.Syncable from other clients to this one.
|
||||
broadcast bool |
||||
// wantCurrentState indicates that the client would like the room to include a copy of the current state of the room
|
||||
// when it joins.
|
||||
wantCurrentState bool |
||||
} |
||||
|
||||
func (j *JoinRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "JoinRequest") |
||||
encoder.AddString("id", j.id.String()) |
||||
encoder.AddBool("broadcast", j.broadcast) |
||||
encoder.AddBool("wantCurrentState", j.wantCurrentState) |
||||
return nil |
||||
} |
||||
|
||||
func (j *JoinRequest) ClientID() xid.ID { |
||||
return j.id |
||||
} |
||||
|
||||
// RefreshRequest is the message sent on the room's IncomingChannel by a client which needs the current value.
|
||||
type RefreshRequest struct { |
||||
id xid.ID |
||||
} |
||||
|
||||
func (r *RefreshRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "RefreshRequest") |
||||
encoder.AddString("id", r.id.String()) |
||||
return nil |
||||
} |
||||
|
||||
func (r *RefreshRequest) ClientID() xid.ID { |
||||
return r.id |
||||
} |
||||
|
||||
// ApplyRequest is the message sent on the room's IncomingChannel by a client which has received an action from the
|
||||
// ws.
|
||||
type ApplyRequest struct { |
||||
id xid.ID |
||||
action action2.IDed |
||||
} |
||||
|
||||
func (f *ApplyRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "ApplyRequest") |
||||
encoder.AddString("id", f.id.String()) |
||||
return encoder.AddObject("action", f.action) |
||||
} |
||||
|
||||
func (f *ApplyRequest) ClientID() xid.ID { |
||||
return f.id |
||||
} |
||||
|
||||
// LeaveRequest is the message sent on the room's IncomingChannel by a client which is shutting down.
|
||||
// The client is indicating that it will send no messages except a possible ShutdownResponse, in the event that a
|
||||
// LeaveRequest and a ShutdownRequest cross paths midflight.
|
||||
type LeaveRequest struct { |
||||
id xid.ID |
||||
} |
||||
|
||||
func (l *LeaveRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "LeaveRequest") |
||||
encoder.AddString("id", l.id.String()) |
||||
return nil |
||||
} |
||||
|
||||
func (l *LeaveRequest) ClientID() xid.ID { |
||||
return l.id |
||||
} |
||||
|
||||
// StopRequest is the message sent on the room's IncomingChannel by a client which wants to make the room shut down.
|
||||
// The response to a StopRequest is a ShutdownRequest, which should be handled as normal.
|
||||
type StopRequest struct { |
||||
id xid.ID |
||||
} |
||||
|
||||
func (s *StopRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "StopRequest") |
||||
encoder.AddString("id", s.id.String()) |
||||
return nil |
||||
} |
||||
|
||||
func (s *StopRequest) ClientID() xid.ID { |
||||
return s.id |
||||
} |
||||
|
||||
// ShutdownResponse is the message sent on the room's IncomingChannel by a client which has accepted the room's ShutdownRequest.
|
||||
type ShutdownResponse struct { |
||||
id xid.ID |
||||
} |
||||
|
||||
func (s *ShutdownResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "ShutdownResponse") |
||||
encoder.AddString("id", s.id.String()) |
||||
return nil |
||||
} |
||||
|
||||
func (s *ShutdownResponse) ClientID() xid.ID { |
||||
return s.id |
||||
} |
@ -1,160 +0,0 @@ |
||||
package room |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/action" |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"github.com/rs/xid" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
// Message marks messages going to clients from the room.
|
||||
type Message interface { |
||||
zapcore.ObjectMarshaler |
||||
// RoomID marks the ID of the room this Message originated from, in case the Client has a shared IncomingChannel.
|
||||
RoomID() xid.ID |
||||
} |
||||
|
||||
// JoinResponse is the message sent by the room on a new client's IncomingChannel after it joins.
|
||||
type JoinResponse struct { |
||||
id xid.ID |
||||
// currentState is a pointer to a copy of the room's current state.
|
||||
// If the client refused the room state in its join message, this will be a nil pointer instead.
|
||||
currentState *state.Synced |
||||
} |
||||
|
||||
func (j *JoinResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "JoinResponse") |
||||
encoder.AddString("id", j.id.String()) |
||||
return encoder.AddObject("currentState", j.currentState) |
||||
} |
||||
|
||||
func (j *JoinResponse) RoomID() xid.ID { |
||||
return j.id |
||||
} |
||||
|
||||
// CurrentState returns the state of the room as of when the JoinRequest was processed.
|
||||
func (j *JoinResponse) CurrentState() *state.Synced { |
||||
return j.currentState |
||||
} |
||||
|
||||
// RefreshResponse is the message sent by the room after a client requests it, or immediately on join.
|
||||
type RefreshResponse struct { |
||||
id xid.ID |
||||
// currentState is a pointer to a copy of the room's current state.
|
||||
currentState *state.Synced |
||||
} |
||||
|
||||
func (r *RefreshResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "JoinResponse") |
||||
encoder.AddString("id", r.id.String()) |
||||
return encoder.AddObject("currentState", r.currentState) |
||||
} |
||||
|
||||
func (r *RefreshResponse) RoomID() xid.ID { |
||||
return r.id |
||||
} |
||||
|
||||
// CurrentState returns the state of the room as of when the RefreshRequest was processed.
|
||||
func (r *RefreshResponse) CurrentState() *state.Synced { |
||||
return r.currentState |
||||
} |
||||
|
||||
// ApplyResponse returns the result of an action to _only_ the one that sent the ApplyRequest.
|
||||
type ApplyResponse struct { |
||||
id xid.ID |
||||
// actionID is the ID of the action that completed or failed.
|
||||
actionID uint32 |
||||
// result is nil if the action was completed, or an error if it failed.
|
||||
result error |
||||
} |
||||
|
||||
func (a *ApplyResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "ApplyResponse") |
||||
encoder.AddString("id", a.id.String()) |
||||
encoder.AddUint32("actionId", a.actionID) |
||||
encoder.AddBool("success", a.result == nil) |
||||
if a.result != nil { |
||||
encoder.AddString("failure", a.result.Error()) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (a *ApplyResponse) RoomID() xid.ID { |
||||
return a.id |
||||
} |
||||
|
||||
func (a *ApplyResponse) ActionID() uint32 { |
||||
return a.actionID |
||||
} |
||||
|
||||
// Success returns true if the action succeeded, false if it failed.
|
||||
func (a *ApplyResponse) Success() bool { |
||||
return a.result == nil |
||||
} |
||||
|
||||
// Failure returns the error if the action failed, or nil if it succeeded.
|
||||
func (a *ApplyResponse) Failure() error { |
||||
return a.result |
||||
} |
||||
|
||||
// ActionBroadcast is sent to all clients _other_ than the one that sent the ApplyRequest when an action succeeds.
|
||||
type ActionBroadcast struct { |
||||
id xid.ID |
||||
// originalClientID is the client that sent the action in the first place.
|
||||
originalClientID xid.ID |
||||
// originalActionID is the ID that the client that sent the action sent.
|
||||
originalActionID uint32 |
||||
// action is the action that succeeded.
|
||||
action action.Server |
||||
} |
||||
|
||||
func (a *ActionBroadcast) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "ActionBroadcast") |
||||
encoder.AddString("id", a.id.String()) |
||||
encoder.AddString("originalClientId", a.originalClientID.String()) |
||||
encoder.AddUint32("originalActionId", a.originalActionID) |
||||
return encoder.AddObject("action", a.action) |
||||
} |
||||
|
||||
func (a *ActionBroadcast) RoomID() xid.ID { |
||||
return a.id |
||||
} |
||||
func (a *ActionBroadcast) OriginalClientID() xid.ID { |
||||
return a.originalClientID |
||||
} |
||||
func (a *ActionBroadcast) OriginalActionID() uint32 { |
||||
return a.originalActionID |
||||
} |
||||
func (a *ActionBroadcast) Action() action.Server { |
||||
return a.action |
||||
} |
||||
|
||||
// LeaveResponse is the message sent by the room when it has accepted that a client has left, and will send it no further messages.
|
||||
type LeaveResponse struct { |
||||
id xid.ID |
||||
} |
||||
|
||||
func (l *LeaveResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "LeaveResponse") |
||||
encoder.AddString("id", l.id.String()) |
||||
return nil |
||||
} |
||||
|
||||
func (l *LeaveResponse) RoomID() xid.ID { |
||||
return l.id |
||||
} |
||||
|
||||
// ShutdownRequest is the message sent by the room when something causes it to shut down. It will send the client no further messages except a possible LeaveResponse.
|
||||
type ShutdownRequest struct { |
||||
id xid.ID |
||||
} |
||||
|
||||
func (s *ShutdownRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "ShutdownRequest") |
||||
encoder.AddString("id", s.id.String()) |
||||
return nil |
||||
} |
||||
|
||||
func (s *ShutdownRequest) RoomID() xid.ID { |
||||
return s.id |
||||
} |
@ -1,54 +0,0 @@ |
||||
package room |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"github.com/rs/xid" |
||||
"go.uber.org/zap" |
||||
) |
||||
|
||||
// NewOptions is the set of information used to control what a room starts with.
|
||||
type NewOptions struct { |
||||
// BaseLogger is the logger that the room should attach its data to.
|
||||
BaseLogger *zap.Logger |
||||
// StartingState is a state.Synced that defines the state of the room on creation.
|
||||
StartingState *state.Synced |
||||
// StartingClientOptions sets the configuration of the first client to be created, the one that will be returned
|
||||
// from New.
|
||||
StartingClientOptions NewClientOptions |
||||
} |
||||
|
||||
// room is a room as seen from within - the information needed for the room process to do its duties.
|
||||
type room struct { |
||||
// id is the room's internal ID - not to be confused with the ID of the map it is serving.
|
||||
id xid.ID |
||||
// incomingChannel is the channel the room uses to receive messages. The room itself owns this channel,
|
||||
// so when the clients map is empty, it can be closed.
|
||||
incomingChannel chan ClientMessage |
||||
// clients contains the map of active clients, each of which is known to have a reference to this room.
|
||||
clients map[xid.ID]internalClient |
||||
// currentState contains the active state being used by actions right now.
|
||||
currentState state.Synced |
||||
// logger is the logger that this room will use. It contains context fields for the room's important fields.
|
||||
logger *zap.Logger |
||||
} |
||||
|
||||
// New creates and starts up a new room, joins a new client to it and returns that client.
|
||||
func New(opts NewOptions) *Client { |
||||
logger := opts.BaseLogger.Named("Room") |
||||
id := xid.New() |
||||
r := room{ |
||||
id: id, |
||||
incomingChannel: make(chan ClientMessage), |
||||
clients: make(map[xid.ID]internalClient), |
||||
currentState: *opts.StartingState, |
||||
logger: logger, |
||||
} |
||||
go r.act() |
||||
return r.newClient(opts.StartingClientOptions) |
||||
} |
||||
|
||||
// newClient creates a new client belonging to this room with a random ID.
|
||||
// The new client will be automatically joined to the channel.
|
||||
func (r *room) newClient(opts NewClientOptions) *Client { |
||||
return newClientForRoom(r.id, r.incomingChannel, opts) |
||||
} |
@ -1,71 +0,0 @@ |
||||
package state |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
// Color is an internal representation of an RGBA8888 color.
|
||||
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 |
||||
} |
||||
|
||||
var TransparentColor = Color{R: 0, G: 0, B: 0, A: 0} |
||||
var ErrInvalidColorString = errors.New("color strings must start with # and be followed by 3, 4, 6, or 8 hex digits") |
||||
|
||||
func ColorFromString(s string) (Color, error) { |
||||
if !strings.HasPrefix(s, "#") { |
||||
return TransparentColor, ErrInvalidColorString |
||||
} |
||||
hex := s[1:] |
||||
v, err := strconv.ParseUint(hex, 16, 64) |
||||
if err != nil { |
||||
return TransparentColor, ErrInvalidColorString |
||||
} |
||||
switch len(hex) { |
||||
case 3: |
||||
return ColorFromRGBA4444(uint16(v<<4 | 0xF)), nil |
||||
case 4: |
||||
return ColorFromRGBA4444(uint16(v)), nil |
||||
case 6: |
||||
return ColorFromRGBA8888(uint32(v<<8 | 0xFF)), nil |
||||
case 8: |
||||
return ColorFromRGBA8888(uint32(v)), nil |
||||
default: |
||||
return TransparentColor, ErrInvalidColorString |
||||
} |
||||
} |
||||
|
||||
// String prints the Color in 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) |
||||
} |
||||
} |
||||
} |
@ -1,31 +0,0 @@ |
||||
package state |
||||
|
||||
// ColorFromRGBA8888 decodes a packed uint32 (RGBA8888) into a hex color.
|
||||
func ColorFromRGBA8888(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), |
||||
} |
||||
} |
||||
|
||||
// ColorFromRGBA4444 decodes a packed uint16 (RGBA4444) into a hex color.
|
||||
func ColorFromRGBA4444(value uint16) Color { |
||||
return Color{ |
||||
R: uint8((value>>12)&0xF) * 0x11, |
||||
G: uint8((value>>8)&0xF) * 0x11, |
||||
B: uint8((value>>4)&0xF) * 0x11, |
||||
A: uint8((value>>0)&0xF) * 0x11, |
||||
} |
||||
} |
||||
|
||||
// ToRGBA8888 packs a hex color into a uint32 as RGBA8888.
|
||||
func (c Color) ToRGBA8888() uint32 { |
||||
return uint32(c.R)<<24 | uint32(c.G)<<16 | uint32(c.B)<<8 | uint32(c.A) |
||||
} |
||||
|
||||
// ToRGBA4444 packs a hex color into a uint16 as RGBA4444.
|
||||
func (c Color) ToRGBA4444() uint16 { |
||||
return uint16((c.R>>4)&0xF)<<12 | uint16((c.G>>4)&0xF)<<8 | uint16((c.B>>4)&0xF)<<4 | uint16((c.A>>4)&0xF) |
||||
} |
@ -1,19 +0,0 @@ |
||||
package state |
||||
|
||||
import ( |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
// StorageCoordinates gives the coordinates of a cell in a form optimized for storage.
|
||||
type StorageCoordinates struct { |
||||
// Line is the index from 0 to Lines - 1 of the HexLine in the HexLayer.
|
||||
Line uint8 `json:"line"` |
||||
// Cell is the index from 0 to CellsPerLine - 1 of the HexCell in the HexLine.
|
||||
Cell uint8 `json:"cell"` |
||||
} |
||||
|
||||
func (s StorageCoordinates) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddUint8("line", s.Line) |
||||
encoder.AddUint8("cell", s.Cell) |
||||
return nil |
||||
} |
@ -1,25 +0,0 @@ |
||||
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,206 +0,0 @@ |
||||
package state |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/rs/xid" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
// HexOrientation is the enum for the direction hexes are facing.
|
||||
type HexOrientation uint8 |
||||
|
||||
const ( |
||||
// UnknownOrientation indicates that an invalid orientation was specified.
|
||||
UnknownOrientation HexOrientation = 0 |
||||
// PointyTop indicates hexes that have a pair of sides on either side in the horizontal direction,
|
||||
// and points on the top and bottom in the vertical direction.
|
||||
PointyTop HexOrientation = 1 |
||||
// FlatTop indicates hexes that have a pair of points on either side in the horizontal direction,
|
||||
// and sides on the top and bottom in the vertical direction.
|
||||
FlatTop HexOrientation = 2 |
||||
) |
||||
|
||||
// String returns the equivalent JavaScript constant name.
|
||||
func (o HexOrientation) String() string { |
||||
switch o { |
||||
case PointyTop: |
||||
return "POINTY_TOP" |
||||
case FlatTop: |
||||
return "FLAT_TOP" |
||||
default: |
||||
return fmt.Sprintf("[unknown HexOrientation %d]", o) |
||||
} |
||||
} |
||||
|
||||
// LineParity indicates whether odd or even lines are indented.
|
||||
type LineParity uint8 |
||||
|
||||
const ( |
||||
// UnknownParity indicates that parity was not specified or unknown.
|
||||
UnknownParity LineParity = 0 |
||||
// OddLines indicates that odd lines - 1, 3, 5... - are indented by 1/2 cell.
|
||||
OddLines LineParity = 1 |
||||
// EvenLines indicates that even lines - 0, 2, 4... - are indented by 1/2 cell.
|
||||
EvenLines LineParity = 2 |
||||
) |
||||
|
||||
// String returns the equivalent JavaScript constant name.
|
||||
func (o LineParity) String() string { |
||||
switch o { |
||||
case OddLines: |
||||
return "ODD_LINES" |
||||
case EvenLines: |
||||
return "EVEN_LINES" |
||||
default: |
||||
return fmt.Sprintf("[unknown LineParity %d]", o) |
||||
} |
||||
} |
||||
|
||||
// Layout combines HexOrientation and LineParity to represent a map's display mode.
|
||||
type Layout struct { |
||||
Orientation HexOrientation `json:"orientation"` |
||||
IndentedLines LineParity `json:"indentedLines"` |
||||
} |
||||
|
||||
func (l Layout) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("orientation", l.Orientation.String()) |
||||
encoder.AddString("indentedLines", l.IndentedLines.String()) |
||||
return nil |
||||
} |
||||
|
||||
// HexCell contains data for a single cell of the map.
|
||||
type HexCell struct { |
||||
// Color contains the color of the cell, in hex notation.
|
||||
Color Color `json:"color"` |
||||
} |
||||
|
||||
func (h HexCell) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("color", h.Color.String()) |
||||
return nil |
||||
} |
||||
|
||||
// HexLine is a line of cells which are adjacent by flat sides in a vertical or horizontal direction.
|
||||
type HexLine []HexCell |
||||
|
||||
func (l HexLine) MarshalLogArray(encoder zapcore.ArrayEncoder) error { |
||||
var finalErr error |
||||
for _, cell := range l { |
||||
err := encoder.AppendObject(cell) |
||||
if err != nil && finalErr == nil { |
||||
finalErr = err |
||||
} |
||||
} |
||||
return finalErr |
||||
} |
||||
|
||||
// Copy creates a deep copy of this HexLine.
|
||||
func (l HexLine) Copy() HexLine { |
||||
duplicate := make(HexLine, len(l)) |
||||
for index, value := range l { |
||||
duplicate[index] = value |
||||
} |
||||
return duplicate |
||||
} |
||||
|
||||
// HexLayer is a two-dimensional plane of cells which are arranged into lines.
|
||||
type HexLayer []HexLine |
||||
|
||||
func (l HexLayer) MarshalLogArray(encoder zapcore.ArrayEncoder) error { |
||||
var finalErr error |
||||
for _, line := range l { |
||||
err := encoder.AppendArray(line) |
||||
if err != nil && finalErr == nil { |
||||
finalErr = err |
||||
} |
||||
} |
||||
return finalErr |
||||
} |
||||
|
||||
// GetCellAt returns a reference to the cell at the given coordinates.
|
||||
// If the coordinates are out of bounds for this map, an error will be returned.
|
||||
func (l HexLayer) GetCellAt(c StorageCoordinates) (*HexCell, error) { |
||||
if int(c.Line) >= len(l) { |
||||
return nil, fmt.Errorf("line %d out of bounds (%d)", c.Line, len(l)) |
||||
} |
||||
line := l[c.Line] |
||||
if int(c.Cell) >= len(line) { |
||||
return nil, fmt.Errorf("cell %d out of bounds (%d)", c.Cell, len(line)) |
||||
} |
||||
return &(line[c.Cell]), nil |
||||
} |
||||
|
||||
// Copy creates a deep copy of this HexLayer.
|
||||
func (l HexLayer) Copy() HexLayer { |
||||
duplicate := make(HexLayer, len(l)) |
||||
for index, value := range l { |
||||
duplicate[index] = value.Copy() |
||||
} |
||||
return duplicate |
||||
} |
||||
|
||||
// HexMap contains the data for a map instance.
|
||||
type HexMap struct { |
||||
// XID is the unique id of the HexMap, used to encourage clients not to blindly interact with a different map.
|
||||
XID xid.ID `json:"xid"` |
||||
// Lines is the rough number of rows (in PointyTop orientation) or columns (in FlatTop orientation) in the map.
|
||||
// Because different lines will be staggered, it's somewhat hard to see.
|
||||
Lines uint8 `json:"lines"` |
||||
// CellsPerLine is the rough number of columns (in PointyTop orientation) or rows (in FlatTop orientation).
|
||||
// This is the number of cells joined together, flat-edge to flat-edge, in each line.
|
||||
CellsPerLine uint8 `json:"cellsPerLine"` |
||||
// Layout is the orientation and line parity used to display the map.
|
||||
Layout Layout `json:"layout"` |
||||
// Layer contains the actual map data.
|
||||
// Layer itself is a slice with Lines elements, each of which is a line;
|
||||
// each of those lines is a slice of CellsPerLine cells.
|
||||
Layer HexLayer `json:"layer"` |
||||
} |
||||
|
||||
func NewHexMap(layout Layout, lines uint8, cellsPerLine uint8) *HexMap { |
||||
layer := make(HexLayer, lines) |
||||
for index := range layer { |
||||
line := make(HexLine, cellsPerLine) |
||||
for index := range line { |
||||
line[index] = HexCell{ |
||||
Color: Color{ |
||||
R: 255, |
||||
G: 255, |
||||
B: 255, |
||||
A: 255, |
||||
}, |
||||
} |
||||
} |
||||
layer[index] = line |
||||
} |
||||
return &HexMap{ |
||||
XID: xid.New(), |
||||
Layout: layout, |
||||
Lines: lines, |
||||
CellsPerLine: cellsPerLine, |
||||
Layer: layer, |
||||
} |
||||
} |
||||
|
||||
func (m HexMap) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("id", m.XID.String()) |
||||
encoder.AddUint8("lines", m.Lines) |
||||
encoder.AddUint8("cellsPerLine", m.CellsPerLine) |
||||
displayModeErr := encoder.AddObject("layout", m.Layout) |
||||
lineCellsErr := encoder.AddArray("lineCells", m.Layer) |
||||
if displayModeErr != nil { |
||||
return displayModeErr |
||||
} else { |
||||
return lineCellsErr |
||||
} |
||||
} |
||||
|
||||
// Copy creates a deep copy of this HexMap.
|
||||
func (m HexMap) Copy() HexMap { |
||||
return HexMap{ |
||||
XID: m.XID, |
||||
Lines: m.Lines, |
||||
CellsPerLine: m.CellsPerLine, |
||||
Layout: m.Layout, |
||||
Layer: m.Layer.Copy(), |
||||
} |
||||
} |
@ -1,142 +0,0 @@ |
||||
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: ColorFromRGBA8888(x.Color), |
||||
} |
||||
} |
||||
|
||||
func (h HexCell) ToPB() *HexCellPB { |
||||
return &HexCellPB{ |
||||
Color: h.Color.ToRGBA8888(), |
||||
} |
||||
} |
||||
|
||||
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(), |
||||
} |
||||
} |
@ -1,5 +0,0 @@ |
||||
package state |
||||
|
||||
import "errors" |
||||
|
||||
var ErrOneofNotSet = errors.New("no value was given for a oneof") |
@ -1,29 +0,0 @@ |
||||
package state |
||||
|
||||
import ( |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
// Synced contains all state that is synced between the server and its clients.
|
||||
type Synced struct { |
||||
Map HexMap `json:"map"` |
||||
User UserState `json:"user"` |
||||
} |
||||
|
||||
func (s *Synced) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
mapErr := encoder.AddObject("map", s.Map) |
||||
userErr := encoder.AddObject("user", s.User) |
||||
if mapErr != nil { |
||||
return mapErr |
||||
} else { |
||||
return userErr |
||||
} |
||||
} |
||||
|
||||
// Copy creates a deep copy of this Synced instance.
|
||||
func (s *Synced) Copy() Synced { |
||||
return Synced{ |
||||
Map: s.Map.Copy(), |
||||
User: s.User.Copy(), |
||||
} |
||||
} |
@ -1,20 +0,0 @@ |
||||
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(), |
||||
} |
||||
} |
@ -1,21 +0,0 @@ |
||||
package state |
||||
|
||||
import "go.uber.org/zap/zapcore" |
||||
|
||||
// UserState contains data about clients that is synced between client and server.
|
||||
// Unlike the map, UserState is not persisted to disk, and all UserState is lost on shutdown.
|
||||
type UserState struct { |
||||
ActiveColor Color `json:"activeColor"` |
||||
} |
||||
|
||||
func (u UserState) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("activeColor", u.ActiveColor.String()) |
||||
return nil |
||||
} |
||||
|
||||
// Copy creates a deep copy of this UserState.
|
||||
func (u UserState) Copy() UserState { |
||||
return UserState{ |
||||
ActiveColor: u.ActiveColor, |
||||
} |
||||
} |
@ -1,13 +0,0 @@ |
||||
package state |
||||
|
||||
func (x *UserStatePB) ToGo() UserState { |
||||
return UserState{ |
||||
ActiveColor: ColorFromRGBA8888(x.Color), |
||||
} |
||||
} |
||||
|
||||
func (u UserState) ToPB() *UserStatePB { |
||||
return &UserStatePB{ |
||||
Color: u.ActiveColor.ToRGBA8888(), |
||||
} |
||||
} |
@ -1,74 +0,0 @@ |
||||
package ws |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/action" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
// ClientCommandType is an enum type for the client's protocol messages.
|
||||
type ClientCommandType string |
||||
|
||||
// ClientCommand s are those sent by the client.
|
||||
type ClientCommand interface { |
||||
zapcore.ObjectMarshaler |
||||
// ToClientPB converts the command to a client protocol buffer which will be sent on the wire.
|
||||
ToClientPB() *ClientCommandPB |
||||
} |
||||
|
||||
// ClientHello is the command sent by the client when it first establishes the connection.
|
||||
type ClientHello struct { |
||||
// Version is the protocol version the client is running.
|
||||
Version uint32 `json:"version"` |
||||
} |
||||
|
||||
func (c ClientHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "Hello") |
||||
encoder.AddUint32("version", c.Version) |
||||
return nil |
||||
} |
||||
|
||||
// ClientRefresh is the command sent by the client when it needs the full state re-sent.
|
||||
type ClientRefresh struct { |
||||
} |
||||
|
||||
func (c ClientRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "Refresh") |
||||
return nil |
||||
} |
||||
|
||||
type IDPairs []action.IDed |
||||
|
||||
func (a IDPairs) MarshalLogArray(encoder zapcore.ArrayEncoder) error { |
||||
var finalErr error = nil |
||||
for _, v := range a { |
||||
err := encoder.AppendObject(v) |
||||
if err != nil && finalErr == nil { |
||||
finalErr = err |
||||
} |
||||
} |
||||
return finalErr |
||||
} |
||||
|
||||
// ClientAct is a command sent in order to deliver one or more Action actions to the server.
|
||||
type ClientAct struct { |
||||
// Actions contains the actions the client wants to apply, in the order they should be applied.
|
||||
Actions IDPairs `json:"actions"` |
||||
} |
||||
|
||||
func (c ClientAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "Act") |
||||
return encoder.AddArray("actions", c.Actions) |
||||
} |
||||
|
||||
// ClientMalformed is synthesized by the reader when it has read a command that does not appear to match the
|
||||
// protocol.
|
||||
type ClientMalformed struct { |
||||
// Error is the error in parse that caused the reader to be unable to read the message.
|
||||
Error error |
||||
} |
||||
|
||||
func (c ClientMalformed) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "(malformed command)") |
||||
encoder.AddString("error", c.Error.Error()) |
||||
return nil |
||||
} |
@ -1,99 +0,0 @@ |
||||
package ws |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/action" |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
) |
||||
|
||||
func (x *ClientCommandPB) ToGo() (ClientCommand, error) { |
||||
switch msg := x.Command.(type) { |
||||
case nil: |
||||
return nil, state.ErrOneofNotSet |
||||
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() (action.IDed, error) { |
||||
act, err := x.Action.ToGo() |
||||
if err != nil { |
||||
return action.IDed{}, nil |
||||
} |
||||
return action.IDed{ |
||||
ID: x.Id, |
||||
Action: act, |
||||
}, nil |
||||
} |
||||
|
||||
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 nil, 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] = &ClientActPB_IDed{ |
||||
Id: ided.ID, |
||||
Action: ided.Action.ToClientPB(), |
||||
} |
||||
} |
||||
return &ClientCommandPB{ |
||||
Command: &ClientCommandPB_Act{ |
||||
Act: &ClientActPB{ |
||||
Actions: actions, |
||||
}, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func (*ClientMalformed) ToClientPB() *ClientCommandPB { |
||||
return nil |
||||
} |
||||
|
||||
func (*SocketClosed) ToClientPB() *ClientCommandPB { |
||||
return nil |
||||
} |
@ -1,276 +0,0 @@ |
||||
package ws |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/action" |
||||
"git.reya.zone/reya/hexmap/server/room" |
||||
"github.com/gorilla/websocket" |
||||
"go.uber.org/zap" |
||||
"net/http" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
// ReadTimeLimit is the maximum time the server is willing to wait after receiving a message before receiving another one.
|
||||
ReadTimeLimit = 60 * time.Second |
||||
// WriteTimeLimit is the maximum time the server is willing to wait to send a message.
|
||||
WriteTimeLimit = 10 * time.Second |
||||
// ControlTimeLimit is the maximum time the server is willing to wait to send a control message like Ping or Close.
|
||||
ControlTimeLimit = (WriteTimeLimit * 5) / 10 |
||||
// PingDelay is the time between pings.
|
||||
// It must be less than ReadTimeLimit to account for latency and delays on either side.
|
||||
PingDelay = (ReadTimeLimit * 7) / 10 |
||||
) |
||||
|
||||
type HTTPHandler struct { |
||||
Upgrader websocket.Upgrader |
||||
Logger *zap.Logger |
||||
Room *room.Client |
||||
} |
||||
|
||||
func destroyBadProtocolSocket(c *websocket.Conn, logger *zap.Logger) { |
||||
err := c.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseProtocolError, "Invalid subprotocols"), time.Now().Add(ControlTimeLimit)) |
||||
if err != nil { |
||||
logger.Error("Failed to write close message") |
||||
} |
||||
err = c.SetReadDeadline(time.Now().Add(ControlTimeLimit)) |
||||
if err != nil { |
||||
logger.Error("Failed to set read deadline") |
||||
} |
||||
for { |
||||
_, _, err := c.ReadMessage() |
||||
if err != nil { |
||||
if !websocket.IsCloseError(err, websocket.CloseProtocolError) { |
||||
logger.Error("Websocket connection shut down ignominiously", zap.Error(err)) |
||||
} |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (h *HTTPHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { |
||||
c, err := h.Upgrader.Upgrade(responseWriter, request, http.Header{}) |
||||
if err != nil { |
||||
h.Logger.Error("Failed to upgrade ws connection", zap.Error(err)) |
||||
return |
||||
} |
||||
if c.Subprotocol() == "" { |
||||
h.Logger.Error("No matching subprotocol", zap.String("clientProtocols", request.Header.Get("Sec-Websocket-Protocol"))) |
||||
go destroyBadProtocolSocket(c, h.Logger) |
||||
return |
||||
} |
||||
result := NewConnection(c, h.Logger.Named("Connection")) |
||||
exchange(result, h.Logger.Named("Link"), func(o room.NewClientOptions) *room.Client { |
||||
return h.Room.NewClient(o) |
||||
}) |
||||
} |
||||
|
||||
func exchange(c *Connection, l *zap.Logger, clientMaker func(options room.NewClientOptions) *room.Client) { |
||||
wsr := c.ReadChannel() |
||||
wsw := c.WriteChannel() |
||||
|
||||
l.Info("Connection established") |
||||
closeWith := &SocketClosed{ |
||||
Code: websocket.CloseAbnormalClosure, |
||||
Text: "I don't know what happened. But goodbye!", |
||||
} |
||||
defer func() { |
||||
l.Info("Shutting down") |
||||
// Wait for the websocket connection to shut down.
|
||||
wsw <- closeWith |
||||
if wsr != nil { |
||||
for { |
||||
msg := <-wsr |
||||
switch msg.(type) { |
||||
case *SocketClosed: |
||||
return |
||||
} |
||||
} |
||||
} |
||||
}() |
||||
// State 1: Waiting for a hello.
|
||||
// Anything else is death.
|
||||
cmd := <-wsr |
||||
switch typedCmd := cmd.(type) { |
||||
case *ClientHello: |
||||
if typedCmd.Version != ProtocolVersion { |
||||
l.Warn("Bad Hello version") |
||||
// Disgusting. I can't even look at you.
|
||||
closeWith = &SocketClosed{ |
||||
Code: websocket.CloseProtocolError, |
||||
Text: "Wrong protocol version", |
||||
} |
||||
return |
||||
} |
||||
l.Info("Got Hello") |
||||
default: |
||||
l.Warn("Got NON-hello") |
||||
closeWith = &SocketClosed{ |
||||
Code: websocket.CloseProtocolError, |
||||
Text: "You don't even say hello?", |
||||
} |
||||
return |
||||
} |
||||
l.Info("Waiting for room.") |
||||
// State 2: Waiting for the room to notice us.
|
||||
rm := clientMaker(room.NewClientOptions{ |
||||
AcceptBroadcasts: true, |
||||
RequestStartingState: true, |
||||
}) |
||||
rmr := rm.IncomingChannel() |
||||
rmw := rm.OutgoingChannel() |
||||
var leaveWith room.ClientMessage = nil |
||||
defer func() { |
||||
l.Info("Leaving room") |
||||
if leaveWith == nil { |
||||
leaveWith = rm.Leave() |
||||
} |
||||
rmw <- leaveWith |
||||
if _, ok := leaveWith.(*room.ShutdownResponse); ok { |
||||
// The room was already shutting down.
|
||||
return |
||||
} |
||||
for { |
||||
msg := <-rmr |
||||
switch msg.(type) { |
||||
case *room.LeaveResponse: |
||||
return |
||||
case *room.ShutdownRequest: |
||||
rmw <- rm.AcknowledgeShutdown() |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
l.Info("Waiting for JoinResponse") |
||||
msg := <-rmr |
||||
switch typedMsg := msg.(type) { |
||||
case *room.JoinResponse: |
||||
l.Info("Got JoinResponse") |
||||
wsw <- &ServerHello{ |
||||
Version: ProtocolVersion, |
||||
State: typedMsg.CurrentState(), |
||||
} |
||||
case *room.ShutdownRequest: |
||||
l.Info("Got ShutdownRequest") |
||||
// Room was shutting down when we joined, oops!
|
||||
closeWith = &SocketClosed{ |
||||
Code: websocket.CloseGoingAway, |
||||
Text: "Shutting down right as you joined. Sorry!", |
||||
} |
||||
return |
||||
default: |
||||
l.Info("Got non-JoinResponse/ShutdownRequest") |
||||
// Uh. That's concerning. We don't have anything to send our client.
|
||||
// Let's just give up.
|
||||
return |
||||
} |
||||
l.Info("Waiting for messages") |
||||
for { |
||||
select { |
||||
case cmd := <-wsr: |
||||
switch typedCmd := cmd.(type) { |
||||
case *ClientHello: |
||||
l.Info("Got unnecessary ClientHello") |
||||
// Huh???
|
||||
closeWith = &SocketClosed{ |
||||
Code: websocket.CloseProtocolError, |
||||
Text: "Enough hellos. Goodbye.", |
||||
} |
||||
return |
||||
case *ClientRefresh: |
||||
l.Info("Got ClientRefresh") |
||||
rmw <- rm.Refresh() |
||||
case *ClientAct: |
||||
l.Info("Got ClientAct") |
||||
for _, act := range typedCmd.Actions { |
||||
rmw <- rm.Apply(act) |
||||
} |
||||
case *SocketClosed: |
||||
l.Info("Got SocketClosed", zap.Object("close", typedCmd)) |
||||
closeWith = typedCmd |
||||
return |
||||
case *ClientMalformed: |
||||
l.Warn("Got ClientMalformed") |
||||
return |
||||
} |
||||
case msg := <-rmr: |
||||
switch typedMsg := msg.(type) { |
||||
case *room.JoinResponse: |
||||
// Huh????
|
||||
l.Info("Got unnecesary JoinResponse") |
||||
return |
||||
case *room.RefreshResponse: |
||||
l.Info("Got RefreshResponse") |
||||
wsw <- &ServerRefresh{ |
||||
State: typedMsg.CurrentState(), |
||||
} |
||||
case *room.ApplyResponse: |
||||
l.Info("Got ApplyResponse") |
||||
if typedMsg.Success() { |
||||
wsw <- &ServerOK{ |
||||
IDs: []uint32{typedMsg.ActionID()}, |
||||
} |
||||
} else { |
||||
wsw <- &ServerFailed{ |
||||
IDs: []uint32{typedMsg.ActionID()}, |
||||
Error: typedMsg.Failure().Error(), |
||||
} |
||||
} |
||||
case *room.ActionBroadcast: |
||||
l.Info("Got ActionBroadcast") |
||||
wsw <- &ServerAct{ |
||||
Actions: action.ServerSlice{typedMsg.Action()}, |
||||
} |
||||
case *room.LeaveResponse: |
||||
l.Info("Got odd LeaveResponse") |
||||
// Oh. u_u I wasn't- okay.
|
||||
return |
||||
case *room.ShutdownRequest: |
||||
// Oh. Oh! Okay! Sorry!
|
||||
l.Info("Got ShutdownRequest") |
||||
leaveWith = rm.AcknowledgeShutdown() |
||||
return |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// A Connection corresponds to a pair of actors.
|
||||
type Connection struct { |
||||
r reader |
||||
w writer |
||||
} |
||||
|
||||
func NewConnection(conn *websocket.Conn, logger *zap.Logger) *Connection { |
||||
readChan := make(chan time.Time) |
||||
out := &Connection{ |
||||
r: reader{ |
||||
conn: conn, |
||||
channel: make(chan ClientCommand), |
||||
readNotifications: readChan, |
||||
logger: logger.Named("reader"), |
||||
}, |
||||
w: writer{ |
||||
conn: conn, |
||||
channel: make(chan ServerCommand), |
||||
readNotifications: readChan, |
||||
timer: nil, |
||||
nextPingAt: time.Time{}, |
||||
logger: logger.Named("writer"), |
||||
}, |
||||
} |
||||
go out.r.act() |
||||
go out.w.act() |
||||
return out |
||||
} |
||||
|
||||
// ReadChannel returns the channel that can be used to read client messages from the connection.
|
||||
// After receiving SocketClosed, the reader will close its channel.
|
||||
func (c *Connection) ReadChannel() <-chan ClientCommand { |
||||
return c.r.channel |
||||
} |
||||
|
||||
// WriteChannel returns the channel that can be used to send server messages on the connection.
|
||||
// After sending SocketClosed, the writer will close its channel; do not send any further messages on the channel.
|
||||
func (c *Connection) WriteChannel() chan<- ServerCommand { |
||||
return c.w.channel |
||||
} |
@ -1,123 +0,0 @@ |
||||
package ws |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/gorilla/websocket" |
||||
"go.uber.org/zap" |
||||
"google.golang.org/protobuf/proto" |
||||
"time" |
||||
) |
||||
|
||||
type reader struct { |
||||
// conn is the connection to the client read from by the reader.
|
||||
conn *websocket.Conn |
||||
// 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() |
||||
// The reader should not automatically respond with close messages.
|
||||
// It should simply let the close message be returned as an error.
|
||||
r.conn.SetCloseHandler(func(code int, text string) error { |
||||
return nil |
||||
}) |
||||
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 |
||||
} |
@ -1,93 +0,0 @@ |
||||
package ws |
||||
|
||||
import ( |
||||
"git.reya.zone/reya/hexmap/server/action" |
||||
"git.reya.zone/reya/hexmap/server/state" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
// ServerMessageType is an enum type for the server's messages.
|
||||
type ServerMessageType string |
||||
|
||||
// ServerCommand s are sent by the server to the client.
|
||||
type ServerCommand interface { |
||||
zapcore.ObjectMarshaler |
||||
// ToServerPB converts the command to a server protocol buffer which will be sent on the wire.
|
||||
ToServerPB() *ServerCommandPB |
||||
} |
||||
|
||||
// ServerHello is the command sent to establish the current state of the server when a new client connects.
|
||||
type ServerHello struct { |
||||
// Version is the protocol version the server is running.
|
||||
Version uint32 `json:"version"` |
||||
// State is the complete state of the server as of when the client joined.
|
||||
State *state.Synced `json:"state"` |
||||
} |
||||
|
||||
func (s ServerHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "Hello") |
||||
encoder.AddUint32("version", s.Version) |
||||
return encoder.AddObject("state", s.State) |
||||
} |
||||
|
||||
// ServerRefresh is the command sent to reestablish the current state of the server in response to ClientRefresh.
|
||||
type ServerRefresh struct { |
||||
// State is the complete state of the server as of when the corresponding ClientRefresh was processed.
|
||||
State *state.Synced `json:"state"` |
||||
} |
||||
|
||||
func (s ServerRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "Refresh") |
||||
return encoder.AddObject("state", s.State) |
||||
} |
||||
|
||||
type IDSlice []uint32 |
||||
|
||||
func (i IDSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error { |
||||
for _, v := range i { |
||||
encoder.AppendUint32(v) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ServerOK is the command sent when one or more client actions have been accepted and applied.
|
||||
type ServerOK struct { |
||||
// IDs contains the IDs of the actions which were accepted and applied, in the order they were accepted and applied.
|
||||
// This is the same as the order they were received, though other actions may have been between these that were
|
||||
// rejected.
|
||||
IDs IDSlice `json:"ids"` |
||||
} |
||||
|
||||
func (s ServerOK) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "OK") |
||||
return encoder.AddArray("ids", s.IDs) |
||||
} |
||||
|
||||
// ServerFailed is the command sent when one or more client actions have been rejected.
|
||||
type ServerFailed struct { |
||||
// IDs contains the IDs of the actions which were rejected, in the order they were rejected.
|
||||
// This is the same as the order they were received, though other actions may have been between these that were
|
||||
// accepted and applied.
|
||||
IDs IDSlice `json:"ids"` |
||||
// Error contains the error text sent from the server about why these actions failed.
|
||||
Error string `json:"error"` |
||||
} |
||||
|
||||
func (s ServerFailed) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "Failed") |
||||
err := encoder.AddArray("ids", s.IDs) |
||||
encoder.AddString("error", s.Error) |
||||
return err |
||||
} |
||||
|
||||
// ServerAct is the command sent when one or more client actions from other clients have been accepted and applied.
|
||||
// The client's own actions will never be included in this command.
|
||||
type ServerAct struct { |
||||
// Actions contains the actions that are now being applied.
|
||||
Actions action.ServerSlice `json:"actions"` |
||||
} |
||||
|
||||
func (s ServerAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "Act") |
||||
return encoder.AddArray("actions", s.Actions) |
||||
} |
@ -1,136 +0,0 @@ |
||||
package ws |
||||
|
||||
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 nil, 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 nil, 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 nil, 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 |
||||
} |
@ -1,37 +0,0 @@ |
||||
package ws |
||||
|
||||
import ( |
||||
"github.com/gorilla/websocket" |
||||
"go.uber.org/zap/zapcore" |
||||
) |
||||
|
||||
const ( |
||||
PingData string = "are you still there?" |
||||
ProtocolVersion uint32 = 1 |
||||
) |
||||
|
||||
var StandardClientCloseTypes = []int{websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure} |
||||
|
||||
// SocketClosed is synthesized when a client closes the WebSocket connection, or sent to the write process to write a
|
||||
// WebSocket close message.
|
||||
// Sending a SocketClosed on a channel causes that channel to be closed right after.
|
||||
type SocketClosed struct { |
||||
// Code is the status code given (or which should be given) in the close message.
|
||||
Code int `json:"code"` |
||||
// Text is the reason text given (or which should be given) in the close message. Max 123 characters.
|
||||
Text string `json:"text"` |
||||
// Error may be an error that resulted in the closure of the socket. It will be set only if the connection was cut
|
||||
// by a condition other than receiving a close message; if it is set, Code and Text will both be zeroed.
|
||||
// Will not be written by the writer; only useful when it's returned from the reader.
|
||||
Error error `json:"error"` |
||||
} |
||||
|
||||
func (c SocketClosed) MarshalLogObject(encoder zapcore.ObjectEncoder) error { |
||||
encoder.AddString("type", "GOODBYE") |
||||
encoder.AddInt("code", c.Code) |
||||
encoder.AddString("text", c.Text) |
||||
if c.Error != nil { |
||||
encoder.AddString("error", c.Error.Error()) |
||||
} |
||||
return nil |
||||
} |
@ -1,197 +0,0 @@ |
||||
package ws |
||||
|
||||
import ( |
||||
"github.com/gorilla/websocket" |
||||
"go.uber.org/zap" |
||||
"google.golang.org/protobuf/proto" |
||||
"io" |
||||
"time" |
||||
) |
||||
|
||||
type writer struct { |
||||
// conn is the ws connection that this writer is responsible for writing on.
|
||||
conn *websocket.Conn |
||||
// channel is the channel used to receive server messages to be sent to the client.
|
||||
// When it receives a SocketClosed, the process on the sending end promises not to send any further messages, as the writer will close it right after.
|
||||
channel chan ServerCommand |
||||
// readNotifications is the channel used to receive pings when the reader receives a message, so that a ping will be sent out before the reader is ready to time out.
|
||||
// When it is closed, the reader has shut down.
|
||||
readNotifications <-chan time.Time |
||||
// timer is the timer used to send pings when the reader is close to timing out, to make sure the other end of the connection is still listening.
|
||||
timer *time.Timer |
||||
// nextPingAt is the time after which the next ping will be sent if the timer is ticking. It will be .IsZero() if the timer is not ticking.
|
||||
nextPingAt time.Time |
||||
// logger is the logger used to record the state of the writer, primarily in Debug level.
|
||||
logger *zap.Logger |
||||
} |
||||
|
||||
// IsTimerTicking returns true if the writer's timer is running.
|
||||
func (w *writer) isTimerTicking() bool { |
||||
return !w.nextPingAt.IsZero() |
||||
} |
||||
|
||||
// sendNextPingAt starts the timer if it's not running, and
|
||||
func (w *writer) sendNextPingAt(nextPing time.Time) { |
||||
if w.nextPingAt.IsZero() { |
||||
// Timer is not running, so set the next ping time and start it.
|
||||
w.nextPingAt = nextPing |
||||
w.timer.Reset(time.Until(nextPing)) |
||||
} else if w.nextPingAt.Before(nextPing) { |
||||
// Timer's already running, so leave it be, but update the next ping time.
|
||||
w.nextPingAt = nextPing |
||||
} else { |
||||
// The timer is already set to a time after the incoming time.
|
||||
// It's extremely unlikely for this empty branch to ever be reached.
|
||||
} |
||||
} |
||||
|
||||
// act is the function responsible for actually doing the writing.
|
||||
func (w *writer) act() { |
||||
defer w.gracefulShutdown() |
||||
w.logger.Debug("Starting up") |
||||
w.timer = time.NewTimer(PingDelay) |
||||
w.nextPingAt = time.Now().Add(PingDelay) |
||||
for { |
||||
select { |
||||
case readAt, open := <-w.readNotifications: |
||||
if open { |
||||
nextPingAt := readAt.Add(PingDelay) |
||||
w.logger.Debug("Received reader read, extending ping timer", zap.Time("nextPingAt", nextPingAt)) |
||||
w.sendNextPingAt(nextPingAt) |
||||
} else { |
||||
w.logger.Debug("Received reader close, shutting down") |
||||
w.readNotifications = nil |
||||
// bye bye, we'll graceful shutdown because we deferred it
|
||||
return |
||||
} |
||||
case raw := <-w.channel: |
||||
switch msg := raw.(type) { |
||||
case *SocketClosed: |
||||
w.logger.Debug("Received close message, forwarding and shutting down", zap.Object("msg", msg)) |
||||
w.sendClose(msg) |
||||
// bye bye, we'll graceful shutdown because we deferred it
|
||||
return |
||||
default: |
||||
w.logger.Debug("Received message, forwarding", zap.Object("msg", msg)) |
||||
w.send(msg) |
||||
} |
||||
case <-w.timer.C: |
||||
now := time.Now() |
||||
if now.After(w.nextPingAt) { |
||||
// We successfully passed the time when a ping should be sent! Let's send it!
|
||||
w.sendPing() |
||||
// The timer doesn't need to be reactivated right now, so just zero out the next ping time.
|
||||
w.nextPingAt = time.Time{} |
||||
} else { |
||||
// It's not time to send the ping yet - we got more reads in the meantime. Restart the timer with the new time-until-next-ping.
|
||||
w.timer.Reset(w.nextPingAt.Sub(now)) |
||||
} |
||||
} |
||||
w.logger.Debug("Awakening handled, resuming listening") |
||||
} |
||||
} |
||||
|
||||
// send actually transmits a ServerCommand to the client according to the protocol.
|
||||
func (w *writer) send(msg ServerCommand) { |
||||
w.logger.Debug("Marshaling command as protobuf", zap.Object("msg", msg)) |
||||
marshaled, err := proto.Marshal(msg.ToServerPB()) |
||||
if err != nil { |
||||
w.logger.Error("Error while marshaling to protobuf", zap.Error(err)) |
||||
return |
||||
} |
||||
writeDeadline := time.Now().Add(WriteTimeLimit) |
||||
w.logger.Debug("Setting deadline to write command", zap.Time("writeDeadline", writeDeadline)) |
||||
err = w.conn.SetWriteDeadline(writeDeadline) |
||||
if err != nil { |
||||
w.logger.Error("Error while setting write deadline", zap.Time("writeDeadline", writeDeadline), zap.Object("msg", msg), zap.Error(err)) |
||||
} |
||||
w.logger.Debug("Opening message writer to send command", zap.Object("msg", msg)) |
||||
writer, err := w.conn.NextWriter(websocket.BinaryMessage) |
||||
if err != nil { |
||||
w.logger.Error("Error while getting writer from connection", zap.Error(err)) |
||||
return |
||||
} |
||||
defer func(writer io.WriteCloser) { |
||||
w.logger.Debug("Closing message writer to send command") |
||||
err := writer.Close() |
||||
if err != nil { |
||||
w.logger.Error("Error while closing writer to send command", zap.Error(err)) |
||||
} |
||||
w.logger.Debug("Command sent") |
||||
}(writer) |
||||
_, err = writer.Write(marshaled) |
||||
if err != nil { |
||||
w.logger.Error("Error while writing marshaled protobuf to connection", zap.Error(err)) |
||||
return |
||||
} |
||||
// Deferred close happens now
|
||||
} |
||||
|
||||
// sendClose sends a close message on the ws connection, but does not actually close the connection.
|
||||
// It does, however, close the incoming message channel to the writer.
|
||||
func (w *writer) sendClose(msg *SocketClosed) { |
||||
w.logger.Debug("Shutting down the writer channel") |
||||
close(w.channel) |
||||
w.channel = nil |
||||
w.logger.Debug("Writing close message") |
||||
err := w.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(msg.Code, msg.Text), time.Now().Add(ControlTimeLimit)) |
||||
if err != nil { |
||||
w.logger.Warn("Error while sending close", zap.Error(err)) |
||||
} |
||||
} |
||||
|
||||
// sendPing sends a ping message on the ws connection. The content is arbitrary.
|
||||
func (w *writer) sendPing() { |
||||
w.logger.Debug("Sending ping") |
||||
err := w.conn.WriteControl(websocket.PingMessage, []byte(PingData), time.Now().Add(ControlTimeLimit)) |
||||
if err != nil { |
||||
w.logger.Error("Error while sending ping", zap.Error(err)) |
||||
} |
||||
} |
||||
|
||||
// gracefulShutdown causes the writer to wait for the close handshake to finish and then shut down.
|
||||
// It waits for the reader's readNotifications to close, indicating that it has also shut down, and for the channel to
|
||||
// receive a SocketClosed message indicating that the main process has shut down.
|
||||
// During this time, the writer ignores all other messages from the channel and sends no pings.
|
||||
func (w *writer) gracefulShutdown() { |
||||
defer w.finalShutdown() |
||||
// If the ping timer is still running, stop it and then close it.
|
||||
if w.isTimerTicking() && !w.timer.Stop() { |
||||
<-w.timer.C |
||||
} |
||||
w.timer = nil |
||||
w.nextPingAt = time.Time{} |
||||
w.logger.Debug("Waiting for all channels to shut down") |
||||
for { |
||||
if w.channel == nil && w.readNotifications == nil { |
||||
w.logger.Debug("All channels closed, beginning final shutdown") |
||||
// all done, we outta here, let the defer pick up the final shutdown
|
||||
return |
||||
} |
||||
select { |
||||
case _, open := <-w.readNotifications: |
||||
if !open { |
||||
w.logger.Debug("Received reader close while shutting down") |
||||
w.readNotifications = nil |
||||
} |
||||
case raw := <-w.channel: |
||||
switch msg := raw.(type) { |
||||
case *SocketClosed: |
||||
w.logger.Debug("Received close message from channel while shutting down, forwarding", zap.Object("msg", msg)) |
||||
w.sendClose(msg) |
||||
default: |
||||
w.logger.Debug("Ignoring non-close message while shutting down", zap.Object("msg", msg)) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// finalShutdown closes the socket and finishes cleanup.
|
||||
func (w *writer) finalShutdown() { |
||||
w.logger.Debug("Closing WebSocket connection") |
||||
err := w.conn.Close() |
||||
if err != nil { |
||||
w.logger.Error("Received an error while closing", zap.Error(err)) |
||||
} |
||||
w.logger.Debug("Shut down") |
||||
} |
@ -0,0 +1,3 @@ |
||||
import {EntryPoint} from "./ui/EntryPoint"; |
||||
|
||||
EntryPoint(); |
@ -1,15 +1,15 @@ |
||||
import {Dispatch, useMemo, useReducer} from "react"; |
||||
import {appStateReducer} from "./reducers/AppStateReducer"; |
||||
import {AppState} from "./state/AppState"; |
||||
import {ServerConnectionState} from "./state/NetworkState"; |
||||
import {sizeFromLinesAndCells} from "./state/Coordinates"; |
||||
import {AppAction} from "./actions/AppAction"; |
||||
import {USER_ACTIVE_COLOR} from "./actions/UserAction"; |
||||
import HexColorPicker from "./ui/HexColorPicker"; |
||||
import HexMapRenderer from "./ui/HexMapRenderer"; |
||||
import {DispatchContext} from "./ui/context/DispatchContext"; |
||||
import {appStateReducer} from "../reducers/AppStateReducer"; |
||||
import {AppState} from "../state/AppState"; |
||||
import {ServerConnectionState} from "../state/NetworkState"; |
||||
import {sizeFromLinesAndCells} from "../state/Coordinates"; |
||||
import {AppAction} from "../actions/AppAction"; |
||||
import {USER_ACTIVE_COLOR} from "../actions/UserAction"; |
||||
import HexColorPicker from "./HexColorPicker"; |
||||
import HexMapRenderer from "./HexMapRenderer"; |
||||
import {DispatchContext} from "./context/DispatchContext"; |
||||
import "./App.css"; |
||||
import {WebsocketReactAdapter} from "./websocket/WebsocketReactAdapter"; |
||||
import {WebsocketReactAdapter} from "./WebsocketReactAdapter"; |
||||
|
||||
function App() { |
||||
const defaultState: AppState = { |
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
import ReactDOM from 'react-dom'; |
||||
import './ui/EntryPoint.css'; |
||||
import App from './App'; |
||||
import reportWebVitals from './reportWebVitals'; |
||||
|
||||
export function EntryPoint() { |
||||
ReactDOM.render( |
||||
<React.StrictMode> |
||||
<App /> |
||||
</React.StrictMode>, |
||||
document.getElementById('root') |
||||
); |
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals(); |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue