Compare commits

...

3 Commits

  1. 1
      .gitignore
  2. 8
      .idea/.gitignore
  3. 21
      .idea/hexmap.iml
  4. 8
      .idea/modules.xml
  5. 17
      .idea/protoeditor.xml
  6. 6
      .idea/vcs.xml
  7. 41
      build/build.go
  8. 3
      client/.gitignore
  9. 109
      client/magefile.go
  10. 314
      client/package-lock.json
  11. 1
      client/package.json
  12. 12
      client/src/App.css
  13. 6
      client/src/App.tsx
  14. 8
      client/src/actions/NetworkAction.ts
  15. 4
      client/src/reducers/HexMapReducer.ts
  16. 18
      client/src/state/Coordinates.ts
  17. 176
      client/src/ui/debug/ConsoleConnection.tsx
  18. 4
      client/src/util/ArrayUtils.ts
  19. 12
      go.mod
  20. 27
      go.sum
  21. 12
      mage.sh
  22. 32
      magefile.go
  23. 27
      proto/action.proto
  24. 28
      proto/client.proto
  25. 8
      proto/coords.proto
  26. 56
      proto/magefile.go
  27. 37
      proto/map.proto
  28. 38
      proto/server.proto
  29. 11
      proto/state.proto
  30. 7
      proto/user.proto
  31. 2
      server/.gitignore
  32. 112
      server/action/action.go
  33. 75
      server/action/action.pbconv.go
  34. 45
      server/action/map.go
  35. 43
      server/action/syncable.go
  36. 37
      server/action/user.go
  37. 9
      server/go.mod
  38. 68
      server/magefile.go
  39. 12
      server/room/actor.go
  40. 6
      server/room/client.go
  41. 2
      server/room/clientmessage.go
  42. 10
      server/room/message.go
  43. 46
      server/state/color.go
  44. 16
      server/state/color.pbconv.go
  45. 4
      server/state/coords.go
  46. 25
      server/state/coords.pbconv.go
  47. 104
      server/state/hexcolor.go
  48. 51
      server/state/hexorientation.go
  49. 49
      server/state/lineparity.go
  50. 80
      server/state/map.go
  51. 142
      server/state/map.pbconv.go
  52. 4
      server/state/state.go
  53. 20
      server/state/state.pbconv.go
  54. 16
      server/state/user.go
  55. 13
      server/state/user.pbconv.go
  56. 66
      server/websocket/client.go
  57. 96
      server/websocket/client.pbconv.go
  58. 4
      server/websocket/connection.go
  59. 114
      server/websocket/reader.go
  60. 69
      server/websocket/server.go
  61. 136
      server/websocket/server.pbconv.go
  62. 29
      server/websocket/shared.go
  63. 106
      server/websocket/writer.go

1
.gitignore vendored

@ -0,0 +1 @@
/buildtools

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true">
<buildTags>
<option name="customFlags">
<array>
<option value="mage" />
</array>
</option>
</buildTags>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/mage" />
<excludeFolder url="file://$MODULE_DIR$/buildtools" />
<excludeFolder url="file://$MODULE_DIR$/client/src/proto" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/hexmap.iml" filepath="$PROJECT_DIR$/.idea/hexmap.iml" />
</modules>
</component>
</project>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProtobufLanguageSettings">
<option name="autoConfigEnabled" value="false" />
<option name="importPathEntries">
<list>
<ImportPathEntry>
<option name="location" value="jar://$APPLICATION_PLUGINS_DIR$/protobuf-editor.jar!/include" />
</ImportPathEntry>
<ImportPathEntry>
<option name="location" value="file://$PROJECT_DIR$/proto" />
</ImportPathEntry>
</list>
</option>
<option name="descriptorPath" value="google/protobuf/descriptor.proto" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,41 @@
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()
}

3
client/.gitignore vendored

@ -11,6 +11,9 @@
# production
/build
# Generated protocol buffers files
/src/proto
# misc
.DS_Store
.env.local

@ -0,0 +1,109 @@
package client
import (
"context"
"git.reya.zone/reya/hexmap/build"
"git.reya.zone/reya/hexmap/proto"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/target"
"os"
"os/exec"
"path/filepath"
)
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

@ -20,6 +20,7 @@
"react-color": "^2.19.3",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"ts-proto": "^1.82.2",
"typescript": "^4.3.5",
"web-vitals": "^1.1.2"
}
@ -2735,6 +2736,60 @@
"node": ">= 8"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78="
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik="
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0="
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q="
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz",
@ -3536,6 +3591,11 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
},
"node_modules/@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
},
"node_modules/@types/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz",
@ -3551,6 +3611,11 @@
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA=="
},
"node_modules/@types/object-hash": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-1.3.4.tgz",
"integrity": "sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -6713,6 +6778,11 @@
"node": ">=10"
}
},
"node_modules/dataloader": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
},
"node_modules/debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
@ -13132,6 +13202,11 @@
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -13922,6 +13997,14 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
"integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==",
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
@ -15873,6 +15956,17 @@
"node": ">=0.10.0"
}
},
"node_modules/prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -15998,6 +16092,36 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/protobufjs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz",
"integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==",
"hasInstallScript": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
}
},
"node_modules/protobufjs/node_modules/@types/node": {
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz",
"integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -19320,6 +19444,46 @@
}
}
},
"node_modules/ts-poet": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-4.5.0.tgz",
"integrity": "sha512-Vs2Zsiz3zf5qdFulFTIEpaLdgWeHXKh+4pv+ycVqEh+ZuUOVGrN0i9lbxVx7DB1FBogExytz3OuaBMJfWffpSQ==",
"dependencies": {
"@types/prettier": "^1.19.0",
"lodash": "^4.17.15",
"prettier": "^2.0.2"
}
},
"node_modules/ts-poet/node_modules/@types/prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ=="
},
"node_modules/ts-proto": {
"version": "1.82.2",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.2.tgz",
"integrity": "sha512-Hceptz4oCnUTeGYbGepNNDg7drYOdv0M+m3LSv8V5hehsilsx5Ca8bhUAYGcQad+CDvbeUDM6PrzW4GGfJXi9A==",
"dependencies": {
"@types/object-hash": "^1.3.0",
"dataloader": "^1.4.0",
"object-hash": "^1.3.1",
"protobufjs": "^6.8.8",
"ts-poet": "^4.5.0",
"ts-proto-descriptors": "^1.2.1"
},
"bin": {
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
}
},
"node_modules/ts-proto-descriptors": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz",
"integrity": "sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw==",
"dependencies": {
"long": "^4.0.0",
"protobufjs": "^6.8.8"
}
},
"node_modules/tsconfig-paths": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz",
@ -23559,6 +23723,60 @@
}
}
},
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78="
},
"@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A="
},
"@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=",
"requires": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E="
},
"@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik="
},
"@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0="
},
"@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q="
},
"@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@rollup/plugin-node-resolve": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz",
@ -24147,6 +24365,11 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
},
"@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
},
"@types/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz",
@ -24162,6 +24385,11 @@
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA=="
},
"@types/object-hash": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-1.3.4.tgz",
"integrity": "sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA=="
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -26695,6 +26923,11 @@
"whatwg-url": "^8.0.0"
}
},
"dataloader": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz",
"integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw=="
},
"debug": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
@ -31501,6 +31734,11 @@
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
"integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw=="
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -32141,6 +32379,11 @@
}
}
},
"object-hash": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
"integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA=="
},
"object-inspect": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
@ -33677,6 +33920,11 @@
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
},
"prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ=="
},
"pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -33779,6 +34027,33 @@
}
}
},
"protobufjs": {
"version": "6.11.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz",
"integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"dependencies": {
"@types/node": {
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz",
"integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw=="
}
}
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -36409,6 +36684,45 @@
"resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz",
"integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw=="
},
"ts-poet": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-4.5.0.tgz",
"integrity": "sha512-Vs2Zsiz3zf5qdFulFTIEpaLdgWeHXKh+4pv+ycVqEh+ZuUOVGrN0i9lbxVx7DB1FBogExytz3OuaBMJfWffpSQ==",
"requires": {
"@types/prettier": "^1.19.0",
"lodash": "^4.17.15",
"prettier": "^2.0.2"
},
"dependencies": {
"@types/prettier": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ=="
}
}
},
"ts-proto": {
"version": "1.82.2",
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.2.tgz",
"integrity": "sha512-Hceptz4oCnUTeGYbGepNNDg7drYOdv0M+m3LSv8V5hehsilsx5Ca8bhUAYGcQad+CDvbeUDM6PrzW4GGfJXi9A==",
"requires": {
"@types/object-hash": "^1.3.0",
"dataloader": "^1.4.0",
"object-hash": "^1.3.1",
"protobufjs": "^6.8.8",
"ts-poet": "^4.5.0",
"ts-proto-descriptors": "^1.2.1"
}
},
"ts-proto-descriptors": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz",
"integrity": "sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw==",
"requires": {
"long": "^4.0.0",
"protobufjs": "^6.8.8"
}
},
"tsconfig-paths": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.10.1.tgz",

@ -15,6 +15,7 @@
"react-color": "^2.19.3",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"ts-proto": "^1.82.2",
"typescript": "^4.3.5",
"web-vitals": "^1.1.2"
},

@ -3,7 +3,7 @@ body {
background-color: #282c34;
}
.App, .scrollbox {
.App, .scrollBox {
position: absolute;
top: 0;
bottom: 0;
@ -11,11 +11,11 @@ body {
right: 0;
}
.scrollbox {
.scrollBox {
overflow-x: scroll;
overflow-y: scroll;
}
.centerbox {
.centerBox {
display: flex;
align-items: center;
justify-content: center;
@ -72,7 +72,7 @@ body {
.hexColorPicker {
width: 30vw;
}
.centerbox {
.centerBox {
padding-bottom: calc(2vh + (30vw * 126 / 855));
}
}
@ -80,7 +80,7 @@ body {
.hexColorPicker {
width: 300px;
}
.centerbox {
.centerBox {
padding-bottom: calc(2vh + (300px * 126 / 855));
}
}
@ -88,7 +88,7 @@ body {
.hexColorPicker {
width: 98vw;
}
.centerbox {
.centerBox {
padding-bottom: calc(2vh + (98vw * 126 / 855));
}
}

@ -7,7 +7,6 @@ import {ServerConnectionState} from "./state/NetworkState";
import HexMapRenderer from "./ui/HexMapRenderer";
import HexColorPicker from "./ui/HexColorPicker";
import {sizeFromLinesAndCells} from "./state/Coordinates";
import {ConsoleConnector} from "./ui/debug/ConsoleConnection";
function App() {
const [state, dispatch] = useReducer<AppStateReducer, null>(
@ -48,15 +47,14 @@ function App() {
return (
<div className="App">
<DispatchContext.Provider value={dispatch}>
<div className={"scrollbox"}>
<div className={"centerbox"}>
<div className={"scrollBox"}>
<div className={"centerBox"}>
<svg className={"map"} width={width} height={height} viewBox={`0 0 ${width} ${height}`} onContextMenu={(e) => e.preventDefault()}>
{mapElement}
</svg>
</div>
</div>
{colorPickerElement}
<ConsoleConnector specialMessage={state.network.specialMessage} pendingMessages={state.network.pendingActions} nextID={state.network.nextID} />
</DispatchContext.Provider>
</div>
);

@ -60,11 +60,6 @@ export function isServerOKAction(action: AppAction): action is ServerOKAction {
return action.type === SERVER_OK
}
export interface ActionFailure {
readonly id: number
readonly error: string
}
export const SERVER_FAILED = "SERVER_FAILED"
/** Sent in response to the client's ClientNestedAction, if it fails and has not been applied to the server map. */
export interface ServerFailedAction extends BaseAction {
@ -103,9 +98,6 @@ export function isServerActAction(action: AppAction): action is ServerActAction
}
export type SyncableAction = SendableAction
export function isSyncableAction(action: AppAction): action is SyncableAction {
return isSendableAction(action)
}
export type ServerAction =
ServerHelloAction | ServerGoodbyeAction | ServerRefreshAction |

@ -40,6 +40,4 @@ export function hexMapReducer(oldState: HexMap, action: MapAction): HexMap {
case CELL_REMOVE:
return replaceCell(oldState, action.at, null)
}
}
export type HexMapReducer = typeof hexMapReducer;
}

@ -1,23 +1,5 @@
/** Cubic (3-dimensional) coordinates for algorithms. */
import {HexagonOrientation, HexMapRepresentation, LineParity} from "./HexMap";
export interface CubicCoordinates {
/** The cubic x-coordinate. */
readonly x: number
/** The cubic y-coordinate. */
readonly y: number
/** The cubic z-coordinate. */
readonly z: number
}
/** Axial (2-dimensional cube variant) coordinates for display. */
export interface AxialCoordinates {
/** The axial x-coordinate. */
readonly q: number
/** The axial y-coordinate. */
readonly r: number
}
/** Staggered (storage) coordinates for accessing cell storage. */
export interface StorageCoordinates {
/** The index of the line within the map. */

@ -1,176 +0,0 @@
import {
CLIENT_ACT,
ClientAction,
ClientActAction,
isClientActAction,
SendableAction,
SentAction,
SERVER_FAILED,
SERVER_GOODBYE,
SERVER_HELLO,
SERVER_OK,
SERVER_ACT,
SERVER_SOCKET_STARTUP,
ServerAction,
SocketState
} from "../../actions/NetworkAction";
import {HexagonOrientation, HexMapRepresentation, initializeMap, LineParity} from "../../state/HexMap";
import {ReactElement, useContext, useEffect, useRef, useState} from "react";
import {DispatchContext} from "../context/DispatchContext";
import {USER_ACTIVE_COLOR} from "../../actions/UserAction";
import {CELL_COLOR} from "../../actions/CellAction";
export enum OrientationConstants {
ROWS = "ROWS",
COLUMNS = "COLUMNS",
EVEN_ROWS = "EVEN_ROWS",
EVEN_COLUMNS = "EVEN_COLUMNS"
}
export function orientationFromString(string: string): HexMapRepresentation {
const normalized = string.toUpperCase().trim()
switch (normalized) {
case OrientationConstants.ROWS:
return { orientation: HexagonOrientation.POINTY_TOP, indentedLines: LineParity.ODD }
case OrientationConstants.COLUMNS:
return { orientation: HexagonOrientation.FLAT_TOP, indentedLines: LineParity.ODD }
case OrientationConstants.EVEN_ROWS:
return { orientation: HexagonOrientation.POINTY_TOP, indentedLines: LineParity.EVEN }
case OrientationConstants.EVEN_COLUMNS:
return { orientation: HexagonOrientation.FLAT_TOP, indentedLines: LineParity.EVEN }
default:
return { orientation: HexagonOrientation.POINTY_TOP, indentedLines: LineParity.ODD }
}
}
/** Fake "connection" to a "server" that actually just goes back and forth with the console. */
export class ConsoleConnection {
public receivedMessages: ClientAction[] = []
private readonly dispatch: (action: ServerAction) => void
constructor(dispatch: (action: ServerAction) => void) {
this.dispatch = dispatch
}
receive(action: ClientAction): void {
this.receivedMessages.push(action)
if (isClientActAction(action)) {
console.log(`Received Sent action containing: ${action.actions.map((value) => `${value.id}/${value.action.type}`).join(", ")}`)
} else {
console.log(`Received: ${action.type}`)
}
}
public sendSocketConnecting(): void {
this.dispatch({
type: SERVER_SOCKET_STARTUP,
state: SocketState.CONNECTING
})
}
public sendSocketConnected(): void {
this.dispatch({
type: SERVER_SOCKET_STARTUP,
state: SocketState.OPEN
})
}
public sendHello({color = "#0000FF", displayMode = "ROWS", xid = "TotallyCoolXID", lines = 10, cells = 10}: {
color?: string,
displayMode?: string,
xid?: string,
lines?: number,
cells?: number
} = {}): void {
this.dispatch({
type: SERVER_HELLO,
version: 1,
state: {
map: initializeMap({
lines,
cellsPerLine: cells,
displayMode: orientationFromString(displayMode),
xid
}),
user: {activeColor: color}
}
})
}
public sendOK(ids: readonly number[]): void {
this.dispatch({
type: SERVER_OK,
ids
})
}
public sendFailed(ids: readonly number[], error: string = "No thanks."): void {
this.dispatch({
type: SERVER_FAILED,
ids,
error
})
}
public sendGoodbye({code = 1000, reason = "Okay, bye then!"}: { code?: number, reason?: string } = {}): void {
this.dispatch({
type: SERVER_GOODBYE,
code,
reason,
currentTime: new Date()
})
}
public sendColorChange(color: string = "#FF0000FF"): void {
this.dispatch({
type: SERVER_ACT,
actions: [{
type: USER_ACTIVE_COLOR,
color
}]
})
}
public sendColorAtTile(color: string = "#FFFF00FF", line: number = 0, cell: number = 0): void {
this.dispatch({
type: SERVER_ACT,
actions: [{
type: CELL_COLOR,
at: { line, cell },
color
}]
})
}
}
export function ConsoleConnector({specialMessage, pendingMessages, nextID}: {specialMessage: ClientAction|null, pendingMessages: readonly SendableAction[], nextID: number}): ReactElement {
const dispatch = useContext(DispatchContext)
const connector = useRef(new ConsoleConnection(dispatch || (() => null)))
const [lastSpecialMessage, setLastSpecialMessage] = useState<ClientAction|null>(null)
useEffect(() => {
// @ts-ignore
window.fakedServerConnection = connector.current
}, []);
useEffect(() => {
if (dispatch !== null) {
if (pendingMessages.length > 0) {
const sentMessages: SentAction[] = pendingMessages.map((action, index) => {
return { id: index + nextID, action }
});
const sentMessage: ClientActAction = {
type: CLIENT_ACT,
actions: sentMessages
};
connector.current.receive(sentMessage)
dispatch(sentMessage)
}
}
}, [nextID, dispatch, pendingMessages])
useEffect(() => {
if (specialMessage !== null && specialMessage !== lastSpecialMessage) {
connector.current.receive(specialMessage);
setLastSpecialMessage(specialMessage);
}
}, [specialMessage, lastSpecialMessage, setLastSpecialMessage])
return <div className="consoleConnector">Console connection active</div>
}

@ -1,7 +1,3 @@
export function arrayShallowEqual<T>(left: readonly T[], right: readonly T[]): boolean {
return left.length === right.length && arrayShallowStartsWith(left, right)
}
export function arrayShallowStartsWith<T>(target: readonly T[], prefix: readonly T[]): boolean {
return target.length >= prefix.length && prefix.every((value, index) => target[index] === value)
}

@ -0,0 +1,12 @@
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
)

@ -3,11 +3,16 @@ github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
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=
@ -19,6 +24,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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=
@ -28,17 +34,36 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
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 h1:Yq9t9jnGoR+dBuitxdo9l6Q7xh/zOyNnYUtDKaQ3x0E=
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=

@ -0,0 +1,12 @@
#!/bin/bash
SCRIPTPATH=$(readlink -e ${BASH_SOURCE})
MAINPATH=${SCRIPTPATH%/*}
BUILDTOOLSPATH=${MAINPATH}/buildtools
MAGEPATH=${BUILDTOOLSPATH}/mage
if [[ ! -x "$MAGEPATH" ]]; then
echo "go install-ing mage..."
GOBIN="$BUILDTOOLSPATH" go install github.com/magefile/mage@latest
fi
exec "$MAGEPATH" -d "$MAINPATH" -w "$MAINPATH" "$@"

@ -0,0 +1,32 @@
// +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
}

@ -0,0 +1,27 @@
syntax = "proto3";
import "coords.proto";
option go_package = "git.reya.zone/reya/hexmap/server/action";
message CellSetColorPB {
fixed32 color = 1;
StorageCoordinatesPB at = 2;
}
message UserSetActiveColorPB {
fixed32 color = 1;
}
message ClientActionPB {
oneof action {
CellSetColorPB cell_set_color = 1;
UserSetActiveColorPB user_set_active_color = 2;
}
}
message ServerActionPB {
oneof action {
ClientActionPB client = 1;
}
}

@ -0,0 +1,28 @@
syntax = "proto3";
import "action.proto";
option go_package = "git.reya.zone/reya/hexmap/server/websocket";
message ClientHelloPB {
uint32 version = 1;
}
message ClientRefreshPB {
}
message ClientActPB {
message IDed {
uint32 id = 1;
ClientActionPB action = 2;
}
repeated IDed actions = 1;
}
message ClientCommandPB {
oneof command {
ClientHelloPB hello = 1;
ClientRefreshPB refresh = 2;
ClientActPB act = 3;
}
}

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

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

@ -0,0 +1,37 @@
syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
message HexCellPB {
fixed32 color = 1;
}
message HexLinePB {
repeated HexCellPB cells = 1;
}
message HexLayerPB {
repeated HexLinePB lines = 1;
}
message HexMapPB {
message Layout {
enum Orientation {
UNKNOWN_ORIENTATION = 0;
POINTY_TOP = 1;
FLAT_TOP = 2;
}
enum LineParity {
UNKNOWN_LINE = 0;
ODD = 1;
EVEN = 2;
}
Orientation orientation = 1;
LineParity indented_lines = 2;
}
bytes xid = 1;
uint32 lines = 2;
uint32 cells_per_line = 3;
Layout layout = 4;
HexLayerPB layer = 5;
}

@ -0,0 +1,38 @@
syntax = "proto3";
import "action.proto";
import "state.proto";
option go_package = "git.reya.zone/reya/hexmap/server/websocket";
message ServerHelloPB {
uint32 version = 1;
SyncableStatePB state = 2;
}
message ServerRefreshPB {
SyncableStatePB state = 1;
}
message ServerOKPB {
repeated uint32 ids = 1;
}
message ServerFailedPB {
repeated uint32 ids = 1;
string error = 2;
}
message ServerActPB {
repeated ServerActionPB actions = 1;
}
message ServerCommandPB {
oneof command {
ServerHelloPB hello = 1;
ServerRefreshPB refresh = 2;
ServerOKPB ok = 3;
ServerFailedPB failed = 4;
ServerActPB act = 5;
}
}

@ -0,0 +1,11 @@
syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
import "map.proto";
import "user.proto";
message SyncableStatePB {
HexMapPB map = 1;
UserStatePB user = 2;
}

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

2
server/.gitignore vendored

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

@ -0,0 +1,112 @@
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
}

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

@ -1,45 +0,0 @@
package action
import (
"git.reya.zone/reya/hexmap/server/state"
"go.uber.org/zap/zapcore"
)
const (
CellColorType SyncableType = "CELL_COLOR"
)
// CellColor is the action sent when a cell of the map has been colored a different color.
type CellColor struct {
// At is the location of the cell in storage coordinates.
At state.StorageCoordinates `json:"at"`
// Color is the color the cell has been changed to.
Color state.HexColor `json:"color"`
}
func (c CellColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(CellColorType))
err := encoder.AddObject("at", c.At)
encoder.AddString("color", c.Color.String())
return err
}
func (c CellColor) Type() SyncableType {
return CellColorType
}
// Apply sets the target cell's color, or returns ErrNoOp if it can't.
func (c CellColor) Apply(s *state.Synced) error {
if c.Color.A < 0xF {
return ErrNoTransparentColors
}
cell, err := s.Map.LineCells.GetCellAt(c.At)
if err != nil {
return err
}
if cell.Color == c.Color {
return ErrNoOp
}
cell.Color = c.Color
return nil
}

@ -1,43 +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")
)
type SyncableType string
// Syncable is the interface for actions that can be shared between clients.
type Syncable interface {
zapcore.ObjectMarshaler
// Type gives the Javascript type that is sent over the wire.
Type() SyncableType
// Apply causes the action's effects to be applied to s, mutating it in place.
// All syncable.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 SyncableSlice []Syncable
func (s SyncableSlice) 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
}

@ -1,37 +0,0 @@
package action
import (
"git.reya.zone/reya/hexmap/server/state"
"go.uber.org/zap/zapcore"
)
const (
UserActiveColorType = "USER_ACTIVE_COLOR"
)
// UserActiveColor is the action sent when the user's current color, the one being painted with, changes.
type UserActiveColor struct {
// Color is the color that is now active.
Color state.HexColor `json:"color"`
}
func (c UserActiveColor) Type() SyncableType {
return UserActiveColorType
}
func (c UserActiveColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("color", c.Color.String())
return nil
}
// Apply sets the user's active color, or returns ErrNoOp if it can't.
func (c UserActiveColor) Apply(s *state.Synced) error {
if c.Color.A < 0xF {
return ErrNoTransparentColors
}
if s.User.ActiveColor == c.Color {
return ErrNoOp
}
s.User.ActiveColor = c.Color
return nil
}

@ -1,9 +0,0 @@
module git.reya.zone/reya/hexmap/server
go 1.16
require (
github.com/gorilla/websocket v1.4.2
github.com/rs/xid v1.3.0
go.uber.org/zap v1.18.1
)

@ -0,0 +1,68 @@
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
})
}

@ -24,7 +24,7 @@ func (r *room) act() {
case RefreshRequest:
r.sendRefresh(msg.id)
case ApplyRequest:
msgLogger.Debug("Received action to apply from client", zap.Int("actionId", msg.action.ID))
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)
@ -120,14 +120,14 @@ func (r *room) sendRefresh(id xid.ID) {
}
// applyAction applies an action to the state and returns the result of it.
func (r *room) applyAction(action action.Syncable) error {
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 int, action action.Syncable) {
logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Int("actionID", originalActionID), zap.Object("action", action))
func (r *room) broadcastAction(originalClientID xid.ID, originalActionID uint32, action action.Action) {
logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Uint32("actionID", originalActionID), zap.Object("action", action))
broadcast := ActionBroadcast{
id: r.id,
originalClientID: originalClientID,
@ -144,8 +144,8 @@ func (r *room) broadcastAction(originalClientID xid.ID, originalActionID int, ac
}
// acknowledgeAction sends a response to the original client which requested an action.
func (r *room) acknowledgeAction(id xid.ID, actionID int, error error) {
logger := r.logger.With(zap.Stringer("id", id), zap.Int("actionId", actionID), zap.Error(error))
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 {

@ -110,7 +110,7 @@ func (c *Client) NewClient(opts NewClientOptions) *Client {
return newClientForRoom(c.roomId, c.outgoingChannel, opts)
}
// The message created by Refresh causes the client to request a fresh copy of the state.
// 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")
@ -120,7 +120,7 @@ func (c *Client) Refresh() RefreshRequest {
}
}
// The message created by Leave causes the local client to signal that it is shutting down.
// 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.
@ -135,7 +135,7 @@ func (c *Client) Leave() LeaveRequest {
}
}
// The message created by Stop causes the local client to signal that it is shutting down.
// 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.

@ -9,7 +9,7 @@ import (
// ClientMessage marks messages coming from clients to the room.
type ClientMessage interface {
zapcore.ObjectMarshaler
// SourceID is the id of the client sending the message.
// ClientID is the id of the client sending the message.
ClientID() xid.ID
}

@ -63,7 +63,7 @@ func (r RefreshResponse) CurrentState() *state.Synced {
type ApplyResponse struct {
id xid.ID
// actionID is the ID of the action that completed or failed.
actionID int
actionID uint32
// result is nil if the action was completed, or an error if it failed.
result error
}
@ -71,7 +71,7 @@ type ApplyResponse struct {
func (a ApplyResponse) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", "ApplyResponse")
encoder.AddString("id", a.id.String())
encoder.AddInt("actionId", a.actionID)
encoder.AddUint32("actionId", a.actionID)
encoder.AddBool("success", a.result == nil)
if a.result != nil {
encoder.AddString("failure", a.result.Error())
@ -99,16 +99,16 @@ type ActionBroadcast struct {
// 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 int
originalActionID uint32
// action is the action that succeeded.
action action.Syncable
action action.Action
}
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.AddInt("originalActionId", a.originalActionID)
encoder.AddUint32("originalActionId", a.originalActionID)
return encoder.AddObject("action", a.action)
}

@ -0,0 +1,46 @@
package state
import "fmt"
// Color is an internal representation of a hexadecimal color string.
// It can take one of these formats:
// #RRGGBBAA
// #RRGGBB
// #RGBA
// #RGB
// When marshaling, it will always choose the most efficient format, but any format can be used when unmarshaling.
type Color struct {
// R is the red component of the color.
R uint8
// G is the green component of the color.
G uint8
// B is the blue component of the color.
B uint8
// A is the alpha component of the color.
A uint8
}
// String prints the Color as an abbreviated notation.
// Specifically, short form is used when possible (i.e., when all components are evenly divisible by 0x11).
// The alpha component is left out if it's 0xFF.
func (c Color) String() string {
if c.R%0x11 == 0 && c.G%0x11 == 0 && c.B%0x11 == 0 && c.A%0x11 == 0 {
// Short form works.
if c.A == 0xFF {
// It's great when it's easy!
return fmt.Sprintf("#%01X%01X%01X", c.R/0x11, c.G/0x11, c.B/0x11)
} else {
// Just need to add the alpha.
return fmt.Sprintf("#%01X%01X%01X%01X", c.R/0x11, c.G/0x11, c.B/0x11, c.A/0x11)
}
} else {
// Gotta use long form.
if c.A == 0xFF {
// Can skip the alpha channel, though.
return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
} else {
// Doing things the hard way.
return fmt.Sprintf("#%02X%02X%02X%02X", c.R, c.G, c.B, c.A)
}
}
}

@ -0,0 +1,16 @@
package state
// ColorFromInt decodes a packed uint32 into a hex color.
func ColorFromInt(value uint32) Color {
return Color{
R: uint8((value >> 24) & 0xFF),
G: uint8((value >> 16) & 0xFF),
B: uint8((value >> 8) & 0xFF),
A: uint8((value >> 0) & 0xFF),
}
}
// ToInt packs a hex color into a uint32.
func (c Color) ToInt() uint32 {
return uint32(c.R)<<24 | uint32(c.G)<<16 | uint32(c.B)<<8 | uint32(c.A)
}

@ -1,6 +1,8 @@
package state
import "go.uber.org/zap/zapcore"
import (
"go.uber.org/zap/zapcore"
)
// StorageCoordinates gives the coordinates of a cell in a form optimized for storage.
type StorageCoordinates struct {

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

@ -1,104 +0,0 @@
package state
import "fmt"
// HexColor is an internal representation of a hexadecimal color string.
// It can take one of these formats:
// #RRGGBBAA
// #RRGGBB
// #RGBA
// #RGB
// When marshaling, it will always choose the most efficient format, but any format can be used when unmarshaling.
type HexColor struct {
// R is the red component of the color.
R uint8
// G is the green component of the color.
G uint8
// B is the blue component of the color.
B uint8
// A is the alpha component of the color.
A uint8
}
// HexColorFromString decodes a hexadecimal string into a hex color.
func HexColorFromString(text string) (HexColor, error) {
var hex HexColor
if err := (&hex).UnmarshalText([]byte(text)); err != nil {
return hex, err
}
return hex, nil
}
func (h *HexColor) UnmarshalText(text []byte) error {
var count, expected int
var short bool
var scanErr error
switch len(text) {
case 9:
// Long form with alpha: #RRGGBBAA
expected = 4
short = false
count, scanErr = fmt.Sscanf(string(text), "#%02X%02X%02X%02X", &h.R, &h.G, &h.B, &h.A)
case 7:
// Long form: #RRGGBB
expected = 3
h.A = 0xFF
count, scanErr = fmt.Sscanf(string(text), "#%02X%02X%02X", &h.R, &h.G, &h.B)
case 5:
// Short form with alpha: #RGBA
expected = 4
short = true
count, scanErr = fmt.Sscanf(string(text), "#%01X%01X%01X%01X", &h.R, &h.G, &h.B, &h.A)
case 4:
// Short form: #RGB
expected = 3
h.A = 0xF
short = true
count, scanErr = fmt.Sscanf(string(text), "#%01X%01X%01X", &h.R, &h.G, &h.B)
default:
return fmt.Errorf("can't decode %s as HexColor: wrong length", text)
}
if scanErr != nil {
return scanErr
}
if count != expected {
return fmt.Errorf("can't decode %s as HexColor: missing components", text)
}
if short {
h.R *= 0x11
h.G *= 0x11
h.B *= 0x11
h.A *= 0x11
}
return nil
}
// MarshalText marshals the HexColor into a small string.
func (h HexColor) MarshalText() (text []byte, err error) {
return []byte(h.String()), nil
}
// String prints the HexColor as an abbreviated notation.
// Specifically, short form is used when possible (i.e., when all components are evenly divisible by 0x11).
// The alpha component is left out if it's 0xFF.
func (h HexColor) String() string {
if h.R%0x11 == 0 && h.G%0x11 == 0 && h.B%0x11 == 0 && h.A%0x11 == 0 {
// Short form works.
if h.A == 0xFF {
// It's great when it's easy!
return fmt.Sprintf("#%01X%01X%01X", h.R/0x11, h.G/0x11, h.B/0x11)
} else {
// Just need to add the alpha.
return fmt.Sprintf("#%01X%01X%01X%01X", h.R/0x11, h.G/0x11, h.B/0x11, h.A/0x11)
}
} else {
// Gotta use long form.
if h.A == 0xFF {
// Can skip the alpha channel, though.
return fmt.Sprintf("#%02X%02X%02X", h.R, h.G, h.B)
} else {
// Doing things the hard way.
return fmt.Sprintf("#%02X%02X%02X%02X", h.R, h.G, h.B, h.A)
}
}
}

@ -1,51 +0,0 @@
package state
import "fmt"
// HexOrientation is the enum for the direction hexes are facing.
type HexOrientation uint8
const (
// PointyTop indicates hexes that have a pair of sides on either side in the horizontal direction,
// and points on the top and bottom in the vertical direction.
PointyTop HexOrientation = 1
// FlatTop indicates hexes that have a pair of points on either side in the horizontal direction,
// and sides on the top and bottom in the vertical direction.
FlatTop HexOrientation = 2
)
// UnmarshalText unmarshals from the equivalent Javascript constant name.
func (o *HexOrientation) UnmarshalText(text []byte) error {
switch string(text) {
case "POINTY_TOP":
*o = PointyTop
return nil
case "FLAT_TOP":
*o = FlatTop
return nil
default:
return fmt.Errorf("can't unmarshal unknown HexOrientation %s", text)
}
}
// MarshalText marshals into the equivalent JavaScript constant name.
func (o HexOrientation) MarshalText() (text []byte, err error) {
switch o {
case PointyTop, FlatTop:
return []byte(o.String()), nil
default:
return nil, fmt.Errorf("can't marshal unknown HexOrientation %d", o)
}
}
// String returns the equivalent JavaScript constant name.
func (o HexOrientation) String() string {
switch o {
case PointyTop:
return "POINTY_TOP"
case FlatTop:
return "FLAT_TOP"
default:
return fmt.Sprintf("[unknown HexOrientation %d]", o)
}
}

@ -1,49 +0,0 @@
package state
import "fmt"
// LineParity indicates whether odd or even lines are indented.
type LineParity uint8
const (
// OddLines indicates that odd lines - 1, 3, 5... - are indented by 1/2 cell.
OddLines LineParity = 1
// EvenLines indicates that even lines - 0, 2, 4... - are indented by 1/2 cell.
EvenLines LineParity = 2
)
// UnmarshalText unmarshals from the equivalent Javascript constant name.
func (o *LineParity) UnmarshalText(text []byte) error {
switch string(text) {
case "ODD":
*o = OddLines
return nil
case "EVEN":
*o = EvenLines
return nil
default:
return fmt.Errorf("can't unmarshal unknown LineParity %s", text)
}
}
// MarshalText marshals into the equivalent JavaScript constant name.
func (o LineParity) MarshalText() (text []byte, err error) {
switch o {
case OddLines, EvenLines:
return []byte(o.String()), nil
default:
return nil, fmt.Errorf("can't marshal unknown LineParity %d", o)
}
}
// String returns the equivalent JavaScript constant name.
func (o LineParity) String() string {
switch o {
case OddLines:
return "ODD_LINES"
case EvenLines:
return "EVEN_LINES"
default:
return fmt.Sprintf("[unknown LineParity %d]", o)
}
}

@ -6,22 +6,72 @@ import (
"go.uber.org/zap/zapcore"
)
// HexMapRepresentation combines HexOrientation and LineParity to represent a map's display mode.
type HexMapRepresentation struct {
// 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 (h HexMapRepresentation) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("orientation", h.Orientation.String())
encoder.AddString("indentedLines", h.IndentedLines.String())
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 HexColor `json:"color"`
Color Color `json:"color"`
}
func (h HexCell) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
@ -98,20 +148,20 @@ type HexMap struct {
// 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"`
// DisplayMode is the orientation and line parity used to display the map.
DisplayMode HexMapRepresentation `json:"displayMode"`
// LineCells contains the actual map data.
// LineCells itself is a slice with Lines elements, each of which is a line;
// Layout is the orientation and line parity used to display the map.
Layout Layout `json:"displayMode"`
// 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.
LineCells HexLayer `json:"lineCells"`
Layer HexLayer `json:"lineCells"`
}
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("displayMode", m.DisplayMode)
lineCellsErr := encoder.AddArray("lineCells", m.LineCells)
displayModeErr := encoder.AddObject("displayMode", m.Layout)
lineCellsErr := encoder.AddArray("lineCells", m.Layer)
if displayModeErr != nil {
return displayModeErr
} else {
@ -125,7 +175,7 @@ func (m HexMap) Copy() HexMap {
XID: m.XID,
Lines: m.Lines,
CellsPerLine: m.CellsPerLine,
DisplayMode: m.DisplayMode,
LineCells: m.LineCells.Copy(),
Layout: m.Layout,
Layer: m.Layer.Copy(),
}
}

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

@ -6,8 +6,8 @@ import (
// Synced contains all state that is synced between the server and its clients.
type Synced struct {
Map HexMap `json:"map"`
User UserData `json:"user"`
Map HexMap `json:"map"`
User UserState `json:"user"`
}
func (s *Synced) MarshalLogObject(encoder zapcore.ObjectEncoder) error {

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

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

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

@ -5,62 +5,47 @@ import (
"go.uber.org/zap/zapcore"
)
// ClientMessageType is an enum type for the client's protocol messages.
type ClientMessageType string
// ClientCommandType is an enum type for the client's protocol messages.
type ClientCommandType string
const (
ClientHelloType ClientMessageType = "HELLO"
ClientRefreshType ClientMessageType = "REFRESH"
ClientActType ClientMessageType = "ACT"
ClientGoodbyeType ClientMessageType = GoodbyeType
)
// ClientMessage s are those sent by the client.
type ClientMessage interface {
// ClientCommand s are those sent by the client.
type ClientCommand interface {
zapcore.ObjectMarshaler
// ClientType gives the type constant that will be sent on or read from the wire.
ClientType() ClientMessageType
// ToClientPB converts the command to a client protocol buffer which will be sent on the wire.
ToClientPB() *ClientCommandPB
}
// ClientHello is the action sent by the client when it first establishes the connection.
// 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 int `json:"version"`
}
func (c ClientHello) ClientType() ClientMessageType {
return ClientHelloType
Version uint32 `json:"version"`
}
func (c ClientHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(ClientHelloType))
encoder.AddInt("version", c.Version)
encoder.AddString("type", "Hello")
encoder.AddUint32("version", c.Version)
return nil
}
// ClientRefresh is the action sent by the client when it needs the full state re-sent.
// ClientRefresh is the command sent by the client when it needs the full state re-sent.
type ClientRefresh struct {
}
func (c ClientRefresh) ClientType() ClientMessageType {
return ClientRefreshType
}
func (c ClientRefresh) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(ClientRefreshType))
encoder.AddString("type", "Refresh")
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 int `json:"id"`
ID uint32 `json:"id"`
// Action contains the action that was actually being sent.
Action action.Syncable `json:"action"`
Action action.Client `json:"action"`
}
func (i IDed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddInt("id", i.ID)
encoder.AddUint32("id", i.ID)
return encoder.AddObject("action", i.Action)
}
@ -77,17 +62,26 @@ func (a IDPairs) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
return finalErr
}
// ClientAct is an action sent in order to deliver one or more Syncable actions to the server.
// 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) ClientType() ClientMessageType {
return ClientActType
}
func (c ClientAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(ClientActType))
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
}

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

@ -26,12 +26,12 @@ type Connection struct {
// 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 ClientMessage {
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<- ServerMessage {
func (c *Connection) WriteChannel() chan<- ServerCommand {
return c.w.channel
}

@ -1,14 +1,118 @@
package websocket
import (
"fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"time"
)
// Todo: Listen for pongs and extend the read deadline every time you get one
type reader struct {
conn *websocket.Conn
channel chan ClientMessage
readNotifications chan<- time.Duration
// 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()
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
}

@ -9,65 +9,48 @@ import (
// ServerMessageType is an enum type for the server's messages.
type ServerMessageType string
const (
ServerHelloType ServerMessageType = "HELLO"
ServerRefreshType ServerMessageType = "REFRESH"
ServerOKType ServerMessageType = "OK"
ServerFailedType ServerMessageType = "FAILED"
ServerActType ServerMessageType = "ACT"
ServerGoodbyeType ServerMessageType = GoodbyeType
)
// ServerMessage s are sent by the server to the client.
type ServerMessage interface {
// ServerCommand s are sent by the server to the client.
type ServerCommand interface {
zapcore.ObjectMarshaler
// ServerType returns the type constant that will be sent on the wire.
ServerType() ServerMessageType
// ToServerPB converts the command to a server protocol buffer which will be sent on the wire.
ToServerPB() *ServerCommandPB
}
// ServerHello is the action sent to establish the current state of the server when a new client connects.
// 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 int `json:"version"`
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", string(ServerHelloType))
encoder.AddInt("version", s.Version)
encoder.AddString("type", "Hello")
encoder.AddUint32("version", s.Version)
return encoder.AddObject("state", s.State)
}
func (s ServerHello) ServerType() ServerMessageType {
return ServerHelloType
}
// ServerRefresh is the action sent to reestablish the current state of the server in response to ClientRefresh.
// 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", string(ServerRefreshType))
encoder.AddString("type", "Refresh")
return encoder.AddObject("state", s.State)
}
func (s ServerRefresh) ServerType() ServerMessageType {
return ServerRefreshType
}
type IDSlice []int
type IDSlice []uint32
func (i IDSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
for _, v := range i {
encoder.AppendInt(v)
encoder.AppendUint32(v)
}
return nil
}
// ServerOK is the action sent when one or more client actions have been accepted and applied.
// 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
@ -76,15 +59,11 @@ type ServerOK struct {
}
func (s ServerOK) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(ServerOKType))
encoder.AddString("type", "OK")
return encoder.AddArray("ids", s.IDs)
}
func (s ServerOK) ServerType() ServerMessageType {
return ServerOKType
}
// ServerFailed is the action sent when one or more client actions have been rejected.
// 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
@ -95,28 +74,20 @@ type ServerFailed struct {
}
func (s ServerFailed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(ServerFailedType))
encoder.AddString("type", "Failed")
err := encoder.AddArray("ids", s.IDs)
encoder.AddString("error", s.Error)
return err
}
func (s ServerFailed) ServerType() ServerMessageType {
return ServerFailedType
}
// ServerAct is the action 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 action.
// 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.SyncableSlice `json:"actions"`
Actions action.ServerSlice `json:"actions"`
}
func (s ServerAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(ServerActType))
encoder.AddString("type", "Act")
return encoder.AddArray("actions", s.Actions)
}
func (s ServerAct) ServerType() ServerMessageType {
return ServerActType
}

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

@ -1,29 +1,28 @@
package websocket
import "go.uber.org/zap/zapcore"
// StatusCode is the code used by the WebSocket protocol to signal the other side on close.
type StatusCode int16
import (
"github.com/gorilla/websocket"
"go.uber.org/zap/zapcore"
)
const (
StatusNormal StatusCode = 1000
StatusGoingAway StatusCode = 1001
StatusProtocolError StatusCode = 1002
StatusTooBig StatusCode = 1009
StatusProtocolVersionOutOfDate StatusCode = 4000
PingData string = "are you still there?"
GoodbyeType = "GOODBYE"
)
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 StatusCode given (or which should be given) in the close message.
Code StatusCode `json:"code"`
// 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.
// 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"`
}
@ -38,10 +37,6 @@ func (c SocketClosed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return nil
}
func (c SocketClosed) ClientType() ClientMessageType {
return ClientGoodbyeType
}
func (c SocketClosed) ServerType() ServerMessageType {
return ServerGoodbyeType
return GoodbyeType
}

@ -1,10 +1,9 @@
package websocket
import (
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"io"
"time"
)
@ -14,31 +13,51 @@ type writer struct {
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 ServerMessage
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.Duration
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 _, open := <-w.readNotifications:
case readAt, open := <-w.readNotifications:
if open {
w.logger.Debug("Received reader read, extending ping")
if !w.timer.Stop() {
// The timer went off while we were doing this, so drain the channel before resetting
<-w.timer.C
}
w.timer.Reset(PingDelay)
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
@ -57,62 +76,74 @@ func (w *writer) act() {
w.send(msg)
}
case <-w.timer.C:
w.sendPing()
w.timer.Reset(PingDelay)
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")
}
}
func (w *writer) send(msg ServerMessage) {
// 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.TextMessage)
if err != nil {
w.logger.Error("error while getting writer from connection", zap.Error(err))
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 message", zap.Error(err))
w.logger.Error("Error while closing writer to send command", zap.Error(err))
}
w.logger.Debug("Command sent")
}(writer)
payload, err := json.Marshal(msg)
_, err = writer.Write(marshaled)
if err != nil {
w.logger.Error("error while rendering message payload to JSON", zap.Error(err))
w.logger.Error("Error while writing marshaled protobuf to connection", zap.Error(err))
return
}
if len(payload) == 2 {
// This is an empty JSON message. We can leave it out.
_, err = fmt.Fprintf(writer, "%s!", msg.ServerType())
if err != nil {
w.logger.Error("error while writing command-only message", zap.Error(err))
}
} else {
// Because we need to send this, we put in a space instead of an exclamation mark.
_, err = fmt.Fprintf(writer, "%s %s", msg.ServerType(), payload)
if err != nil {
w.logger.Error("error while writing command-only message", zap.Error(err))
}
}
// Deferred close happens now
}
// sendClose sends a close message on the websocket connection, but does not actually close the connection.
// It does, however, close the incoming message channel.
// 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(int(msg.Code), msg.Text), time.Now().Add(ControlTimeLimit))
err := w.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(msg.Code, msg.Text), time.Now().Add(ControlTimeLimit))
if err != nil {
w.logger.Error("Error while sending close", zap.Error(err))
w.logger.Warn("Error while sending close", zap.Error(err))
}
}
// sendPing sends a ping message on the websocket connection. The content is arbitrary.
func (w *writer) sendPing() {
w.logger.Debug("Sending ping")
err := w.conn.WriteControl(websocket.PingMessage, []byte("are you still there?"), time.Now().Add(ControlTimeLimit))
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))
}
@ -124,7 +155,12 @@ func (w *writer) sendPing() {
// 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 {

Loading…
Cancel
Save