Commit while working on mage stuff.

main
Mari 3 years ago
parent 41f882190f
commit f5f3427656
  1. 1
      .gitignore
  2. 8
      .idea/.gitignore
  3. 19
      .idea/hexmap.iml
  4. 8
      .idea/modules.xml
  5. 17
      .idea/protoeditor.xml
  6. 6
      .idea/vcs.xml
  7. 51
      build/build.go
  8. 5
      client/magefile.go
  9. 314
      client/package-lock.json
  10. 1
      client/package.json
  11. 12
      go.mod
  12. 27
      go.sum
  13. 12
      mage.sh
  14. 8
      magefile.go
  15. 14
      proto/action.proto
  16. 31
      proto/client.proto
  17. 8
      proto/coords.proto
  18. 33
      proto/map.proto
  19. 41
      proto/server.proto
  20. 7
      proto/user.proto
  21. 2
      server/.gitignore
  22. 20
      server/action/action.go
  23. 4
      server/action/map.go
  24. 6
      server/action/user.go
  25. 9
      server/go.mod
  26. 76
      server/magefile.go
  27. 4
      server/room/actor.go
  28. 2
      server/room/message.go
  29. 50
      server/websocket/client.go
  30. 4
      server/websocket/connection.go
  31. 175
      server/websocket/reader.go
  32. 18
      server/websocket/server.go
  33. 25
      server/websocket/shared.go
  34. 86
      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,19 @@
<?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" />
</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,51 @@
package build
import (
"context"
"github.com/magefile/mage/mg"
"os"
"os/exec"
"path/filepath"
)
func GetBuildToolsDir() (string, error) {
basedir, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Join(basedir, "buildtools"), nil
}
func HasExecutableInBuildtools(name string) (bool, error) {
tooldir, err := GetBuildToolsDir()
if err != nil {
return false, err
}
info, err := os.Stat(filepath.Join(tooldir, 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 := GetBuildToolsDir()
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()
}

@ -0,0 +1,5 @@
// +build mage
package client
const ProtocPluginPath = "client/node_modules/.bin/protoc-gen-ts_proto"

@ -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"
},

@ -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
MAINPATH=$(readlink -e ${BASH_SOURCE%/*})
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,8 @@
// +build mage
package main
import (
// mage:import
_ "git.reya.zone/reya/hexmap/server"
)

@ -0,0 +1,14 @@
syntax = "proto3";
import "coords.proto";
option go_package = "git.reya.zone/reya/hexmap/server/action";
message CellSetColor {
fixed32 color = 1;
StorageCoordinates at = 2;
}
message UserSetActiveColor {
fixed32 color = 1;
}

@ -0,0 +1,31 @@
syntax = "proto3";
import "action.proto";
option go_package = "git.reya.zone/reya/hexmap/server/websocket";
message ClientHello {
uint32 version = 1;
}
message ClientRefresh {
}
message ClientAct {
message ClientAction {
uint32 id = 1;
oneof action {
CellSetColor cell_set_color = 2;
UserSetActiveColor user_set_active_color = 3;
}
}
repeated ClientAction actions = 1;
}
message ClientCommand {
oneof command {
ClientHello hello = 1;
ClientRefresh refresh = 2;
ClientAct act = 3;
}
}

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

@ -0,0 +1,33 @@
syntax = "proto3";
option go_package = "git.reya.zone/reya/hexmap/server/state";
message HexCell {
fixed32 color = 1;
}
message HexLine {
repeated HexCell cells = 1;
}
message HexMap {
message Layout {
enum Orientation {
UNKNOWN_ORIENTATION = 0;
FLAT_TOP_ORIENTATION = 1;
POINTY_TOP_ORIENTATION = 2;
}
enum LineParity {
UNKNOWN_LINE_PARITY = 0;
EVEN_LINE_PARITY = 1;
ODD_LINE_PARITY = 2;
}
Orientation orientation = 1;
LineParity line_parity = 2;
}
string xid = 1;
uint32 lines = 2;
uint32 cells_per_line = 3;
Layout layout = 4;
repeated HexLine line_cells = 5;
}

@ -0,0 +1,41 @@
syntax = "proto3";
import "action.proto";
import "map.proto";
import "user.proto";
option go_package = "git.reya.zone/reya/hexmap/server/websocket";
message ServerState {
HexMap map = 1;
UserState user = 2;
}
message ServerHello {
uint32 version = 1;
ServerState state = 2;
}
message ServerRefresh {
ServerState state = 1;
}
message ServerOK {
repeated uint32 ids = 1;
}
message ServerFailed {
repeated uint32 ids = 1;
string error = 2;
}
message ServerAction {
oneof action {
CellSetColor cell_set_color = 1;
UserSetActiveColor user_set_active_color = 2;
}
}
message ServerAct {
repeated ServerAction actions = 1;
}

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

2
server/.gitignore vendored

@ -0,0 +1,2 @@
*.pb.go
*.zap.go

@ -14,24 +14,30 @@ var (
ErrNoTransparentColors = errors.New("transparent colors not allowed")
)
type SyncableType string
type Type string
// Syncable is the interface for actions that can be shared between clients.
type Syncable interface {
// Action is the interface for actions that can be shared between clients, or between the server and a client.
type Action interface {
zapcore.ObjectMarshaler
// Type gives the Javascript type that is sent over the wire.
Type() SyncableType
Type() Type
// 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
// 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
// fromJSONMap causes the action's state to be overwritten by data from the given map.
fromJSONMap(data map[string] interface{}) error
}
type SyncableSlice []Syncable
type parseAction struct {
Type string `json:"type"`
}
type Slice []Action
func (s SyncableSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
func (s Slice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
var finalErr error = nil
for _, a := range s {
err := encoder.AppendObject(a)

@ -6,7 +6,7 @@ import (
)
const (
CellColorType SyncableType = "CELL_COLOR"
CellColorType Type = "CELL_COLOR"
)
// CellColor is the action sent when a cell of the map has been colored a different color.
@ -24,7 +24,7 @@ func (c CellColor) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return err
}
func (c CellColor) Type() SyncableType {
func (c CellColor) Type() Type {
return CellColorType
}

@ -15,10 +15,6 @@ type UserActiveColor struct {
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
@ -34,4 +30,4 @@ func (c UserActiveColor) Apply(s *state.Synced) error {
}
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,76 @@
package server
import (
"context"
"fmt"
"git.reya.zone/reya/hexmap/build"
"github.com/magefile/mage/mg"
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
)
func InstallZapMarshaler(ctx context.Context) error {
alreadyDone, err := build.HasExecutableInBuildtools("protoc-gen-zap-marshaler")
if err != nil {
return err
}
if alreadyDone {
return nil
}
return build.InstallGoExecutable(ctx, "github.com/kazegusuri/go-proto-zap-marshaler/protoc-gen-zap-marshaler@latest")
}
func InstallGoProtoc(ctx context.Context) error {
alreadyDone, err := build.HasExecutableInBuildtools("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 InstallProtoDeps(ctx context.Context) {
mg.CtxDeps(ctx, InstallZapMarshaler, InstallGoProtoc)
}
type Protobuf mg.Namespace
func (Protobuf) BuildProtocolBuffers(ctx context.Context) error {
mg.CtxDeps(ctx, InstallProtoDeps)
tooldir, err := build.GetBuildToolsDir()
if err != nil {
return err
}
pluginPathFlag := fmt.Sprintf("--plugin=%s", path.Join(tooldir, "protoc-gen-go"))
cmd := exec.CommandContext(ctx, "protoc", pluginPathFlag, "-I=proto", "--go_out=.", "--go_opt=module=git.reya.zone/reya/hexmap", "proto/action.proto", "proto/client.proto", "proto/coords.proto", "proto/map.proto", "proto/server.proto", "proto/user.proto")
cmd.Stderr = os.Stderr
return cmd.Run()
}
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
})
}

@ -120,13 +120,13 @@ 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) {
func (r *room) broadcastAction(originalClientID xid.ID, originalActionID int, action action.Action) {
logger := r.logger.With(zap.Stringer("originalClient", originalClientID), zap.Int("actionID", originalActionID), zap.Object("action", action))
broadcast := ActionBroadcast{
id: r.id,

@ -101,7 +101,7 @@ type ActionBroadcast struct {
// originalActionID is the ID that the client that sent the action sent.
originalActionID int
// action is the action that succeeded.
action action.Syncable
action action.Action
}
func (a ActionBroadcast) MarshalLogObject(encoder zapcore.ObjectEncoder) error {

@ -5,30 +5,31 @@ 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
ClientHelloType ClientCommandType = "HELLO"
ClientRefreshType ClientCommandType = "REFRESH"
ClientActType ClientCommandType = "ACT"
ClientGoodbyeType ClientCommandType = GoodbyeType
ClientMalformedCommandType ClientCommandType = "(malformed command)"
)
// 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
ClientType() ClientCommandType
}
// 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 {
func (c ClientHello) ClientType() ClientCommandType {
return ClientHelloType
}
@ -38,11 +39,11 @@ func (c ClientHello) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
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 {
func (c ClientRefresh) ClientType() ClientCommandType {
return ClientRefreshType
}
@ -56,7 +57,7 @@ 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"`
// Action contains the action that was actually being sent.
Action action.Syncable `json:"action"`
Action action.Action `json:"action"`
}
func (i IDed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
@ -77,13 +78,13 @@ 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 {
func (c ClientAct) ClientType() ClientCommandType {
return ClientActType
}
@ -91,3 +92,20 @@ func (c ClientAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
encoder.AddString("type", string(ClientActType))
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", string(ClientMalformedCommandType))
encoder.AddString("error", c.Error.Error())
return nil
}
func (c ClientMalformed) ClientType() ClientCommandType {
return ClientMalformedCommandType
}

@ -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,181 @@
package websocket
import (
"bytes"
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"time"
)
// Todo: Listen for pongs and extend the read deadline every time you get one
type reader struct {
// conn is the connection to the client read from by the reader.
conn *websocket.Conn
channel chan ClientMessage
readNotifications chan<- time.Duration
// 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)
}
// InvalidCommandType is placed in ClientMalformed when the type of the command was unknown.
type InvalidCommandType struct {
CommandType ClientCommandType
}
func (i InvalidCommandType) Error() string {
return fmt.Sprintf("invalid command type %s", i.CommandType)
}
// InvalidPayload is placed in ClientMalformed when the payload could not be parsed.
type InvalidPayload struct {
CommandType ClientCommandType
Cause error
}
func (i InvalidPayload) Error() string {
return fmt.Sprintf("command type %s had invalid payload: %s", i.CommandType, i.Cause)
}
func (i InvalidPayload) Unwrap() error {
return i.Cause
}
// 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.TextMessage {
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, parse")
parts := bytes.SplitN(data, []byte(" "), 2)
commandBytes := parts[0]
payloadJson := parts[1]
var command ClientCommandType
if len(payloadJson) == 0 {
// Since there's no payload, we expect the command to end with an exclamation point.
if bytes.HasSuffix(commandBytes, []byte("!")) {
command = ClientCommandType(bytes.TrimSuffix(commandBytes, []byte("!")))
} else {
r.logger.Warn("Received command not fitting the protocol: has no payload but no ! after command type")
command = ClientCommandType(commandBytes)
}
} else {
command = ClientCommandType(commandBytes)
}
switch command {
case ClientHelloType:
hello := &ClientHello{}
err := json.Unmarshal(payloadJson, hello)
if err != nil {
return ClientMalformed{
Error: &InvalidPayload{
CommandType: command,
Cause: err,
},
}
}
return hello
case ClientRefreshType:
if len(payloadJson) != 0 {
r.logger.Warn("Received command not fitting the protocol: has payload for payloadless Refresh command")
}
refresh := &ClientRefresh{}
return refresh
case ClientActType:
act := &ClientAct{}
err := json.Unmarshal(payloadJson, act)
if err != nil {
return ClientMalformed{
Error: &InvalidPayload{
CommandType: command,
Cause: err,
},
}
}
return act
default:
return ClientMalformed{
Error: InvalidCommandType{CommandType: command},
}
}
}
// 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
}

@ -18,14 +18,14 @@ const (
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
}
// 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"`
@ -43,7 +43,7 @@ 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"`
@ -67,7 +67,7 @@ func (i IDSlice) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
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
@ -84,7 +84,7 @@ 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
@ -105,11 +105,11 @@ 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.Slice `json:"actions"`
}
func (s ServerAct) MarshalLogObject(encoder zapcore.ObjectEncoder) error {

@ -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,7 +37,7 @@ func (c SocketClosed) MarshalLogObject(encoder zapcore.ObjectEncoder) error {
return nil
}
func (c SocketClosed) ClientType() ClientMessageType {
func (c SocketClosed) ClientType() ClientCommandType {
return ClientGoodbyeType
}

@ -14,31 +14,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,47 +77,68 @@ 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) {
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)
w.logger.Debug("Marshaling command to JSON", zap.Object("msg", msg))
payload, err := json.Marshal(msg)
if err != nil {
w.logger.Error("error while rendering message payload to JSON", zap.Error(err))
w.logger.Error("Error while rendering command payload to JSON", zap.Object("msg", msg), zap.Error(err))
return
}
if len(payload) == 2 {
// This is an empty JSON message. We can leave it out.
w.logger.Debug("Empty payload, sending only command type", zap.String("type", string(msg.ServerType())))
_, err = fmt.Fprintf(writer, "%s!", msg.ServerType())
if err != nil {
w.logger.Error("error while writing command-only message", zap.Error(err))
w.logger.Error("Error while writing no-payload command", zap.Error(err), zap.Object("msg", msg))
}
} else {
// Because we need to send this, we put in a space instead of an exclamation mark.
w.logger.Debug("Sending command with payload", zap.String("type", string(msg.ServerType())), zap.ByteString("payload", payload))
_, err = fmt.Fprintf(writer, "%s %s", msg.ServerType(), payload)
if err != nil {
w.logger.Error("error while writing command-only message", zap.Error(err))
w.logger.Error("Error while writing command with payload", zap.Error(err), zap.Object("msg", msg))
}
}
}
// 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)
@ -105,14 +146,14 @@ func (w *writer) sendClose(msg SocketClosed) {
w.logger.Debug("Writing close message")
err := w.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(int(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 +165,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