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"; |
syntax = "proto3"; |
||||||
|
|
||||||
option go_package = "git.reya.zone/reya/hexmap/server/state"; |
|
||||||
|
|
||||||
message StorageCoordinatesPB { |
message StorageCoordinatesPB { |
||||||
uint32 line = 1; |
uint32 line = 1; |
||||||
uint32 cell = 2; |
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"; |
syntax = "proto3"; |
||||||
|
|
||||||
option go_package = "git.reya.zone/reya/hexmap/server/state"; |
|
||||||
|
|
||||||
message UserStatePB { |
message UserStatePB { |
||||||
fixed32 color = 1; |
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 {Dispatch, useMemo, useReducer} from "react"; |
||||||
import {appStateReducer} from "./reducers/AppStateReducer"; |
import {appStateReducer} from "../reducers/AppStateReducer"; |
||||||
import {AppState} from "./state/AppState"; |
import {AppState} from "../state/AppState"; |
||||||
import {ServerConnectionState} from "./state/NetworkState"; |
import {ServerConnectionState} from "../state/NetworkState"; |
||||||
import {sizeFromLinesAndCells} from "./state/Coordinates"; |
import {sizeFromLinesAndCells} from "../state/Coordinates"; |
||||||
import {AppAction} from "./actions/AppAction"; |
import {AppAction} from "../actions/AppAction"; |
||||||
import {USER_ACTIVE_COLOR} from "./actions/UserAction"; |
import {USER_ACTIVE_COLOR} from "../actions/UserAction"; |
||||||
import HexColorPicker from "./ui/HexColorPicker"; |
import HexColorPicker from "./HexColorPicker"; |
||||||
import HexMapRenderer from "./ui/HexMapRenderer"; |
import HexMapRenderer from "./HexMapRenderer"; |
||||||
import {DispatchContext} from "./ui/context/DispatchContext"; |
import {DispatchContext} from "./context/DispatchContext"; |
||||||
import "./App.css"; |
import "./App.css"; |
||||||
import {WebsocketReactAdapter} from "./websocket/WebsocketReactAdapter"; |
import {WebsocketReactAdapter} from "./WebsocketReactAdapter"; |
||||||
|
|
||||||
function App() { |
function App() { |
||||||
const defaultState: AppState = { |
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