Start moving to Typescript server

main
Mari 3 years ago
parent 5e7aee5e76
commit f4ed4a261a
  1. 25
      .gitignore
  2. 1
      .idea/hexmap.iml
  3. 2
      .idea/protoeditor.xml
  4. 0
      README.md
  5. 41
      build/build.go
  6. 26
      client/.gitignore
  7. 109
      client/magefile.go
  8. 17
      client/src/index.tsx
  9. 12
      go.mod
  10. 74
      go.sum
  11. 16
      mage.sh
  12. 32
      magefile.go
  13. 859
      package-lock.json
  14. 4
      package.json
  15. 2
      proto/action.proto
  16. 2
      proto/client.proto
  17. 2
      proto/coords.proto
  18. 56
      proto/magefile.go
  19. 2
      proto/map.proto
  20. 2
      proto/server.proto
  21. 2
      proto/state.proto
  22. 2
      proto/user.proto
  23. 0
      public/favicon.ico
  24. 0
      public/index.html
  25. 0
      public/logo192.png
  26. 0
      public/logo512.png
  27. 0
      public/manifest.json
  28. 0
      public/robots.txt
  29. 2
      server/.gitignore
  30. 125
      server/action/action.go
  31. 78
      server/action/action.pbconv.go
  32. 240
      server/action/action.pbconv_test.go
  33. 190
      server/action/action_test.go
  34. 193
      server/host/HttpServer.go
  35. 68
      server/magefile.go
  36. 1
      server/persistence/persistence.go
  37. 273
      server/room/actor.go
  38. 173
      server/room/client.go
  39. 124
      server/room/clientmessage.go
  40. 160
      server/room/message.go
  41. 54
      server/room/room.go
  42. 71
      server/state/color.go
  43. 31
      server/state/color.pbconv.go
  44. 19
      server/state/coords.go
  45. 25
      server/state/coords.pbconv.go
  46. 206
      server/state/map.go
  47. 142
      server/state/map.pbconv.go
  48. 5
      server/state/protobuf.go
  49. 29
      server/state/state.go
  50. 20
      server/state/state.pbconv.go
  51. 21
      server/state/user.go
  52. 13
      server/state/user.pbconv.go
  53. 74
      server/ws/client.go
  54. 99
      server/ws/client.pbconv.go
  55. 276
      server/ws/connection.go
  56. 123
      server/ws/reader.go
  57. 93
      server/ws/server.go
  58. 136
      server/ws/server.pbconv.go
  59. 37
      server/ws/shared.go
  60. 197
      server/ws/writer.go
  61. 0
      src/actions/AppAction.ts
  62. 0
      src/actions/BaseAction.ts
  63. 0
      src/actions/CellAction.ts
  64. 0
      src/actions/ClientAction.ts
  65. 0
      src/actions/MapAction.ts
  66. 0
      src/actions/NetworkAction.ts
  67. 0
      src/actions/ServerAction.ts
  68. 0
      src/actions/TileAction.ts
  69. 0
      src/actions/UserAction.ts
  70. 3
      src/index.tsx
  71. 0
      src/pbconv/ClientToPb.ts
  72. 0
      src/pbconv/MapFromPb.ts
  73. 0
      src/pbconv/MapToPb.ts
  74. 0
      src/pbconv/ServerFromPb.ts
  75. 0
      src/pbconv/SyncableActionFromPb.ts
  76. 0
      src/pbconv/SyncableActionToPb.ts
  77. 0
      src/react-app-env.d.ts
  78. 0
      src/reducers/AppStateReducer.ts
  79. 0
      src/reducers/ClientReducer.ts
  80. 0
      src/reducers/HexMapReducer.ts
  81. 0
      src/reducers/NetworkReducer.ts
  82. 0
      src/reducers/ServerReducer.ts
  83. 0
      src/reducers/SyncedStateReducer.ts
  84. 0
      src/reducers/TileReducer.ts
  85. 0
      src/reducers/UserReducer.ts
  86. 0
      src/setupTests.ts
  87. 0
      src/state/AppState.ts
  88. 0
      src/state/Coordinates.ts
  89. 0
      src/state/HexMap.ts
  90. 0
      src/state/NetworkState.ts
  91. 0
      src/state/SyncedState.ts
  92. 0
      src/state/UserState.ts
  93. 0
      src/ui/App.css
  94. 0
      src/ui/App.test.tsx
  95. 20
      src/ui/App.tsx
  96. 6
      src/ui/DOMWebsocketTranslator.ts
  97. 0
      src/ui/EntryPoint.css
  98. 18
      src/ui/EntryPoint.tsx
  99. 0
      src/ui/HexColorPicker.tsx
  100. 0
      src/ui/HexMapRenderer.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

25
.gitignore vendored

@ -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*

@ -13,6 +13,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/mage" />
<excludeFolder url="file://$MODULE_DIR$/buildtools" />
<excludeFolder url="file://$MODULE_DIR$/client/coverage" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

@ -5,7 +5,7 @@
<option name="importPathEntries">
<list>
<ImportPathEntry>
<option name="location" value="jar://$APPLICATION_PLUGINS_DIR$/protobuf-editor.jar!/include" />
<option name="location" value="jar://$APPLICATION_PLUGINS_DIR$/protoeditor/lib/protoeditor.jar!/protobuf" />
</ImportPathEntry>
<ImportPathEntry>
<option name="location" value="file://$PROJECT_DIR$/proto" />

@ -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()
}

26
client/.gitignore vendored

@ -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

@ -13,6 +13,7 @@
"@types/react-dom": "^17.0.8",
"base64-arraybuffer": "^0.2.0",
"protobufjs": "^6.11.2",
"protoc-tools": "^3.11.3",
"react": "^17.0.2",
"react-color": "^2.19.3",
"react-dom": "^17.0.2",
@ -22,8 +23,9 @@
"web-vitals": "^1.1.2"
},
"scripts": {
"protoc": "mkdir -p src/proto && protoc -I=proto --plugin=node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=src/proto --ts_proto_opt=env=both --ts_proto_opt=esModuleInterop=true proto/*.proto",
"start": "react-scripts start",
"build": "react-scripts build",
"build": "npm run protoc && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},

@ -2,8 +2,6 @@ syntax = "proto3";
import "coords.proto";
option go_package = "git.reya.zone/reya/hexmap/server/action";
message CellSetColorPB {
fixed32 color = 1;
StorageCoordinatesPB at = 2;

@ -2,8 +2,6 @@ syntax = "proto3";
import "action.proto";
option go_package = "git.reya.zone/reya/hexmap/server/ws";
message ClientHelloPB {
uint32 version = 1;
}

@ -1,7 +1,5 @@
syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
message StorageCoordinatesPB {
uint32 line = 1;
uint32 cell = 2;

@ -1,56 +0,0 @@
package proto
import (
"context"
"github.com/magefile/mage/mg"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
)
func BaseProtocFlags() ([]string, error) {
return []string{"-I=proto"}, nil
}
type ProtocFlagsFunc func() ([]string, error)
func Sources() ([]string, error) {
result := []string(nil)
err := filepath.WalkDir("proto", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && strings.HasSuffix(path, ".proto") {
result = append(result, path)
}
return nil
})
return result, err
}
func Compile(ctx context.Context, withPlugins []ProtocFlagsFunc) error {
flags, err := BaseProtocFlags()
if err != nil {
return err
}
for _, flagsFunc := range withPlugins {
pluginFlags, err := flagsFunc()
if err != nil {
return err
}
flags = append(flags, pluginFlags...)
}
protoFiles, err := Sources()
if err != nil {
return err
}
args := append(flags, protoFiles...)
cmd := exec.CommandContext(ctx, "protoc", args...)
cmd.Stderr = os.Stderr
if mg.Verbose() {
cmd.Stdout = os.Stdout
}
return cmd.Run()
}

@ -1,7 +1,5 @@
syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
message HexCellPB {
fixed32 color = 1;
}

@ -3,8 +3,6 @@ syntax = "proto3";
import "action.proto";
import "state.proto";
option go_package = "git.reya.zone/reya/hexmap/server/ws";
message ServerHelloPB {
uint32 version = 1;
SyncableStatePB state = 2;

@ -1,7 +1,5 @@
syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
import "map.proto";
import "user.proto";

@ -1,7 +1,5 @@
syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
message UserStatePB {
fixed32 color = 1;
}

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

2
server/.gitignore vendored

@ -1,2 +0,0 @@
*.pb.go
proto/

@ -1,125 +0,0 @@
package action
import (
"errors"
"git.reya.zone/reya/hexmap/server/state"
"go.uber.org/zap/zapcore"
)
var (
// ErrNoOp is returned when an action has no effect.
ErrNoOp = errors.New("action's effects were already applied, or it's an empty action")
// ErrNoTransparentColors is returned when a user tries to set their active color or a cell color to transparent.
// Transparent here is defined as having an alpha component of less than 15 (0xF).
ErrNoTransparentColors = errors.New("transparent colors not allowed")
)
// Action is the interface for actions that can be shared between clients, or between the server and a client.
type Action interface {
zapcore.ObjectMarshaler
// Apply causes the action's effects to be applied to s, mutating it in place.
// All Actions must conform to the standard that if an action can't be correctly applied, or if it would
// have no effect, it returns an error without changing s.
// If an action can be correctly applied but would have no effect, it should return ErrNoOp.
// If an action is correctly applied and has an effect, it should return nil.
Apply(s *state.Synced) error
}
type Client interface {
Server
// ToClientPB converts the action into a client action protocol buffer.
ToClientPB() *ClientActionPB
}
type Server interface {
Action
// ToServerPB converts the action into a server action protocol buffer.
ToServerPB() *ServerActionPB
}
func serverPBFromClient(c Client) *ServerActionPB {
return &ServerActionPB{
Action: &ServerActionPB_Client{Client: c.ToClientPB()},
}
}
type ServerSlice []Server
func (s ServerSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
var finalErr error = nil
for _, a := range s {
err := encoder.AppendObject(a)
if err != nil && finalErr == nil {
finalErr = err
}
}
return finalErr
}
// CellColor is the action sent when a cell of the map has been colored a different color.
type CellColor struct {
// At is the location of the cell in storage coordinates.
At state.StorageCoordinates `json:"at"`
// Color is the color the cell has been changed to.
Color state.Color `json:"color"`
}
func (c CellColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "CellColor")
err := encoder.AddObject("at", c.At)
encoder.AddString("color", c.Color.String())
return err
}
// Apply sets the target cell's color, or returns ErrNoOp if it can't.
func (c CellColor) Apply(s *state.Synced) error {
if c.Color.A < 0xF {
return ErrNoTransparentColors
}
cell, err := s.Map.Layer.GetCellAt(c.At)
if err != nil {
return err
}
if cell.Color == c.Color {
return ErrNoOp
}
cell.Color = c.Color
return nil
}
// UserActiveColor is the action sent when the user's current color, the one being painted with, changes.
type UserActiveColor struct {
// Color is the color that is now active.
Color state.Color `json:"color"`
}
func (c UserActiveColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "UserActiveColor")
encoder.AddString("color", c.Color.String())
return nil
}
// Apply sets the user's active color, or returns ErrNoOp if it can't.
func (c UserActiveColor) Apply(s *state.Synced) error {
if c.Color.A < 0xF {
return ErrNoTransparentColors
}
if s.User.ActiveColor == c.Color {
return ErrNoOp
}
s.User.ActiveColor = c.Color
return nil
}
// IDed contains a pair of ID and Action, as sent by the client.
type IDed struct {
// ID contains the arbitrary ID that was sent by the client, for identifying the action in future messages.
ID uint32 `json:"id"`
// Action contains the action that was actually being sent.
Action Client `json:"action"`
}
func (i IDed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddUint32("id", i.ID)
return encoder.AddObject("action", i.Action)
}

@ -1,78 +0,0 @@
package action
import (
"git.reya.zone/reya/hexmap/server/state"
)
func (x *ServerActionPB) ToGo() (Server, error) {
switch action := x.Action.(type) {
case nil:
return nil, state.ErrOneofNotSet
case *ServerActionPB_Client:
return action.Client.ToGo()
default:
panic("A case was missed in ServerActionPB.ToGo!")
}
}
func (x *ClientActionPB) ToGo() (Client, error) {
if x == nil {
return nil, nil
}
switch action := x.Action.(type) {
case nil:
return nil, state.ErrOneofNotSet
case *ClientActionPB_CellSetColor:
return action.CellSetColor.ToGo()
case *ClientActionPB_UserSetActiveColor:
return action.UserSetActiveColor.ToGo(), nil
default:
panic("A case was missed in ClientActionPB.ToGo!")
}
}
func (x *CellSetColorPB) ToGo() (*CellColor, error) {
at, err := x.At.ToGo()
if err != nil {
return nil, err
}
return &CellColor{
At: at,
Color: state.ColorFromRGBA8888(x.Color),
}, nil
}
func (c CellColor) ToServerPB() *ServerActionPB {
return serverPBFromClient(c)
}
func (c CellColor) ToClientPB() *ClientActionPB {
return &ClientActionPB{
Action: &ClientActionPB_CellSetColor{
CellSetColor: &CellSetColorPB{
Color: c.Color.ToRGBA8888(),
At: c.At.ToPB(),
},
},
}
}
func (x *UserSetActiveColorPB) ToGo() *UserActiveColor {
return &UserActiveColor{
Color: state.ColorFromRGBA8888(x.Color),
}
}
func (c UserActiveColor) ToServerPB() *ServerActionPB {
return serverPBFromClient(c)
}
func (c UserActiveColor) ToClientPB() *ClientActionPB {
return &ClientActionPB{
Action: &ClientActionPB_UserSetActiveColor{
UserSetActiveColor: &UserSetActiveColorPB{
Color: c.Color.ToRGBA8888(),
},
},
}
}

@ -1,240 +0,0 @@
package action
import (
"git.reya.zone/reya/hexmap/server/state"
"google.golang.org/protobuf/runtime/protoimpl"
"reflect"
"testing"
)
func TestCellColor_ToClientPB(t *testing.T) {
type fields struct {
At state.StorageCoordinates
Color state.Color
}
tests := []struct {
name string
fields fields
want *ClientActionPB
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := CellColor{
At: tt.fields.At,
Color: tt.fields.Color,
}
if got := c.ToClientPB(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToClientPB() = %v, want %v", got, tt.want)
}
})
}
}
func TestCellColor_ToServerPB(t *testing.T) {
type fields struct {
At state.StorageCoordinates
Color state.Color
}
tests := []struct {
name string
fields fields
want *ServerActionPB
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := CellColor{
At: tt.fields.At,
Color: tt.fields.Color,
}
if got := c.ToServerPB(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToServerPB() = %v, want %v", got, tt.want)
}
})
}
}
func TestCellSetColorPB_ToGo(t *testing.T) {
type fields struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Color uint32
At *state.StorageCoordinatesPB
}
tests := []struct {
name string
fields fields
want *CellColor
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
x := &CellSetColorPB{
state: tt.fields.state,
sizeCache: tt.fields.sizeCache,
unknownFields: tt.fields.unknownFields,
Color: tt.fields.Color,
At: tt.fields.At,
}
got, err := x.ToGo()
if (err != nil) != tt.wantErr {
t.Errorf("ToGo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToGo() got = %v, want %v", got, tt.want)
}
})
}
}
func TestClientActionPB_ToGo(t *testing.T) {
type fields struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Action isClientActionPB_Action
}
tests := []struct {
name string
fields fields
want Client
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
x := &ClientActionPB{
state: tt.fields.state,
sizeCache: tt.fields.sizeCache,
unknownFields: tt.fields.unknownFields,
Action: tt.fields.Action,
}
got, err := x.ToGo()
if (err != nil) != tt.wantErr {
t.Errorf("ToGo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToGo() got = %v, want %v", got, tt.want)
}
})
}
}
func TestServerActionPB_ToGo(t *testing.T) {
type fields struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Action isServerActionPB_Action
}
tests := []struct {
name string
fields fields
want Server
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
x := &ServerActionPB{
state: tt.fields.state,
sizeCache: tt.fields.sizeCache,
unknownFields: tt.fields.unknownFields,
Action: tt.fields.Action,
}
got, err := x.ToGo()
if (err != nil) != tt.wantErr {
t.Errorf("ToGo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToGo() got = %v, want %v", got, tt.want)
}
})
}
}
func TestUserActiveColor_ToClientPB(t *testing.T) {
type fields struct {
Color state.Color
}
tests := []struct {
name string
fields fields
want *ClientActionPB
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := UserActiveColor{
Color: tt.fields.Color,
}
if got := c.ToClientPB(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToClientPB() = %v, want %v", got, tt.want)
}
})
}
}
func TestUserActiveColor_ToServerPB(t *testing.T) {
type fields struct {
Color state.Color
}
tests := []struct {
name string
fields fields
want *ServerActionPB
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := UserActiveColor{
Color: tt.fields.Color,
}
if got := c.ToServerPB(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToServerPB() = %v, want %v", got, tt.want)
}
})
}
}
func TestUserSetActiveColorPB_ToGo(t *testing.T) {
type fields struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Color uint32
}
tests := []struct {
name string
fields fields
want *UserActiveColor
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
x := &UserSetActiveColorPB{
state: tt.fields.state,
sizeCache: tt.fields.sizeCache,
unknownFields: tt.fields.unknownFields,
Color: tt.fields.Color,
}
if got := x.ToGo(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToGo() = %v, want %v", got, tt.want)
}
})
}
}

@ -1,190 +0,0 @@
package action
import (
"git.reya.zone/reya/hexmap/server/state"
"go.uber.org/zap/zapcore"
"reflect"
"testing"
)
func TestCellColor_Apply(t *testing.T) {
type fields struct {
At state.StorageCoordinates
Color state.Color
}
type args struct {
s *state.Synced
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := CellColor{
At: tt.fields.At,
Color: tt.fields.Color,
}
if err := c.Apply(tt.args.s); (err != nil) != tt.wantErr {
t.Errorf("Apply() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestCellColor_MarshalLogObject(t *testing.T) {
type fields struct {
At state.StorageCoordinates
Color state.Color
}
type args struct {
encoder zapcore.ObjectEncoder
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := CellColor{
At: tt.fields.At,
Color: tt.fields.Color,
}
if err := c.MarshalLogObject(tt.args.encoder); (err != nil) != tt.wantErr {
t.Errorf("MarshalLogObject() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestIDed_MarshalLogObject(t *testing.T) {
type fields struct {
ID uint32
Action Client
}
type args struct {
encoder zapcore.ObjectEncoder
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
i := IDed{
ID: tt.fields.ID,
Action: tt.fields.Action,
}
if err := i.MarshalLogObject(tt.args.encoder); (err != nil) != tt.wantErr {
t.Errorf("MarshalLogObject() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestServerSlice_MarshalLogArray(t *testing.T) {
type args struct {
encoder zapcore.ArrayEncoder
}
tests := []struct {
name string
s ServerSlice
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.s.MarshalLogArray(tt.args.encoder); (err != nil) != tt.wantErr {
t.Errorf("MarshalLogArray() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestUserActiveColor_Apply(t *testing.T) {
type fields struct {
Color state.Color
}
type args struct {
s *state.Synced
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := UserActiveColor{
Color: tt.fields.Color,
}
if err := c.Apply(tt.args.s); (err != nil) != tt.wantErr {
t.Errorf("Apply() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestUserActiveColor_MarshalLogObject(t *testing.T) {
type fields struct {
Color state.Color
}
type args struct {
encoder zapcore.ObjectEncoder
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := UserActiveColor{
Color: tt.fields.Color,
}
if err := c.MarshalLogObject(tt.args.encoder); (err != nil) != tt.wantErr {
t.Errorf("MarshalLogObject() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_serverPBFromClient(t *testing.T) {
type args struct {
c Client
}
tests := []struct {
name string
args args
want *ServerActionPB
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := serverPBFromClient(tt.args.c); !reflect.DeepEqual(got, tt.want) {
t.Errorf("serverPBFromClient() = %v, want %v", got, tt.want)
}
})
}
}

@ -1,193 +0,0 @@
package main
import (
"context"
"git.reya.zone/reya/hexmap/server/room"
"git.reya.zone/reya/hexmap/server/state"
"git.reya.zone/reya/hexmap/server/ws"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
const SaveDir = "/home/reya/hexmaps"
func save(m state.HexMap, l *zap.Logger) error {
filename := filepath.Join(SaveDir, "map."+strconv.FormatInt(time.Now().Unix(), 16))
l.Debug("Saving to file", zap.String("filename", filename))
marshaled, err := proto.Marshal(m.ToPB())
l.Debug("Marshaled proto")
if err != nil {
return err
}
l.Debug("Opening file")
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0x644)
if err != nil {
return err
}
l.Debug("Writing to file")
_, err = file.Write(marshaled)
if err != nil {
return err
}
l.Debug("Closing file")
err = file.Close()
if err != nil {
return err
}
l.Info("Saved to file", zap.String("filename", filename))
return nil
}
func load(l *zap.Logger) (*state.HexMap, error) {
filename := filepath.Join(SaveDir, "map.LOAD")
l.Debug("Loading from file", zap.String("filename", filename))
file, err := os.Open(filename)
if err != nil {
return nil, err
}
l.Debug("Reading file")
marshaled, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
pb := &state.HexMapPB{}
l.Debug("Extracting protobuf from file")
err = proto.Unmarshal(marshaled, pb)
if err != nil {
return nil, err
}
l.Debug("Closing file")
err = file.Close()
if err != nil {
return nil, err
}
m, err := pb.ToGo()
if err != nil {
return nil, err
}
l.Info("Loaded from file", zap.String("filename", filename))
return &m, nil
}
func BackupMap(client *room.Client, l *zap.Logger) {
var err error
myState := &state.Synced{}
l.Info("Starting backup system")
for {
msg := <-client.IncomingChannel()
switch typedMsg := msg.(type) {
case *room.JoinResponse:
myState = typedMsg.CurrentState()
err := save(myState.Map, l)
if err != nil {
l.Error("Failed saving during join response", zap.Error(err))
}
case *room.ActionBroadcast:
err = typedMsg.Action().Apply(myState)
if err == nil {
err = save(myState.Map, l)
if err != nil {
l.Error("Failed saving during action broadcast", zap.Error(err))
}
}
case *room.ShutdownRequest:
client.OutgoingChannel() <- client.AcknowledgeShutdown()
return
}
}
}
func ServeWS(logger *zap.Logger) (err error) {
m := http.NewServeMux()
httpLogger := logger.Named("HTTP")
hexes, err := load(logger)
if err != nil {
hexes = state.NewHexMap(state.Layout{
Orientation: state.PointyTop,
IndentedLines: state.EvenLines,
}, 25, 10)
}
rm := room.New(room.NewOptions{
BaseLogger: logger.Named("Room"),
StartingState: &state.Synced{
Map: *hexes,
User: state.UserState{
ActiveColor: state.Color{
R: 0,
G: 0,
B: 0,
A: 255,
},
},
},
StartingClientOptions: room.NewClientOptions{
IncomingChannel: nil,
AcceptBroadcasts: true,
RequestStartingState: true,
},
})
go BackupMap(rm, logger.Named("BackupMap"))
m.Handle("/map", &ws.HTTPHandler{
Upgrader: websocket.Upgrader{
Subprotocols: []string{"v1.hexmap.deliciousreya.net"},
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == "https://hexmap.deliciousreya.net"
},
},
Logger: logger.Named("WS"),
Room: rm,
})
srv := http.Server{
Addr: "127.0.0.1:5238",
Handler: m,
ErrorLog: zap.NewStdLog(httpLogger),
}
m.HandleFunc("/exit", func(writer http.ResponseWriter, request *http.Request) {
// Some light dissuasion of accidental probing.
// To keep good people out.
if request.FormValue("superSecretPassword") != "Gesture/Retrial5/Untrained/Countable/Extrude/Jeep/Cheese/Carbon" {
writer.WriteHeader(403)
_, err = writer.Write([]byte("... What are you trying to pull?"))
return
}
writer.WriteHeader(200)
_, err := writer.Write([]byte("OK, shutting down, bye!"))
if err != nil {
logger.Warn("Error while writing goodbye response", zap.Error(err))
}
time.AfterFunc(500*time.Millisecond, func() {
err := srv.Shutdown(context.Background())
if err != nil {
logger.Error("Error while shutting down the server", zap.Error(err))
}
})
})
err = srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
return err
}
rm.OutgoingChannel() <- rm.Stop()
for {
msg := <-rm.IncomingChannel()
switch msg.(type) {
case *room.ShutdownRequest:
rm.OutgoingChannel() <- rm.AcknowledgeShutdown()
return nil
}
}
}
func main() {
logger, err := zap.NewDevelopment()
err = ServeWS(logger)
if err != nil {
logger.Fatal("Error while serving HTTP", zap.Error(err))
}
}

@ -1,68 +0,0 @@
package server
import (
"context"
"fmt"
"git.reya.zone/reya/hexmap/build"
"git.reya.zone/reya/hexmap/proto"
"github.com/magefile/mage/mg"
"io/fs"
"os"
"path/filepath"
"strings"
)
// TODO: GoGet
// TODO: Build
// TODO: Clean
type Protobuf mg.Namespace
func (Protobuf) InstallGoPlugin(ctx context.Context) error {
alreadyDone, err := build.HasExecutableInTools("protoc-gen-go")
if err != nil {
return err
}
if alreadyDone {
return nil
}
return build.InstallGoExecutable(ctx, "google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1")
}
func (Protobuf) InstallPlugins(ctx context.Context) {
mg.CtxDeps(ctx, Protobuf.InstallGoPlugin)
}
func ProtocFlags() ([]string, error) {
buildPath, err := filepath.Abs(filepath.Join(build.ToolsDir, "protoc-gen-go"))
if err != nil {
return nil, err
}
return []string{"--plugin=" + buildPath, "--go_out=.", "--go_opt=module=git.reya.zone/reya/hexmap"}, nil
}
func (Protobuf) Build(ctx context.Context) error {
mg.SerialCtxDeps(ctx, Protobuf.Clean, Protobuf.InstallPlugins)
return proto.Compile(ctx, []proto.ProtocFlagsFunc{ProtocFlags})
}
func (Protobuf) Clean(ctx context.Context) error {
return filepath.WalkDir("server", func(path string, d fs.DirEntry, dirErr error) error {
if ctx.Err() != nil {
return ctx.Err()
}
if dirErr != nil {
return dirErr
}
if strings.HasSuffix(path, ".pb.go") && !d.IsDir() {
if mg.Verbose() {
fmt.Printf("Removing generated protobuf code at %s\n", path)
}
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
})
}

@ -1 +0,0 @@
package persistence

@ -1,273 +0,0 @@
package room
import (
"git.reya.zone/reya/hexmap/server/action"
"git.reya.zone/reya/hexmap/server/state"
"github.com/rs/xid"
"go.uber.org/zap"
)
// act is the meat and potatoes of the room - it's responsible for actually running the room.
// It will gracefully remove all clients before shutting down.
func (r *room) act() {
defer r.gracefulShutdown()
r.logger.Info("Room starting up, listening for incoming actions")
for {
raw := <-r.incomingChannel
client := raw.ClientID()
msgLogger := r.logger.With(zap.Stringer("client", client))
msgLogger.Debug("Message received, handling", zap.Object("message", raw))
switch msg := raw.(type) {
case *JoinRequest:
r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel)
r.acknowledgeJoin(msg.id, msg.wantCurrentState)
case *RefreshRequest:
r.sendRefresh(msg.id)
case *ApplyRequest:
msgLogger.Debug("Received action to apply from client", zap.Uint32("actionId", msg.action.ID))
result := r.applyAction(msg.action.Action)
if result == nil {
r.broadcastAction(client, msg.action.ID, msg.action.Action)
}
r.acknowledgeAction(client, msg.action.ID, result)
case *LeaveRequest:
// So long, then. We can close immediately here; they promised not to send any more messages after this
// unless we were shutting down, which we're not.
r.acknowledgeLeave(client)
r.closeClient(client)
case *StopRequest:
// As requested, we shut down. Our deferred gracefulShutdown will catch us as we fall.
msgLogger.Info("Received StopRequest from client, shutting down")
return
case *ShutdownResponse:
// Uh... thank... you. I'm not... Never mind. I guess this means you're leaving?
msgLogger.Error("Received unexpected ShutdownResponse from client while not shutting down")
r.closeClient(client)
default:
msgLogger.Warn("Ignoring unhandled message", zap.Object("message", msg))
}
msgLogger.Debug("Message handled, resuming listening")
}
}
// addClient records a client's presence in the client map.
func (r *room) addClient(id xid.ID, returnChannel chan<- Message, broadcast bool, privateChannel bool) {
logger := r.logger.With(zap.Stringer("client", id))
logger.Debug("Adding client")
if client, ok := r.clients[id]; ok {
if client.outgoingChannel == returnChannel {
if broadcast == client.broadcast && privateChannel == client.privateChannel {
logger.Warn("Already have client when adding client")
} else {
logger.Error("Already have client but with different settings when adding client")
}
} else {
logger.Error("Already have a different client with the same id when adding client")
}
return
}
r.clients[id] = internalClient{
id: id,
outgoingChannel: returnChannel,
broadcast: broadcast,
privateChannel: privateChannel,
}
}
// stateCopy creates and returns a fresh copy of the current state.
// This avoids concurrent modification of the state while clients are reading it.
func (r *room) stateCopy() *state.Synced {
s := r.currentState.Copy()
return &s
}
// acknowledgeJoin composes and sends a JoinResponse to the given client.
func (r *room) acknowledgeJoin(id xid.ID, includeState bool) {
logger := r.logger.With(zap.Stringer("client", id))
client, ok := r.clients[id]
if !ok {
logger.Error("No such client when acknowledging join")
return
}
var s *state.Synced = nil
if includeState {
logger.Debug("Preparing state copy for client")
s = r.stateCopy()
}
logger.Debug("Sending JoinResponse to client")
client.outgoingChannel <- &JoinResponse{
id: r.id,
currentState: s,
}
}
// sendRefresh composes and sends a RefreshResponse to the given client.
func (r *room) sendRefresh(id xid.ID) {
logger := r.logger.With(zap.Stringer("client", id))
client, ok := r.clients[id]
if !ok {
logger.Error("No such client when sending refresh")
return
}
var s *state.Synced = nil
logger.Debug("Preparing state copy for client")
s = r.stateCopy()
logger.Debug("Sending RefreshResponse to client")
client.outgoingChannel <- &RefreshResponse{
id: r.id,
currentState: s,
}
}
// applyAction applies an action to the state and returns the result of it.
func (r *room) applyAction(action action.Action) error {
r.logger.Debug("Applying action", zap.Object("action", action))
return action.Apply(&r.currentState)
}
// broadcastAction sends an action to everyone other than the original client which requested it.
func (r *room) broadcastAction(originalClientID xid.ID, originalActionID uint32, action action.Server) {
logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Uint32("actionID", originalActionID), zap.Object("action", action))
broadcast := ActionBroadcast{
id: r.id,
originalClientID: originalClientID,
originalActionID: originalActionID,
action: action,
}
logger.Debug("Broadcasting action to all clients")
for id, client := range r.clients {
if id.Compare(originalClientID) != 0 {
logger.Debug("Sending ActionBroadcast to client", zap.Stringer("client", id))
client.outgoingChannel <- &broadcast
}
}
}
// acknowledgeAction sends a response to the original client which requested an action.
func (r *room) acknowledgeAction(id xid.ID, actionID uint32, error error) {
logger := r.logger.With(zap.Stringer("id", id), zap.Uint32("actionId", actionID), zap.Error(error))
logger.Debug("Responding to client with the status of its action")
client, ok := r.clients[id]
if !ok {
logger.Error("No such client when acknowledging action")
return
}
logger.Debug("Sending ApplyResponse to client")
client.outgoingChannel <- &ApplyResponse{
id: id,
actionID: actionID,
result: error,
}
}
// acknowledgeLeave causes the room to signal to the client that it has received and acknowledged the client's LeaveRequest,
// and will not send any further messages.
func (r *room) acknowledgeLeave(id xid.ID) {
logger := r.logger.With(zap.Stringer("client", id))
logger.Debug("Acknowledging client's leave request")
client, ok := r.clients[id]
if !ok {
logger.Error("No such client when acknowledging leave request")
return
}
logger.Debug("Sending LeaveResponse to client")
client.outgoingChannel <- &LeaveResponse{id: r.id}
}
// closeClient causes the room to remove the client with the given id from its clients.
// This should only be used after a shutdown handshake between this client and the room has taken place.
func (r *room) closeClient(id xid.ID) {
logger := r.logger.With(zap.Stringer("client", id))
logger.Debug("Closing client")
client, ok := r.clients[id]
if !ok {
logger.Error("Attempted to close a client that didn't exist")
return
}
if client.privateChannel {
logger.Debug("Closing outgoingChannel, as client has a privateChannel")
close(client.outgoingChannel)
}
delete(r.clients, id)
}
// gracefulShutdown causes the room to shut down cleanly, making sure that all clients have been removed.
func (r *room) gracefulShutdown() {
defer r.finalShutdown()
if len(r.clients) == 0 {
// Nothing to do, we've already won.
r.logger.Debug("No remaining clients, so just shutting down")
return
}
r.requestShutdown()
for len(r.clients) > 0 {
raw := <-r.incomingChannel
client := raw.ClientID()
msgLogger := r.logger.With(zap.Stringer("client", client))
msgLogger.Debug("Post-shutdown message received, handling", zap.Object("message", raw))
switch msg := raw.(type) {
case *JoinRequest:
// Don't you hate it when someone comes to the desk right as you're getting ready to pack up?
// Can't ignore them - they have our channel, and they might be sending things to it. We have to add them
// and then immediately send them a ShutdownRequest and wait for them to answer it.
msgLogger.Debug("Received join request from client while shutting down")
r.addClient(msg.id, msg.returnChannel, msg.broadcast, msg.privateChannel)
r.requestShutdownFrom(client)
case *RefreshRequest:
// Ugh, seriously, now? Fine. You can have this - you might be our friend the persistence actor.
r.sendRefresh(client)
case *LeaveRequest:
// We sent them a shutdown already, so unfortunately, we can't close them immediately. We have to wait for
// them to tell us they've heard that we're shutting down.
msgLogger.Debug("Received leave request from client while shutting down")
r.acknowledgeLeave(client)
case *StopRequest:
// Yes. We're doing that. Check your inbox, I already sent you the shutdown.
msgLogger.Debug("Received stop request from client while shutting down")
case *ShutdownResponse:
// The only way we would be getting one of these is if the client knows it doesn't have to send us anything
// else. Therefore, we can remove them now.
// Similarly, we know that they'll receive the LeaveResponse they need and shut down.
// Like us, even if it sent a LeaveRequest before realizing we were shutting down, it would have gotten our
// ShutdownRequest and sent this before it could read the LeaveResponse, but it will wait for the
// LeaveResponse regardless.
msgLogger.Debug("Received shutdown confirmation from client")
r.closeClient(client)
default:
msgLogger.Debug("Ignoring irrelevant message from client while shutting down", zap.Object("message", raw))
}
msgLogger.Debug("Message handled, resuming listening and waiting to be safe to shut down", zap.Int("clientsLeft", len(r.clients)))
}
r.logger.Debug("All clients have acknowledged the ShutdownRequest, now shutting down")
}
// requestShutdown produces a ShutdownRequest and sends it to all clients to indicate that the room is shutting down.
func (r *room) requestShutdown() {
r.logger.Debug("Alerting clients that shutdown is in progress", zap.Int("clientsLeft", len(r.clients)))
for id := range r.clients {
r.requestShutdownFrom(id)
}
}
// requestShutdownFrom produces a ShutdownRequest and sends it to the client with id to indicate that the room is
// shutting down.
func (r *room) requestShutdownFrom(id xid.ID) {
clientField := zap.Stringer("client", id)
r.logger.Debug("Alerting client that shutdown is in progress", clientField)
shutdown := ShutdownRequest{
id: r.id,
}
client, ok := r.clients[id]
if !ok {
r.logger.Error("No such client when requesting shutdown from client", clientField)
}
client.outgoingChannel <- &shutdown
}
// finalShutdown causes the room to do any final cleanup not involving its clients before stopping.
// Use gracefulShutdown instead, which calls this once it's safe to do so.
func (r *room) finalShutdown() {
r.logger.Debug("Closing incoming channel")
close(r.incomingChannel)
r.logger.Info("Shut down")
}

@ -1,173 +0,0 @@
package room
import (
"git.reya.zone/reya/hexmap/server/action"
"github.com/rs/xid"
)
// internalClient is used by the room itself to track information about a client.
type internalClient struct {
// id is the id that the client identifies itself with in all clientMessage instances it sends.
id xid.ID
// outgoingChannel is a channel that the room can send messages to the client on.
outgoingChannel chan<- Message
// privateChannel is true iff the room can close the outgoingChannel when the client and room have completed their
// close handshake.
privateChannel bool
// broadcast is true iff the client requested to be included on broadcasts on creation.
broadcast bool
}
type NewClientOptions struct {
// IncomingChannel is the channel to use as the room's channel to send messages to - the new Client's IncomingChannel.
// If this is non-nil, the room will not automatically close the IncomingChannel after a shutdown is negotiated.
// If this is nil, a new channel will be allocated on join and closed on shutdown.
IncomingChannel chan Message
// If AcceptBroadcasts is true, the room will send all broadcasts originating from other clients to this client.
AcceptBroadcasts bool
// If RequestStartingState is true, the room will send a copy of the current state as of when the JoinRequest was
// received in the JoinResponse that will be the first message the Client receives.
RequestStartingState bool
}
// Client is the structure used by clients external to the Room package to communicate with the Room.
// It is not expected to be parallel-safe; to run it in parallel, use the NewClient method and send the new client to
// the new goroutine.
type Client struct {
// id is the ClientID used by the client for all communications.
id xid.ID
// roomId is the unique ID of the room (not its map).
roomId xid.ID
// incomingChannel is the channel that this client receives messages on.
incomingChannel <-chan Message
// outgoingChannel is the channel that this client sends messages on.
// Becomes nil if the client has been completely shut down.
outgoingChannel chan<- ClientMessage
// Once Leave or AcknowledgeShutdown have been triggered, this flag is set, preventing use of other messages.
shuttingDown bool
}
// ID is the ID used by this client to identify itself to the Room.
func (c *Client) ID() xid.ID {
return c.id
}
// RoomID is the ID used by the room to differentiate itself from other rooms.
// It is not the map's internal ID.
func (c *Client) RoomID() xid.ID {
return c.id
}
// IncomingChannel is the channel the client can listen on for messages from the room.
func (c *Client) IncomingChannel() <-chan Message {
return c.incomingChannel
}
// OutgoingChannel is the channel the client can send messages to the room on.
func (c *Client) OutgoingChannel() chan<- ClientMessage {
if c.outgoingChannel == nil {
panic("Already finished shutting down; no new messages should be sent")
}
return c.outgoingChannel
}
// newClientForRoom uses the necessary parameters to create and join a Client for the given room.
func newClientForRoom(roomId xid.ID, outgoingChannel chan<- ClientMessage, opts NewClientOptions) *Client {
var privateChannel bool
var incomingChannel chan Message
if opts.IncomingChannel != nil {
incomingChannel = opts.IncomingChannel
privateChannel = false
} else {
incomingChannel = make(chan Message, 1)
privateChannel = true
}
result := Client{
id: xid.New(),
roomId: roomId,
incomingChannel: incomingChannel,
outgoingChannel: outgoingChannel,
shuttingDown: false,
}
result.outgoingChannel <- &JoinRequest{
id: result.id,
returnChannel: incomingChannel,
privateChannel: privateChannel,
broadcast: opts.AcceptBroadcasts,
wantCurrentState: opts.RequestStartingState,
}
return &result
}
// NewClient creates a new client belonging to the same room as this client with a random ID.
// The new client will be automatically joined to the channel.
func (c *Client) NewClient(opts NewClientOptions) *Client {
if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent")
}
return newClientForRoom(c.roomId, c.outgoingChannel, opts)
}
// Refresh creates a message which causes the client to request a fresh copy of the state.
func (c *Client) Refresh() *RefreshRequest {
if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent")
}
return &RefreshRequest{
id: c.id,
}
}
func (c *Client) Apply(a action.IDed) *ApplyRequest {
if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent")
}
return &ApplyRequest{
id: c.id,
action: a,
}
}
// Leave creates a message which causes the local client to signal that it is shutting down.
// It is important to Leave to avoid dangling clients having messages sent to nothing.
// After sending Leave, the client must confirm that it has been removed by waiting for a LeaveResponse, accompanied by
// the closing of the Client's IncomingChannel if it was a private channel.
// No further messages should be sent after Leave except AcknowledgeShutdown if Leave and requestShutdown crossed paths in midair.
func (c *Client) Leave() *LeaveRequest {
if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent")
}
c.shuttingDown = true
return &LeaveRequest{
id: c.id,
}
}
// Stop creates a message which causes the local client to signal that it is shutting down.
// It is important to Stop when the room needs to be shut down.
// After sending Stop, the client must confirm that it has been removed by waiting for a ShutdownRequest, which should
// be handled normally.
// No further messages should be sent after Stop except AcknowledgeShutdown.
func (c *Client) Stop() *StopRequest {
if c.shuttingDown {
panic("Already started shutting down; no new messages should be sent")
}
c.shuttingDown = true
return &StopRequest{
id: c.id,
}
}
// AcknowledgeShutdown causes the local client to signal that it has acknowledged that the room is shutting down.
// No further messages can be sent after AcknowledgeShutdown; attempting to do so will block forever, as the
// OutgoingChannel has become nil.
func (c *Client) AcknowledgeShutdown() *ShutdownResponse {
if c.outgoingChannel == nil {
panic("Already finished shutting down; no new messages should be sent")
}
c.shuttingDown = true
c.outgoingChannel = nil
return &ShutdownResponse{
id: c.id,
}
}

@ -1,124 +0,0 @@
package room
import (
action2 "git.reya.zone/reya/hexmap/server/action"
"github.com/rs/xid"
"go.uber.org/zap/zapcore"
)
// ClientMessage marks messages coming from clients to the room.
type ClientMessage interface {
zapcore.ObjectMarshaler
// ClientID is the id of the client sending the message.
ClientID() xid.ID
}
// JoinRequest is the message sent on the room's IncomingChannel by a new client joining the room.
type JoinRequest struct {
// id is the SourceID the client will use to identify itself in future messages.
id xid.ID
// returnChannel is a buffered channel the client is ready to receive messages from the room on.
// This becomes the Client's OutgoingChannel.
returnChannel chan<- Message
// privateChannel is true iff the room can close returnChannel after completing a shutdown handshake.
// This permits extra safety by causing channels that somehow leak into other contexts to become noticeable by
// causing panics.
privateChannel bool
// broadcast is true iff the room should send action.Syncable from other clients to this one.
broadcast bool
// wantCurrentState indicates that the client would like the room to include a copy of the current state of the room
// when it joins.
wantCurrentState bool
}
func (j *JoinRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "JoinRequest")
encoder.AddString("id", j.id.String())
encoder.AddBool("broadcast", j.broadcast)
encoder.AddBool("wantCurrentState", j.wantCurrentState)
return nil
}
func (j *JoinRequest) ClientID() xid.ID {
return j.id
}
// RefreshRequest is the message sent on the room's IncomingChannel by a client which needs the current value.
type RefreshRequest struct {
id xid.ID
}
func (r *RefreshRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "RefreshRequest")
encoder.AddString("id", r.id.String())
return nil
}
func (r *RefreshRequest) ClientID() xid.ID {
return r.id
}
// ApplyRequest is the message sent on the room's IncomingChannel by a client which has received an action from the
// ws.
type ApplyRequest struct {
id xid.ID
action action2.IDed
}
func (f *ApplyRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ApplyRequest")
encoder.AddString("id", f.id.String())
return encoder.AddObject("action", f.action)
}
func (f *ApplyRequest) ClientID() xid.ID {
return f.id
}
// LeaveRequest is the message sent on the room's IncomingChannel by a client which is shutting down.
// The client is indicating that it will send no messages except a possible ShutdownResponse, in the event that a
// LeaveRequest and a ShutdownRequest cross paths midflight.
type LeaveRequest struct {
id xid.ID
}
func (l *LeaveRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "LeaveRequest")
encoder.AddString("id", l.id.String())
return nil
}
func (l *LeaveRequest) ClientID() xid.ID {
return l.id
}
// StopRequest is the message sent on the room's IncomingChannel by a client which wants to make the room shut down.
// The response to a StopRequest is a ShutdownRequest, which should be handled as normal.
type StopRequest struct {
id xid.ID
}
func (s *StopRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "StopRequest")
encoder.AddString("id", s.id.String())
return nil
}
func (s *StopRequest) ClientID() xid.ID {
return s.id
}
// ShutdownResponse is the message sent on the room's IncomingChannel by a client which has accepted the room's ShutdownRequest.
type ShutdownResponse struct {
id xid.ID
}
func (s *ShutdownResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ShutdownResponse")
encoder.AddString("id", s.id.String())
return nil
}
func (s *ShutdownResponse) ClientID() xid.ID {
return s.id
}

@ -1,160 +0,0 @@
package room
import (
"git.reya.zone/reya/hexmap/server/action"
"git.reya.zone/reya/hexmap/server/state"
"github.com/rs/xid"
"go.uber.org/zap/zapcore"
)
// Message marks messages going to clients from the room.
type Message interface {
zapcore.ObjectMarshaler
// RoomID marks the ID of the room this Message originated from, in case the Client has a shared IncomingChannel.
RoomID() xid.ID
}
// JoinResponse is the message sent by the room on a new client's IncomingChannel after it joins.
type JoinResponse struct {
id xid.ID
// currentState is a pointer to a copy of the room's current state.
// If the client refused the room state in its join message, this will be a nil pointer instead.
currentState *state.Synced
}
func (j *JoinResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "JoinResponse")
encoder.AddString("id", j.id.String())
return encoder.AddObject("currentState", j.currentState)
}
func (j *JoinResponse) RoomID() xid.ID {
return j.id
}
// CurrentState returns the state of the room as of when the JoinRequest was processed.
func (j *JoinResponse) CurrentState() *state.Synced {
return j.currentState
}
// RefreshResponse is the message sent by the room after a client requests it, or immediately on join.
type RefreshResponse struct {
id xid.ID
// currentState is a pointer to a copy of the room's current state.
currentState *state.Synced
}
func (r *RefreshResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "JoinResponse")
encoder.AddString("id", r.id.String())
return encoder.AddObject("currentState", r.currentState)
}
func (r *RefreshResponse) RoomID() xid.ID {
return r.id
}
// CurrentState returns the state of the room as of when the RefreshRequest was processed.
func (r *RefreshResponse) CurrentState() *state.Synced {
return r.currentState
}
// ApplyResponse returns the result of an action to _only_ the one that sent the ApplyRequest.
type ApplyResponse struct {
id xid.ID
// actionID is the ID of the action that completed or failed.
actionID uint32
// result is nil if the action was completed, or an error if it failed.
result error
}
func (a *ApplyResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ApplyResponse")
encoder.AddString("id", a.id.String())
encoder.AddUint32("actionId", a.actionID)
encoder.AddBool("success", a.result == nil)
if a.result != nil {
encoder.AddString("failure", a.result.Error())
}
return nil
}
func (a *ApplyResponse) RoomID() xid.ID {
return a.id
}
func (a *ApplyResponse) ActionID() uint32 {
return a.actionID
}
// Success returns true if the action succeeded, false if it failed.
func (a *ApplyResponse) Success() bool {
return a.result == nil
}
// Failure returns the error if the action failed, or nil if it succeeded.
func (a *ApplyResponse) Failure() error {
return a.result
}
// ActionBroadcast is sent to all clients _other_ than the one that sent the ApplyRequest when an action succeeds.
type ActionBroadcast struct {
id xid.ID
// originalClientID is the client that sent the action in the first place.
originalClientID xid.ID
// originalActionID is the ID that the client that sent the action sent.
originalActionID uint32
// action is the action that succeeded.
action action.Server
}
func (a *ActionBroadcast) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ActionBroadcast")
encoder.AddString("id", a.id.String())
encoder.AddString("originalClientId", a.originalClientID.String())
encoder.AddUint32("originalActionId", a.originalActionID)
return encoder.AddObject("action", a.action)
}
func (a *ActionBroadcast) RoomID() xid.ID {
return a.id
}
func (a *ActionBroadcast) OriginalClientID() xid.ID {
return a.originalClientID
}
func (a *ActionBroadcast) OriginalActionID() uint32 {
return a.originalActionID
}
func (a *ActionBroadcast) Action() action.Server {
return a.action
}
// LeaveResponse is the message sent by the room when it has accepted that a client has left, and will send it no further messages.
type LeaveResponse struct {
id xid.ID
}
func (l *LeaveResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "LeaveResponse")
encoder.AddString("id", l.id.String())
return nil
}
func (l *LeaveResponse) RoomID() xid.ID {
return l.id
}
// ShutdownRequest is the message sent by the room when something causes it to shut down. It will send the client no further messages except a possible LeaveResponse.
type ShutdownRequest struct {
id xid.ID
}
func (s *ShutdownRequest) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ShutdownRequest")
encoder.AddString("id", s.id.String())
return nil
}
func (s *ShutdownRequest) RoomID() xid.ID {
return s.id
}

@ -1,54 +0,0 @@
package room
import (
"git.reya.zone/reya/hexmap/server/state"
"github.com/rs/xid"
"go.uber.org/zap"
)
// NewOptions is the set of information used to control what a room starts with.
type NewOptions struct {
// BaseLogger is the logger that the room should attach its data to.
BaseLogger *zap.Logger
// StartingState is a state.Synced that defines the state of the room on creation.
StartingState *state.Synced
// StartingClientOptions sets the configuration of the first client to be created, the one that will be returned
// from New.
StartingClientOptions NewClientOptions
}
// room is a room as seen from within - the information needed for the room process to do its duties.
type room struct {
// id is the room's internal ID - not to be confused with the ID of the map it is serving.
id xid.ID
// incomingChannel is the channel the room uses to receive messages. The room itself owns this channel,
// so when the clients map is empty, it can be closed.
incomingChannel chan ClientMessage
// clients contains the map of active clients, each of which is known to have a reference to this room.
clients map[xid.ID]internalClient
// currentState contains the active state being used by actions right now.
currentState state.Synced
// logger is the logger that this room will use. It contains context fields for the room's important fields.
logger *zap.Logger
}
// New creates and starts up a new room, joins a new client to it and returns that client.
func New(opts NewOptions) *Client {
logger := opts.BaseLogger.Named("Room")
id := xid.New()
r := room{
id: id,
incomingChannel: make(chan ClientMessage),
clients: make(map[xid.ID]internalClient),
currentState: *opts.StartingState,
logger: logger,
}
go r.act()
return r.newClient(opts.StartingClientOptions)
}
// newClient creates a new client belonging to this room with a random ID.
// The new client will be automatically joined to the channel.
func (r *room) newClient(opts NewClientOptions) *Client {
return newClientForRoom(r.id, r.incomingChannel, opts)
}

@ -1,71 +0,0 @@
package state
import (
"errors"
"fmt"
"strconv"
"strings"
)
// Color is an internal representation of an RGBA8888 color.
type Color struct {
// R is the red component of the color.
R uint8
// G is the green component of the color.
G uint8
// B is the blue component of the color.
B uint8
// A is the alpha component of the color.
A uint8
}
var TransparentColor = Color{R: 0, G: 0, B: 0, A: 0}
var ErrInvalidColorString = errors.New("color strings must start with # and be followed by 3, 4, 6, or 8 hex digits")
func ColorFromString(s string) (Color, error) {
if !strings.HasPrefix(s, "#") {
return TransparentColor, ErrInvalidColorString
}
hex := s[1:]
v, err := strconv.ParseUint(hex, 16, 64)
if err != nil {
return TransparentColor, ErrInvalidColorString
}
switch len(hex) {
case 3:
return ColorFromRGBA4444(uint16(v<<4 | 0xF)), nil
case 4:
return ColorFromRGBA4444(uint16(v)), nil
case 6:
return ColorFromRGBA8888(uint32(v<<8 | 0xFF)), nil
case 8:
return ColorFromRGBA8888(uint32(v)), nil
default:
return TransparentColor, ErrInvalidColorString
}
}
// String prints the Color in an abbreviated notation.
// Specifically, short form is used when possible (i.e., when all components are evenly divisible by 0x11).
// The alpha component is left out if it's 0xFF.
func (c Color) String() string {
if c.R%0x11 == 0 && c.G%0x11 == 0 && c.B%0x11 == 0 && c.A%0x11 == 0 {
// Short form works.
if c.A == 0xFF {
// It's great when it's easy!
return fmt.Sprintf("#%01X%01X%01X", c.R/0x11, c.G/0x11, c.B/0x11)
} else {
// Just need to add the alpha.
return fmt.Sprintf("#%01X%01X%01X%01X", c.R/0x11, c.G/0x11, c.B/0x11, c.A/0x11)
}
} else {
// Gotta use long form.
if c.A == 0xFF {
// Can skip the alpha channel, though.
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
} else {
// Doing things the hard way.
return fmt.Sprintf("#%02X%02X%02X%02X", c.R, c.G, c.B, c.A)
}
}
}

@ -1,31 +0,0 @@
package state
// ColorFromRGBA8888 decodes a packed uint32 (RGBA8888) into a hex color.
func ColorFromRGBA8888(value uint32) Color {
return Color{
R: uint8((value >> 24) & 0xFF),
G: uint8((value >> 16) & 0xFF),
B: uint8((value >> 8) & 0xFF),
A: uint8((value >> 0) & 0xFF),
}
}
// ColorFromRGBA4444 decodes a packed uint16 (RGBA4444) into a hex color.
func ColorFromRGBA4444(value uint16) Color {
return Color{
R: uint8((value>>12)&0xF) * 0x11,
G: uint8((value>>8)&0xF) * 0x11,
B: uint8((value>>4)&0xF) * 0x11,
A: uint8((value>>0)&0xF) * 0x11,
}
}
// ToRGBA8888 packs a hex color into a uint32 as RGBA8888.
func (c Color) ToRGBA8888() uint32 {
return uint32(c.R)<<24 | uint32(c.G)<<16 | uint32(c.B)<<8 | uint32(c.A)
}
// ToRGBA4444 packs a hex color into a uint16 as RGBA4444.
func (c Color) ToRGBA4444() uint16 {
return uint16((c.R>>4)&0xF)<<12 | uint16((c.G>>4)&0xF)<<8 | uint16((c.B>>4)&0xF)<<4 | uint16((c.A>>4)&0xF)
}

@ -1,19 +0,0 @@
package state
import (
"go.uber.org/zap/zapcore"
)
// StorageCoordinates gives the coordinates of a cell in a form optimized for storage.
type StorageCoordinates struct {
// Line is the index from 0 to Lines - 1 of the HexLine in the HexLayer.
Line uint8 `json:"line"`
// Cell is the index from 0 to CellsPerLine - 1 of the HexCell in the HexLine.
Cell uint8 `json:"cell"`
}
func (s StorageCoordinates) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddUint8("line", s.Line)
encoder.AddUint8("cell", s.Cell)
return nil
}

@ -1,25 +0,0 @@
package state
import (
"errors"
"math"
)
var ErrOutOfBounds = errors.New("coordinate was out of bounds")
func (x *StorageCoordinatesPB) ToGo() (StorageCoordinates, error) {
if x.Line > math.MaxUint8 || x.Cell > math.MaxUint8 {
return StorageCoordinates{}, ErrOutOfBounds
}
return StorageCoordinates{
Line: uint8(x.Line),
Cell: uint8(x.Cell),
}, nil
}
func (s StorageCoordinates) ToPB() *StorageCoordinatesPB {
return &StorageCoordinatesPB{
Line: uint32(s.Line),
Cell: uint32(s.Cell),
}
}

@ -1,206 +0,0 @@
package state
import (
"fmt"
"github.com/rs/xid"
"go.uber.org/zap/zapcore"
)
// HexOrientation is the enum for the direction hexes are facing.
type HexOrientation uint8
const (
// UnknownOrientation indicates that an invalid orientation was specified.
UnknownOrientation HexOrientation = 0
// PointyTop indicates hexes that have a pair of sides on either side in the horizontal direction,
// and points on the top and bottom in the vertical direction.
PointyTop HexOrientation = 1
// FlatTop indicates hexes that have a pair of points on either side in the horizontal direction,
// and sides on the top and bottom in the vertical direction.
FlatTop HexOrientation = 2
)
// String returns the equivalent JavaScript constant name.
func (o HexOrientation) String() string {
switch o {
case PointyTop:
return "POINTY_TOP"
case FlatTop:
return "FLAT_TOP"
default:
return fmt.Sprintf("[unknown HexOrientation %d]", o)
}
}
// LineParity indicates whether odd or even lines are indented.
type LineParity uint8
const (
// UnknownParity indicates that parity was not specified or unknown.
UnknownParity LineParity = 0
// OddLines indicates that odd lines - 1, 3, 5... - are indented by 1/2 cell.
OddLines LineParity = 1
// EvenLines indicates that even lines - 0, 2, 4... - are indented by 1/2 cell.
EvenLines LineParity = 2
)
// String returns the equivalent JavaScript constant name.
func (o LineParity) String() string {
switch o {
case OddLines:
return "ODD_LINES"
case EvenLines:
return "EVEN_LINES"
default:
return fmt.Sprintf("[unknown LineParity %d]", o)
}
}
// Layout combines HexOrientation and LineParity to represent a map's display mode.
type Layout struct {
Orientation HexOrientation `json:"orientation"`
IndentedLines LineParity `json:"indentedLines"`
}
func (l Layout) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("orientation", l.Orientation.String())
encoder.AddString("indentedLines", l.IndentedLines.String())
return nil
}
// HexCell contains data for a single cell of the map.
type HexCell struct {
// Color contains the color of the cell, in hex notation.
Color Color `json:"color"`
}
func (h HexCell) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("color", h.Color.String())
return nil
}
// HexLine is a line of cells which are adjacent by flat sides in a vertical or horizontal direction.
type HexLine []HexCell
func (l HexLine) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
var finalErr error
for _, cell := range l {
err := encoder.AppendObject(cell)
if err != nil && finalErr == nil {
finalErr = err
}
}
return finalErr
}
// Copy creates a deep copy of this HexLine.
func (l HexLine) Copy() HexLine {
duplicate := make(HexLine, len(l))
for index, value := range l {
duplicate[index] = value
}
return duplicate
}
// HexLayer is a two-dimensional plane of cells which are arranged into lines.
type HexLayer []HexLine
func (l HexLayer) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
var finalErr error
for _, line := range l {
err := encoder.AppendArray(line)
if err != nil && finalErr == nil {
finalErr = err
}
}
return finalErr
}
// GetCellAt returns a reference to the cell at the given coordinates.
// If the coordinates are out of bounds for this map, an error will be returned.
func (l HexLayer) GetCellAt(c StorageCoordinates) (*HexCell, error) {
if int(c.Line) >= len(l) {
return nil, fmt.Errorf("line %d out of bounds (%d)", c.Line, len(l))
}
line := l[c.Line]
if int(c.Cell) >= len(line) {
return nil, fmt.Errorf("cell %d out of bounds (%d)", c.Cell, len(line))
}
return &(line[c.Cell]), nil
}
// Copy creates a deep copy of this HexLayer.
func (l HexLayer) Copy() HexLayer {
duplicate := make(HexLayer, len(l))
for index, value := range l {
duplicate[index] = value.Copy()
}
return duplicate
}
// HexMap contains the data for a map instance.
type HexMap struct {
// XID is the unique id of the HexMap, used to encourage clients not to blindly interact with a different map.
XID xid.ID `json:"xid"`
// Lines is the rough number of rows (in PointyTop orientation) or columns (in FlatTop orientation) in the map.
// Because different lines will be staggered, it's somewhat hard to see.
Lines uint8 `json:"lines"`
// CellsPerLine is the rough number of columns (in PointyTop orientation) or rows (in FlatTop orientation).
// This is the number of cells joined together, flat-edge to flat-edge, in each line.
CellsPerLine uint8 `json:"cellsPerLine"`
// Layout is the orientation and line parity used to display the map.
Layout Layout `json:"layout"`
// Layer contains the actual map data.
// Layer itself is a slice with Lines elements, each of which is a line;
// each of those lines is a slice of CellsPerLine cells.
Layer HexLayer `json:"layer"`
}
func NewHexMap(layout Layout, lines uint8, cellsPerLine uint8) *HexMap {
layer := make(HexLayer, lines)
for index := range layer {
line := make(HexLine, cellsPerLine)
for index := range line {
line[index] = HexCell{
Color: Color{
R: 255,
G: 255,
B: 255,
A: 255,
},
}
}
layer[index] = line
}
return &HexMap{
XID: xid.New(),
Layout: layout,
Lines: lines,
CellsPerLine: cellsPerLine,
Layer: layer,
}
}
func (m HexMap) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("id", m.XID.String())
encoder.AddUint8("lines", m.Lines)
encoder.AddUint8("cellsPerLine", m.CellsPerLine)
displayModeErr := encoder.AddObject("layout", m.Layout)
lineCellsErr := encoder.AddArray("lineCells", m.Layer)
if displayModeErr != nil {
return displayModeErr
} else {
return lineCellsErr
}
}
// Copy creates a deep copy of this HexMap.
func (m HexMap) Copy() HexMap {
return HexMap{
XID: m.XID,
Lines: m.Lines,
CellsPerLine: m.CellsPerLine,
Layout: m.Layout,
Layer: m.Layer.Copy(),
}
}

@ -1,142 +0,0 @@
package state
import (
"github.com/rs/xid"
"math"
)
func (x HexMapPB_Layout_Orientation) ToGo() HexOrientation {
switch x {
case HexMapPB_Layout_POINTY_TOP:
return PointyTop
case HexMapPB_Layout_FLAT_TOP:
return FlatTop
default:
return UnknownOrientation
}
}
func (o HexOrientation) ToPB() HexMapPB_Layout_Orientation {
switch o {
case PointyTop:
return HexMapPB_Layout_POINTY_TOP
case FlatTop:
return HexMapPB_Layout_FLAT_TOP
default:
return HexMapPB_Layout_UNKNOWN_ORIENTATION
}
}
func (x HexMapPB_Layout_LineParity) ToGo() LineParity {
switch x {
case HexMapPB_Layout_EVEN:
return EvenLines
case HexMapPB_Layout_ODD:
return OddLines
default:
return UnknownParity
}
}
func (o LineParity) ToPB() HexMapPB_Layout_LineParity {
switch o {
case OddLines:
return HexMapPB_Layout_ODD
case EvenLines:
return HexMapPB_Layout_EVEN
default:
return HexMapPB_Layout_UNKNOWN_LINE
}
}
func (x *HexMapPB_Layout) ToGo() Layout {
return Layout{
Orientation: x.Orientation.ToGo(),
IndentedLines: x.IndentedLines.ToGo(),
}
}
func (l Layout) ToPB() *HexMapPB_Layout {
return &HexMapPB_Layout{
Orientation: l.Orientation.ToPB(),
IndentedLines: l.IndentedLines.ToPB(),
}
}
func (x *HexCellPB) ToGo() HexCell {
return HexCell{
Color: ColorFromRGBA8888(x.Color),
}
}
func (h HexCell) ToPB() *HexCellPB {
return &HexCellPB{
Color: h.Color.ToRGBA8888(),
}
}
func (x *HexLinePB) ToGo() HexLine {
r := make(HexLine, len(x.Cells))
for index, cell := range x.Cells {
r[index] = cell.ToGo()
}
return r
}
func (l HexLine) ToPB() *HexLinePB {
cells := make([]*HexCellPB, len(l))
for index, cell := range l {
cells[index] = cell.ToPB()
}
return &HexLinePB{
Cells: cells,
}
}
func (x *HexLayerPB) ToGo() HexLayer {
r := make(HexLayer, len(x.Lines))
for index, line := range x.Lines {
r[index] = line.ToGo()
}
return r
}
func (l HexLayer) ToPB() *HexLayerPB {
lines := make([]*HexLinePB, len(l))
for index, line := range l {
lines[index] = line.ToPB()
}
return &HexLayerPB{
Lines: lines,
}
}
func (x *HexMapPB) ToGo() (HexMap, error) {
pbId, err := xid.FromBytes(x.Xid)
if err != nil {
return HexMap{}, err
}
if x.Lines > math.MaxUint8 {
return HexMap{}, ErrOutOfBounds
}
if x.CellsPerLine > math.MaxUint8 {
return HexMap{}, ErrOutOfBounds
}
return HexMap{
XID: pbId,
Lines: uint8(x.Lines),
CellsPerLine: uint8(x.CellsPerLine),
Layout: x.Layout.ToGo(),
Layer: x.Layer.ToGo(),
}, nil
}
func (m HexMap) ToPB() *HexMapPB {
return &HexMapPB{
Xid: m.XID.Bytes(),
Lines: uint32(m.Lines),
CellsPerLine: uint32(m.CellsPerLine),
Layout: m.Layout.ToPB(),
Layer: m.Layer.ToPB(),
}
}

@ -1,5 +0,0 @@
package state
import "errors"
var ErrOneofNotSet = errors.New("no value was given for a oneof")

@ -1,29 +0,0 @@
package state
import (
"go.uber.org/zap/zapcore"
)
// Synced contains all state that is synced between the server and its clients.
type Synced struct {
Map HexMap `json:"map"`
User UserState `json:"user"`
}
func (s *Synced) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
mapErr := encoder.AddObject("map", s.Map)
userErr := encoder.AddObject("user", s.User)
if mapErr != nil {
return mapErr
} else {
return userErr
}
}
// Copy creates a deep copy of this Synced instance.
func (s *Synced) Copy() Synced {
return Synced{
Map: s.Map.Copy(),
User: s.User.Copy(),
}
}

@ -1,20 +0,0 @@
package state
func (x *SyncableStatePB) ToGo() (Synced, error) {
pbMap, err := x.Map.ToGo()
if err != nil {
return Synced{}, err
}
user := x.User.ToGo()
return Synced{
Map: pbMap,
User: user,
}, nil
}
func (s Synced) ToPB() *SyncableStatePB {
return &SyncableStatePB{
Map: s.Map.ToPB(),
User: s.User.ToPB(),
}
}

@ -1,21 +0,0 @@
package state
import "go.uber.org/zap/zapcore"
// UserState contains data about clients that is synced between client and server.
// Unlike the map, UserState is not persisted to disk, and all UserState is lost on shutdown.
type UserState struct {
ActiveColor Color `json:"activeColor"`
}
func (u UserState) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("activeColor", u.ActiveColor.String())
return nil
}
// Copy creates a deep copy of this UserState.
func (u UserState) Copy() UserState {
return UserState{
ActiveColor: u.ActiveColor,
}
}

@ -1,13 +0,0 @@
package state
func (x *UserStatePB) ToGo() UserState {
return UserState{
ActiveColor: ColorFromRGBA8888(x.Color),
}
}
func (u UserState) ToPB() *UserStatePB {
return &UserStatePB{
Color: u.ActiveColor.ToRGBA8888(),
}
}

@ -1,74 +0,0 @@
package ws
import (
"git.reya.zone/reya/hexmap/server/action"
"go.uber.org/zap/zapcore"
)
// ClientCommandType is an enum type for the client's protocol messages.
type ClientCommandType string
// ClientCommand s are those sent by the client.
type ClientCommand interface {
zapcore.ObjectMarshaler
// ToClientPB converts the command to a client protocol buffer which will be sent on the wire.
ToClientPB() *ClientCommandPB
}
// ClientHello is the command sent by the client when it first establishes the connection.
type ClientHello struct {
// Version is the protocol version the client is running.
Version uint32 `json:"version"`
}
func (c ClientHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "Hello")
encoder.AddUint32("version", c.Version)
return nil
}
// ClientRefresh is the command sent by the client when it needs the full state re-sent.
type ClientRefresh struct {
}
func (c ClientRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "Refresh")
return nil
}
type IDPairs []action.IDed
func (a IDPairs) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
var finalErr error = nil
for _, v := range a {
err := encoder.AppendObject(v)
if err != nil && finalErr == nil {
finalErr = err
}
}
return finalErr
}
// ClientAct is a command sent in order to deliver one or more Action actions to the server.
type ClientAct struct {
// Actions contains the actions the client wants to apply, in the order they should be applied.
Actions IDPairs `json:"actions"`
}
func (c ClientAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "Act")
return encoder.AddArray("actions", c.Actions)
}
// ClientMalformed is synthesized by the reader when it has read a command that does not appear to match the
// protocol.
type ClientMalformed struct {
// Error is the error in parse that caused the reader to be unable to read the message.
Error error
}
func (c ClientMalformed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "(malformed command)")
encoder.AddString("error", c.Error.Error())
return nil
}

@ -1,99 +0,0 @@
package ws
import (
"git.reya.zone/reya/hexmap/server/action"
"git.reya.zone/reya/hexmap/server/state"
)
func (x *ClientCommandPB) ToGo() (ClientCommand, error) {
switch msg := x.Command.(type) {
case nil:
return nil, state.ErrOneofNotSet
case *ClientCommandPB_Hello:
return msg.Hello.ToGo(), nil
case *ClientCommandPB_Refresh:
return msg.Refresh.ToGo(), nil
case *ClientCommandPB_Act:
return msg.Act.ToGo()
default:
panic("A case was missed in ClientCommandPB.ToGo!")
}
}
func (x *ClientHelloPB) ToGo() *ClientHello {
return &ClientHello{
Version: x.Version,
}
}
func (c *ClientHello) ToClientPB() *ClientCommandPB {
return &ClientCommandPB{
Command: &ClientCommandPB_Hello{
Hello: &ClientHelloPB{
Version: c.Version,
},
},
}
}
func (*ClientRefreshPB) ToGo() *ClientRefresh {
return &ClientRefresh{}
}
func (c *ClientRefresh) ToClientPB() *ClientCommandPB {
return &ClientCommandPB{
Command: &ClientCommandPB_Refresh{
Refresh: &ClientRefreshPB{},
},
}
}
func (x *ClientActPB_IDed) ToGo() (action.IDed, error) {
act, err := x.Action.ToGo()
if err != nil {
return action.IDed{}, nil
}
return action.IDed{
ID: x.Id,
Action: act,
}, nil
}
func (x *ClientActPB) ToGo() (*ClientAct, error) {
actions := make(IDPairs, len(x.Actions))
for index, ided := range x.Actions {
action, err := ided.ToGo()
if err != nil {
return nil, err
}
actions[index] = action
}
return &ClientAct{
Actions: actions,
}, nil
}
func (c *ClientAct) ToClientPB() *ClientCommandPB {
actions := make([]*ClientActPB_IDed, len(c.Actions))
for index, ided := range c.Actions {
actions[index] = &ClientActPB_IDed{
Id: ided.ID,
Action: ided.Action.ToClientPB(),
}
}
return &ClientCommandPB{
Command: &ClientCommandPB_Act{
Act: &ClientActPB{
Actions: actions,
},
},
}
}
func (*ClientMalformed) ToClientPB() *ClientCommandPB {
return nil
}
func (*SocketClosed) ToClientPB() *ClientCommandPB {
return nil
}

@ -1,276 +0,0 @@
package ws
import (
"git.reya.zone/reya/hexmap/server/action"
"git.reya.zone/reya/hexmap/server/room"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"net/http"
"time"
)
const (
// ReadTimeLimit is the maximum time the server is willing to wait after receiving a message before receiving another one.
ReadTimeLimit = 60 * time.Second
// WriteTimeLimit is the maximum time the server is willing to wait to send a message.
WriteTimeLimit = 10 * time.Second
// ControlTimeLimit is the maximum time the server is willing to wait to send a control message like Ping or Close.
ControlTimeLimit = (WriteTimeLimit * 5) / 10
// PingDelay is the time between pings.
// It must be less than ReadTimeLimit to account for latency and delays on either side.
PingDelay = (ReadTimeLimit * 7) / 10
)
type HTTPHandler struct {
Upgrader websocket.Upgrader
Logger *zap.Logger
Room *room.Client
}
func destroyBadProtocolSocket(c *websocket.Conn, logger *zap.Logger) {
err := c.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseProtocolError, "Invalid subprotocols"), time.Now().Add(ControlTimeLimit))
if err != nil {
logger.Error("Failed to write close message")
}
err = c.SetReadDeadline(time.Now().Add(ControlTimeLimit))
if err != nil {
logger.Error("Failed to set read deadline")
}
for {
_, _, err := c.ReadMessage()
if err != nil {
if !websocket.IsCloseError(err, websocket.CloseProtocolError) {
logger.Error("Websocket connection shut down ignominiously", zap.Error(err))
}
return
}
}
}
func (h *HTTPHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
c, err := h.Upgrader.Upgrade(responseWriter, request, http.Header{})
if err != nil {
h.Logger.Error("Failed to upgrade ws connection", zap.Error(err))
return
}
if c.Subprotocol() == "" {
h.Logger.Error("No matching subprotocol", zap.String("clientProtocols", request.Header.Get("Sec-Websocket-Protocol")))
go destroyBadProtocolSocket(c, h.Logger)
return
}
result := NewConnection(c, h.Logger.Named("Connection"))
exchange(result, h.Logger.Named("Link"), func(o room.NewClientOptions) *room.Client {
return h.Room.NewClient(o)
})
}
func exchange(c *Connection, l *zap.Logger, clientMaker func(options room.NewClientOptions) *room.Client) {
wsr := c.ReadChannel()
wsw := c.WriteChannel()
l.Info("Connection established")
closeWith := &SocketClosed{
Code: websocket.CloseAbnormalClosure,
Text: "I don't know what happened. But goodbye!",
}
defer func() {
l.Info("Shutting down")
// Wait for the websocket connection to shut down.
wsw <- closeWith
if wsr != nil {
for {
msg := <-wsr
switch msg.(type) {
case *SocketClosed:
return
}
}
}
}()
// State 1: Waiting for a hello.
// Anything else is death.
cmd := <-wsr
switch typedCmd := cmd.(type) {
case *ClientHello:
if typedCmd.Version != ProtocolVersion {
l.Warn("Bad Hello version")
// Disgusting. I can't even look at you.
closeWith = &SocketClosed{
Code: websocket.CloseProtocolError,
Text: "Wrong protocol version",
}
return
}
l.Info("Got Hello")
default:
l.Warn("Got NON-hello")
closeWith = &SocketClosed{
Code: websocket.CloseProtocolError,
Text: "You don't even say hello?",
}
return
}
l.Info("Waiting for room.")
// State 2: Waiting for the room to notice us.
rm := clientMaker(room.NewClientOptions{
AcceptBroadcasts: true,
RequestStartingState: true,
})
rmr := rm.IncomingChannel()
rmw := rm.OutgoingChannel()
var leaveWith room.ClientMessage = nil
defer func() {
l.Info("Leaving room")
if leaveWith == nil {
leaveWith = rm.Leave()
}
rmw <- leaveWith
if _, ok := leaveWith.(*room.ShutdownResponse); ok {
// The room was already shutting down.
return
}
for {
msg := <-rmr
switch msg.(type) {
case *room.LeaveResponse:
return
case *room.ShutdownRequest:
rmw <- rm.AcknowledgeShutdown()
return
}
}
}()
l.Info("Waiting for JoinResponse")
msg := <-rmr
switch typedMsg := msg.(type) {
case *room.JoinResponse:
l.Info("Got JoinResponse")
wsw <- &ServerHello{
Version: ProtocolVersion,
State: typedMsg.CurrentState(),
}
case *room.ShutdownRequest:
l.Info("Got ShutdownRequest")
// Room was shutting down when we joined, oops!
closeWith = &SocketClosed{
Code: websocket.CloseGoingAway,
Text: "Shutting down right as you joined. Sorry!",
}
return
default:
l.Info("Got non-JoinResponse/ShutdownRequest")
// Uh. That's concerning. We don't have anything to send our client.
// Let's just give up.
return
}
l.Info("Waiting for messages")
for {
select {
case cmd := <-wsr:
switch typedCmd := cmd.(type) {
case *ClientHello:
l.Info("Got unnecessary ClientHello")
// Huh???
closeWith = &SocketClosed{
Code: websocket.CloseProtocolError,
Text: "Enough hellos. Goodbye.",
}
return
case *ClientRefresh:
l.Info("Got ClientRefresh")
rmw <- rm.Refresh()
case *ClientAct:
l.Info("Got ClientAct")
for _, act := range typedCmd.Actions {
rmw <- rm.Apply(act)
}
case *SocketClosed:
l.Info("Got SocketClosed", zap.Object("close", typedCmd))
closeWith = typedCmd
return
case *ClientMalformed:
l.Warn("Got ClientMalformed")
return
}
case msg := <-rmr:
switch typedMsg := msg.(type) {
case *room.JoinResponse:
// Huh????
l.Info("Got unnecesary JoinResponse")
return
case *room.RefreshResponse:
l.Info("Got RefreshResponse")
wsw <- &ServerRefresh{
State: typedMsg.CurrentState(),
}
case *room.ApplyResponse:
l.Info("Got ApplyResponse")
if typedMsg.Success() {
wsw <- &ServerOK{
IDs: []uint32{typedMsg.ActionID()},
}
} else {
wsw <- &ServerFailed{
IDs: []uint32{typedMsg.ActionID()},
Error: typedMsg.Failure().Error(),
}
}
case *room.ActionBroadcast:
l.Info("Got ActionBroadcast")
wsw <- &ServerAct{
Actions: action.ServerSlice{typedMsg.Action()},
}
case *room.LeaveResponse:
l.Info("Got odd LeaveResponse")
// Oh. u_u I wasn't- okay.
return
case *room.ShutdownRequest:
// Oh. Oh! Okay! Sorry!
l.Info("Got ShutdownRequest")
leaveWith = rm.AcknowledgeShutdown()
return
}
}
}
}
// A Connection corresponds to a pair of actors.
type Connection struct {
r reader
w writer
}
func NewConnection(conn *websocket.Conn, logger *zap.Logger) *Connection {
readChan := make(chan time.Time)
out := &Connection{
r: reader{
conn: conn,
channel: make(chan ClientCommand),
readNotifications: readChan,
logger: logger.Named("reader"),
},
w: writer{
conn: conn,
channel: make(chan ServerCommand),
readNotifications: readChan,
timer: nil,
nextPingAt: time.Time{},
logger: logger.Named("writer"),
},
}
go out.r.act()
go out.w.act()
return out
}
// ReadChannel returns the channel that can be used to read client messages from the connection.
// After receiving SocketClosed, the reader will close its channel.
func (c *Connection) ReadChannel() <-chan ClientCommand {
return c.r.channel
}
// WriteChannel returns the channel that can be used to send server messages on the connection.
// After sending SocketClosed, the writer will close its channel; do not send any further messages on the channel.
func (c *Connection) WriteChannel() chan<- ServerCommand {
return c.w.channel
}

@ -1,123 +0,0 @@
package ws
import (
"fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"time"
)
type reader struct {
// conn is the connection to the client read from by the reader.
conn *websocket.Conn
// channel is the channel that the reader sends messages it has received on.
channel chan ClientCommand
// readNotifications is the channel that alerts are sent to the writer on, to let it know to
readNotifications chan<- time.Time
// logger is the logger used to record the state of the reader, primarily in Debug level.
logger *zap.Logger
}
// InvalidMessageType is placed in ClientMalformed when the type of the WebSocket message was incorrect.
type InvalidMessageType struct {
MessageType int
}
func (i InvalidMessageType) Error() string {
return fmt.Sprintf("invalid message type %d", i.MessageType)
}
// act contains the main read loop of the reader.
func (r *reader) act() {
defer r.shutdown()
// Our first deadline starts immediately, so let the writer know.
r.updateDeadlines()
// The reader should not automatically respond with close messages.
// It should simply let the close message be returned as an error.
r.conn.SetCloseHandler(func(code int, text string) error {
return nil
})
r.conn.SetPongHandler(func(appData string) error {
r.logger.Debug("Received pong, extending read deadline")
r.updateDeadlines()
if appData != PingData {
r.logger.Warn("Got unexpected data in the pong", zap.String("appData", appData))
}
return nil
})
for {
messageType, messageData, err := r.conn.ReadMessage()
if err != nil {
var closure *SocketClosed
if websocket.IsCloseError(err, StandardClientCloseTypes...) {
typedErr := err.(*websocket.CloseError)
r.logger.Debug("Received normal close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text))
closure = &SocketClosed{Code: typedErr.Code, Text: typedErr.Text}
} else if websocket.IsUnexpectedCloseError(err, StandardClientCloseTypes...) {
typedErr := err.(*websocket.CloseError)
r.logger.Warn("Received unexpected close message, shutting down", zap.Int("code", typedErr.Code), zap.String("text", typedErr.Text))
closure = &SocketClosed{Code: typedErr.Code, Text: typedErr.Text}
} else {
r.logger.Error("Error while reading message, shutting down", zap.Error(err))
closure = &SocketClosed{Error: err}
}
r.logger.Debug("Sending close message to reader", zap.Object("closeMessage", closure))
r.channel <- closure
// We must exit now - errors from this method are permanent, after all.
// We'll do the shutdown we deferred.
return
}
r.updateDeadlines()
r.channel <- r.parseCommand(messageType, messageData)
}
}
// parseCommand attempts to parse the incoming message
func (r *reader) parseCommand(socketType int, data []byte) ClientCommand {
if socketType != websocket.BinaryMessage {
err := &InvalidMessageType{
MessageType: socketType,
}
r.logger.Error("Received command with unknown WebSocket message type", zap.Error(err))
return &ClientMalformed{
Error: err,
}
}
r.logger.Debug("Received command, parsing")
var cmdPb ClientCommandPB
err := proto.Unmarshal(data, &cmdPb)
if err != nil {
return &ClientMalformed{
Error: err,
}
}
cmd, err := (&cmdPb).ToGo()
if err != nil {
return &ClientMalformed{Error: err}
}
return cmd
}
// updateDeadlines extends the time limit for pongs, and instructs the writer to hold off on sending a ping for the next PingDelay.
func (r *reader) updateDeadlines() {
receivedAt := time.Now()
r.logger.Debug("Alerting writer to extend ping timer", zap.Time("receivedAt", receivedAt))
r.readNotifications <- receivedAt
newDeadline := receivedAt.Add(ReadTimeLimit)
r.logger.Debug("Extending read deadline", zap.Time("newDeadline", newDeadline))
err := r.conn.SetReadDeadline(newDeadline)
if err != nil {
r.logger.Error("Error while extending read deadline", zap.Error(err))
}
r.logger.Debug("Read deadline extended")
}
// shutdown closes all resources associated with the reader (the channel and readNotifications) but leaves its conn running.
func (r *reader) shutdown() {
close(r.channel)
r.channel = nil
close(r.readNotifications)
r.readNotifications = nil
r.conn = nil
}

@ -1,93 +0,0 @@
package ws
import (
"git.reya.zone/reya/hexmap/server/action"
"git.reya.zone/reya/hexmap/server/state"
"go.uber.org/zap/zapcore"
)
// ServerMessageType is an enum type for the server's messages.
type ServerMessageType string
// ServerCommand s are sent by the server to the client.
type ServerCommand interface {
zapcore.ObjectMarshaler
// ToServerPB converts the command to a server protocol buffer which will be sent on the wire.
ToServerPB() *ServerCommandPB
}
// ServerHello is the command sent to establish the current state of the server when a new client connects.
type ServerHello struct {
// Version is the protocol version the server is running.
Version uint32 `json:"version"`
// State is the complete state of the server as of when the client joined.
State *state.Synced `json:"state"`
}
func (s ServerHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "Hello")
encoder.AddUint32("version", s.Version)
return encoder.AddObject("state", s.State)
}
// ServerRefresh is the command sent to reestablish the current state of the server in response to ClientRefresh.
type ServerRefresh struct {
// State is the complete state of the server as of when the corresponding ClientRefresh was processed.
State *state.Synced `json:"state"`
}
func (s ServerRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "Refresh")
return encoder.AddObject("state", s.State)
}
type IDSlice []uint32
func (i IDSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
for _, v := range i {
encoder.AppendUint32(v)
}
return nil
}
// ServerOK is the command sent when one or more client actions have been accepted and applied.
type ServerOK struct {
// IDs contains the IDs of the actions which were accepted and applied, in the order they were accepted and applied.
// This is the same as the order they were received, though other actions may have been between these that were
// rejected.
IDs IDSlice `json:"ids"`
}
func (s ServerOK) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "OK")
return encoder.AddArray("ids", s.IDs)
}
// ServerFailed is the command sent when one or more client actions have been rejected.
type ServerFailed struct {
// IDs contains the IDs of the actions which were rejected, in the order they were rejected.
// This is the same as the order they were received, though other actions may have been between these that were
// accepted and applied.
IDs IDSlice `json:"ids"`
// Error contains the error text sent from the server about why these actions failed.
Error string `json:"error"`
}
func (s ServerFailed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "Failed")
err := encoder.AddArray("ids", s.IDs)
encoder.AddString("error", s.Error)
return err
}
// ServerAct is the command sent when one or more client actions from other clients have been accepted and applied.
// The client's own actions will never be included in this command.
type ServerAct struct {
// Actions contains the actions that are now being applied.
Actions action.ServerSlice `json:"actions"`
}
func (s ServerAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "Act")
return encoder.AddArray("actions", s.Actions)
}

@ -1,136 +0,0 @@
package ws
import "git.reya.zone/reya/hexmap/server/action"
func (x *ServerCommandPB) ToGo() (ServerCommand, error) {
switch msg := x.Command.(type) {
case *ServerCommandPB_Hello:
return msg.Hello.ToGo()
case *ServerCommandPB_Refresh:
return msg.Refresh.ToGo()
case *ServerCommandPB_Ok:
return msg.Ok.ToGo(), nil
case *ServerCommandPB_Failed:
return msg.Failed.ToGo(), nil
case *ServerCommandPB_Act:
return msg.Act.ToGo()
default:
panic("A case was missed in ServerCommandPB.ToGo!")
}
}
func (x *ServerHelloPB) ToGo() (*ServerHello, error) {
state, err := x.State.ToGo()
if err != nil {
return nil, err
}
return &ServerHello{
Version: x.Version,
State: &state,
}, nil
}
func (s *ServerHello) ToServerPB() *ServerCommandPB {
return &ServerCommandPB{
Command: &ServerCommandPB_Hello{
Hello: &ServerHelloPB{
Version: s.Version,
State: s.State.ToPB(),
},
},
}
}
func (x *ServerRefreshPB) ToGo() (*ServerRefresh, error) {
state, err := x.State.ToGo()
if err != nil {
return nil, err
}
return &ServerRefresh{
State: &state,
}, nil
}
func (s *ServerRefresh) ToServerPB() *ServerCommandPB {
return &ServerCommandPB{
Command: &ServerCommandPB_Refresh{
Refresh: &ServerRefreshPB{
State: s.State.ToPB(),
},
},
}
}
func (x *ServerOKPB) ToGo() *ServerOK {
ids := make(IDSlice, len(x.Ids))
copy(ids, x.Ids)
return &ServerOK{
IDs: ids,
}
}
func (s *ServerOK) ToServerPB() *ServerCommandPB {
ids := make([]uint32, len(s.IDs))
copy(ids, s.IDs)
return &ServerCommandPB{
Command: &ServerCommandPB_Ok{
Ok: &ServerOKPB{
Ids: ids,
},
},
}
}
func (x *ServerFailedPB) ToGo() *ServerFailed {
ids := make(IDSlice, len(x.Ids))
copy(ids, x.Ids)
return &ServerFailed{
IDs: ids,
Error: x.Error,
}
}
func (s *ServerFailed) ToServerPB() *ServerCommandPB {
ids := make([]uint32, len(s.IDs))
copy(ids, s.IDs)
return &ServerCommandPB{
Command: &ServerCommandPB_Failed{
Failed: &ServerFailedPB{
Ids: ids,
Error: s.Error,
},
},
}
}
func (x *ServerActPB) ToGo() (*ServerAct, error) {
actions := make(action.ServerSlice, len(x.Actions))
for index, act := range x.Actions {
convertedAct, err := act.ToGo()
if err != nil {
return nil, err
}
actions[index] = convertedAct
}
return &ServerAct{
Actions: actions,
}, nil
}
func (s *ServerAct) ToServerPB() *ServerCommandPB {
actions := make([]*action.ServerActionPB, len(s.Actions))
for index, act := range s.Actions {
actions[index] = act.ToServerPB()
}
return &ServerCommandPB{
Command: &ServerCommandPB_Act{
Act: &ServerActPB{
Actions: actions,
},
},
}
}
func (*SocketClosed) ToServerPB() *ServerCommandPB {
return nil
}

@ -1,37 +0,0 @@
package ws
import (
"github.com/gorilla/websocket"
"go.uber.org/zap/zapcore"
)
const (
PingData string = "are you still there?"
ProtocolVersion uint32 = 1
)
var StandardClientCloseTypes = []int{websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure}
// SocketClosed is synthesized when a client closes the WebSocket connection, or sent to the write process to write a
// WebSocket close message.
// Sending a SocketClosed on a channel causes that channel to be closed right after.
type SocketClosed struct {
// Code is the status code given (or which should be given) in the close message.
Code int `json:"code"`
// Text is the reason text given (or which should be given) in the close message. Max 123 characters.
Text string `json:"text"`
// Error may be an error that resulted in the closure of the socket. It will be set only if the connection was cut
// by a condition other than receiving a close message; if it is set, Code and Text will both be zeroed.
// Will not be written by the writer; only useful when it's returned from the reader.
Error error `json:"error"`
}
func (c SocketClosed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "GOODBYE")
encoder.AddInt("code", c.Code)
encoder.AddString("text", c.Text)
if c.Error != nil {
encoder.AddString("error", c.Error.Error())
}
return nil
}

@ -1,197 +0,0 @@
package ws
import (
"github.com/gorilla/websocket"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"io"
"time"
)
type writer struct {
// conn is the ws connection that this writer is responsible for writing on.
conn *websocket.Conn
// channel is the channel used to receive server messages to be sent to the client.
// When it receives a SocketClosed, the process on the sending end promises not to send any further messages, as the writer will close it right after.
channel chan ServerCommand
// readNotifications is the channel used to receive pings when the reader receives a message, so that a ping will be sent out before the reader is ready to time out.
// When it is closed, the reader has shut down.
readNotifications <-chan time.Time
// timer is the timer used to send pings when the reader is close to timing out, to make sure the other end of the connection is still listening.
timer *time.Timer
// nextPingAt is the time after which the next ping will be sent if the timer is ticking. It will be .IsZero() if the timer is not ticking.
nextPingAt time.Time
// logger is the logger used to record the state of the writer, primarily in Debug level.
logger *zap.Logger
}
// IsTimerTicking returns true if the writer's timer is running.
func (w *writer) isTimerTicking() bool {
return !w.nextPingAt.IsZero()
}
// sendNextPingAt starts the timer if it's not running, and
func (w *writer) sendNextPingAt(nextPing time.Time) {
if w.nextPingAt.IsZero() {
// Timer is not running, so set the next ping time and start it.
w.nextPingAt = nextPing
w.timer.Reset(time.Until(nextPing))
} else if w.nextPingAt.Before(nextPing) {
// Timer's already running, so leave it be, but update the next ping time.
w.nextPingAt = nextPing
} else {
// The timer is already set to a time after the incoming time.
// It's extremely unlikely for this empty branch to ever be reached.
}
}
// act is the function responsible for actually doing the writing.
func (w *writer) act() {
defer w.gracefulShutdown()
w.logger.Debug("Starting up")
w.timer = time.NewTimer(PingDelay)
w.nextPingAt = time.Now().Add(PingDelay)
for {
select {
case readAt, open := <-w.readNotifications:
if open {
nextPingAt := readAt.Add(PingDelay)
w.logger.Debug("Received reader read, extending ping timer", zap.Time("nextPingAt", nextPingAt))
w.sendNextPingAt(nextPingAt)
} else {
w.logger.Debug("Received reader close, shutting down")
w.readNotifications = nil
// bye bye, we'll graceful shutdown because we deferred it
return
}
case raw := <-w.channel:
switch msg := raw.(type) {
case *SocketClosed:
w.logger.Debug("Received close message, forwarding and shutting down", zap.Object("msg", msg))
w.sendClose(msg)
// bye bye, we'll graceful shutdown because we deferred it
return
default:
w.logger.Debug("Received message, forwarding", zap.Object("msg", msg))
w.send(msg)
}
case <-w.timer.C:
now := time.Now()
if now.After(w.nextPingAt) {
// We successfully passed the time when a ping should be sent! Let's send it!
w.sendPing()
// The timer doesn't need to be reactivated right now, so just zero out the next ping time.
w.nextPingAt = time.Time{}
} else {
// It's not time to send the ping yet - we got more reads in the meantime. Restart the timer with the new time-until-next-ping.
w.timer.Reset(w.nextPingAt.Sub(now))
}
}
w.logger.Debug("Awakening handled, resuming listening")
}
}
// send actually transmits a ServerCommand to the client according to the protocol.
func (w *writer) send(msg ServerCommand) {
w.logger.Debug("Marshaling command as protobuf", zap.Object("msg", msg))
marshaled, err := proto.Marshal(msg.ToServerPB())
if err != nil {
w.logger.Error("Error while marshaling to protobuf", zap.Error(err))
return
}
writeDeadline := time.Now().Add(WriteTimeLimit)
w.logger.Debug("Setting deadline to write command", zap.Time("writeDeadline", writeDeadline))
err = w.conn.SetWriteDeadline(writeDeadline)
if err != nil {
w.logger.Error("Error while setting write deadline", zap.Time("writeDeadline", writeDeadline), zap.Object("msg", msg), zap.Error(err))
}
w.logger.Debug("Opening message writer to send command", zap.Object("msg", msg))
writer, err := w.conn.NextWriter(websocket.BinaryMessage)
if err != nil {
w.logger.Error("Error while getting writer from connection", zap.Error(err))
return
}
defer func(writer io.WriteCloser) {
w.logger.Debug("Closing message writer to send command")
err := writer.Close()
if err != nil {
w.logger.Error("Error while closing writer to send command", zap.Error(err))
}
w.logger.Debug("Command sent")
}(writer)
_, err = writer.Write(marshaled)
if err != nil {
w.logger.Error("Error while writing marshaled protobuf to connection", zap.Error(err))
return
}
// Deferred close happens now
}
// sendClose sends a close message on the ws connection, but does not actually close the connection.
// It does, however, close the incoming message channel to the writer.
func (w *writer) sendClose(msg *SocketClosed) {
w.logger.Debug("Shutting down the writer channel")
close(w.channel)
w.channel = nil
w.logger.Debug("Writing close message")
err := w.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(msg.Code, msg.Text), time.Now().Add(ControlTimeLimit))
if err != nil {
w.logger.Warn("Error while sending close", zap.Error(err))
}
}
// sendPing sends a ping message on the ws connection. The content is arbitrary.
func (w *writer) sendPing() {
w.logger.Debug("Sending ping")
err := w.conn.WriteControl(websocket.PingMessage, []byte(PingData), time.Now().Add(ControlTimeLimit))
if err != nil {
w.logger.Error("Error while sending ping", zap.Error(err))
}
}
// gracefulShutdown causes the writer to wait for the close handshake to finish and then shut down.
// It waits for the reader's readNotifications to close, indicating that it has also shut down, and for the channel to
// receive a SocketClosed message indicating that the main process has shut down.
// During this time, the writer ignores all other messages from the channel and sends no pings.
func (w *writer) gracefulShutdown() {
defer w.finalShutdown()
// If the ping timer is still running, stop it and then close it.
if w.isTimerTicking() && !w.timer.Stop() {
<-w.timer.C
}
w.timer = nil
w.nextPingAt = time.Time{}
w.logger.Debug("Waiting for all channels to shut down")
for {
if w.channel == nil && w.readNotifications == nil {
w.logger.Debug("All channels closed, beginning final shutdown")
// all done, we outta here, let the defer pick up the final shutdown
return
}
select {
case _, open := <-w.readNotifications:
if !open {
w.logger.Debug("Received reader close while shutting down")
w.readNotifications = nil
}
case raw := <-w.channel:
switch msg := raw.(type) {
case *SocketClosed:
w.logger.Debug("Received close message from channel while shutting down, forwarding", zap.Object("msg", msg))
w.sendClose(msg)
default:
w.logger.Debug("Ignoring non-close message while shutting down", zap.Object("msg", msg))
}
}
}
}
// finalShutdown closes the socket and finishes cleanup.
func (w *writer) finalShutdown() {
w.logger.Debug("Closing WebSocket connection")
err := w.conn.Close()
if err != nil {
w.logger.Error("Received an error while closing", zap.Error(err))
}
w.logger.Debug("Shut down")
}

@ -0,0 +1,3 @@
import {EntryPoint} from "./ui/EntryPoint";
EntryPoint();

@ -1,15 +1,15 @@
import {Dispatch, useMemo, useReducer} from "react";
import {appStateReducer} from "./reducers/AppStateReducer";
import {AppState} from "./state/AppState";
import {ServerConnectionState} from "./state/NetworkState";
import {sizeFromLinesAndCells} from "./state/Coordinates";
import {AppAction} from "./actions/AppAction";
import {USER_ACTIVE_COLOR} from "./actions/UserAction";
import HexColorPicker from "./ui/HexColorPicker";
import HexMapRenderer from "./ui/HexMapRenderer";
import {DispatchContext} from "./ui/context/DispatchContext";
import {appStateReducer} from "../reducers/AppStateReducer";
import {AppState} from "../state/AppState";
import {ServerConnectionState} from "../state/NetworkState";
import {sizeFromLinesAndCells} from "../state/Coordinates";
import {AppAction} from "../actions/AppAction";
import {USER_ACTIVE_COLOR} from "../actions/UserAction";
import HexColorPicker from "./HexColorPicker";
import HexMapRenderer from "./HexMapRenderer";
import {DispatchContext} from "./context/DispatchContext";
import "./App.css";
import {WebsocketReactAdapter} from "./websocket/WebsocketReactAdapter";
import {WebsocketReactAdapter} from "./WebsocketReactAdapter";
function App() {
const defaultState: AppState = {

@ -10,13 +10,13 @@ import {
ServerSocketStartupAction,
SocketState
} from "../actions/ServerAction";
import {clientToPb} from "./ClientToPb";
import {clientToPb} from "../pbconv/ClientToPb";
import {ClientCommandPB} from "../proto/client";
import {Reader} from "protobufjs";
import {ServerCommandPB} from "../proto/server";
import {serverFromPb} from "./ServerFromPb";
import {serverFromPb} from "../pbconv/ServerFromPb";
export class WebsocketTranslator {
export class DOMWebsocketTranslator {
readonly url: string
readonly protocols: readonly string[]
readonly onStartup: (startup: ServerSocketStartupAction) => void

@ -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…
Cancel
Save