Compare commits

...

292 commits

Author SHA1 Message Date
f29b189a42 Update README.md
Some checks failed
Docker / build (push) Has been cancelled
Tests / Build and Test (push) Has been cancelled
2024-09-27 10:43:28 +01:00
Marc Di Luzio
fe8029a4b3
Merge pull request #43 from mdiluz/gameplay-improvements
Assorted gameplay improvements from play testing
2020-08-02 12:52:29 +01:00
35b25dde98 Add the upgrade command to the cmdline client 2020-08-02 12:49:15 +01:00
804f82dd20 Fix up commandline interface for repeat commands to go after the command 2020-08-02 12:43:44 +01:00
4e4af1a1be Add a test for the upgrade command 2020-08-02 12:15:49 +01:00
b114b68ff7 Add upgrade command code 2020-08-02 12:03:12 +01:00
1200b0a2a2 Add "upgrade" command to use the rover parts 2020-08-02 11:39:28 +01:00
6a44633d40 Add rover parts to the cmdline pretty printer 2020-08-02 11:28:14 +01:00
636f0ed773 Spawn rover parts a little more frequently 2020-08-01 11:26:10 +01:00
018c122861 Stop spawning dormant rovers in the world 2020-08-01 11:24:53 +01:00
e66b899e2a Increase the base rover range to 10 2020-08-01 11:15:34 +01:00
70f041ae5d Spawn rover parts in the world 2020-08-01 11:09:15 +01:00
d7bda3f607
Merge pull request #42 from mdiluz/tls
Add TLS to server-client communications
2020-07-26 23:57:35 +01:00
94767f06d3 Fix to disable TLS in tests 2020-07-26 23:53:29 +01:00
500e0f9557 Skip the tls verify on the client side for now 2020-07-26 23:46:42 +01:00
4f2a7edeb1 Skip local tests, removing duplicate test runs 2020-07-26 23:42:23 +01:00
cf1dff2814 Make sure the client verifies the TLS 2020-07-26 23:41:52 +01:00
71a0ef9920 Use the fullchain.pem not the cert.pem as explained by letsencrypt 2020-07-26 23:36:34 +01:00
9b03ffb7f1 Add skip verify on the client for now 2020-07-26 23:30:09 +01:00
4821a90143 Pass the cert name into the docker deployment 2020-07-26 23:29:58 +01:00
ac3844fe7a Mount letsencrypt in the docker to read the certs 2020-07-26 23:26:36 +01:00
70d92c2d5e Add TLS to gRPC 2020-07-26 23:10:39 +01:00
bb50fae00b
Merge pull request #41 from mdiluz/add-reflection
Add gRPC reflection to the server
2020-07-26 22:53:59 +01:00
a321e5d72f Add gRPC reflection to the server 2020-07-26 22:48:48 +01:00
49ffa18f23
Merge pull request #40 from mdiluz/add-wait-command
Add wait command
2020-07-26 18:19:30 +01:00
e542999b91 Move test deployment out to it's own file 2020-07-26 18:11:01 +01:00
74e1cd4564 Convert number to repeat to avoid confusion 2020-07-26 18:02:06 +01:00
c0d4a809c9 Update command line client to allow specifying command number 2020-07-26 17:31:09 +01:00
1514603517 Allow number to be used in all commands 2020-07-26 17:19:04 +01:00
9dcbbee1a2
Merge pull request #39 from mdiluz/fix-help-text
Big update to help text and add a simple description
2020-07-26 17:12:22 +01:00
bcf71f0bf9 Add a "wait" command with a number 2020-07-26 17:09:47 +01:00
cec61a9db7 Big update to help text and add a simple description 2020-07-26 16:57:43 +01:00
47e2e13c49
Merge pull request #38 from mdiluz/world-gen-improve
World gen improve
2020-07-25 23:48:59 +01:00
6891ec8439 Adjust the terrain scale to be much larger 2020-07-25 23:39:32 +01:00
113090fbcb Fix bug where we were still placing psuedo-random objects down 2020-07-25 23:39:13 +01:00
a235f6a5f5
Merge pull request #37 from mdiluz/destroy-rovers
Destroy rovers
2020-07-25 23:24:46 +01:00
cd97220a11 Perform rover destruction during the main server tick 2020-07-25 23:18:21 +01:00
f9b3ce3edb Destroy the rover when it has 0 integrity 2020-07-25 23:13:05 +01:00
abf67f7f37
Merge pull request #35 from mdiluz/fix-starting-wind
Add the starting wind as north and ensure it's only updated the next day
2020-07-24 23:27:07 +01:00
5d4fd801c1 Add the starting wind as north and ensure it's only updated the next day 2020-07-24 23:22:46 +01:00
fa05e7f253
Merge pull request #34 from mdiluz/implement-salvage
Add rover salvage mechanics
2020-07-24 23:08:27 +01:00
1f5aa765f4 Merge remote-tracking branch 'origin/master' into implement-salvage 2020-07-24 23:05:02 +01:00
7be0f83c5e Fix golanglint missing error check 2020-07-24 22:58:59 +01:00
a0e04b7e3a Placed dormant world rovers randomly have better base stats 2020-07-24 22:56:35 +01:00
a93ce97b0b Only assign rovers to accounts if given an account 2020-07-24 22:54:06 +01:00
57621d169a Implement a test for transfer and fix bugs 2020-07-24 22:50:47 +01:00
fdfcc88540 Move the account registration into the world 2020-07-24 22:50:35 +01:00
6f2d67bd7c Tag rovers by the controlling account 2020-07-24 22:22:32 +01:00
e840b3e47b Move accountant into world 2020-07-24 20:06:06 +01:00
1e4d642038 Add rover transfer command and implementation
Need to swap the accounts
2020-07-24 20:01:35 +01:00
5b2ea533f4 Remove incorrect proto comment 2020-07-24 19:46:32 +01:00
edd3e5a6cb Fix test by removing object before warping rover 2020-07-24 19:42:34 +01:00
b0ff3eb6ea Remove redundant tests (covered in command_tests) 2020-07-24 19:39:33 +01:00
be36f0631b Add logs for failed stashes 2020-07-24 19:39:25 +01:00
c321f88d96 Add code for salvage command 2020-07-24 19:39:14 +01:00
7cccb4394f Fix the help text comment 2020-07-24 19:28:44 +01:00
524487ce14 Stop the dormant rover from being a blocking object 2020-07-24 19:27:54 +01:00
2f1ccdfdb9 Make repair require rover parts 2020-07-24 19:08:39 +01:00
ce6e10afbb Add salvage command to main.go man page 2020-07-24 19:08:03 +01:00
2c1bb80779 Add salvage command
Slight refactor to re-use command variables

	Also fixes the cmdline client turn command
2020-07-23 20:57:36 +01:00
a5ac809387
Merge pull request #33 from mdiluz/dev
General dev
2020-07-23 18:50:31 +01:00
41cd93e986 Add command test for no command as error 2020-07-23 18:41:12 +01:00
8cc3b9155e Implement broadcast command test 2020-07-23 18:40:32 +01:00
8a8a27ab47 Add a test for the repair command 2020-07-23 18:37:54 +01:00
3bfc91b8f6 Add command test for stashing 2020-07-23 16:58:17 +01:00
8279a08a37 Limit the log entries to a max number 2020-07-23 16:47:39 +01:00
46d904acc6 Rename and comment ticksPerNormalMove 2020-07-23 16:44:15 +01:00
be74183878
Merge pull request #32 from mdiluz/simplifystatus
Organise the status response into sub-sections
2020-07-23 16:33:10 +01:00
13e4d6a5e6 Remove an old log left in 2020-07-23 00:34:57 +01:00
f7192b3997 Organise the status response into sub-sections 2020-07-23 00:32:19 +01:00
4cbf4a9ab5
Merge pull request #31 from mdiluz/remove-doublequeue
Remove the incoming command streams, de-scopes and simplifies
2020-07-23 00:18:51 +01:00
2bc2477128 Remove the incoming command streams, de-scopes and simplifies 2020-07-23 00:13:28 +01:00
d49d034f0e
Merge pull request #30 from mdiluz/sailing
Swap out movement mechanics for sailing mechanics
2020-07-23 00:03:26 +01:00
6adc652cea Fix lint errors 2020-07-22 23:59:28 +01:00
1e18610c5c Set tick rate to 3 2020-07-22 23:57:22 +01:00
5d80cb2596 Implement sailing tests and fix into-the-wind bug 2020-07-22 23:50:42 +01:00
c89c5f6e74 Implement current wind direction and rover wind movement 2020-07-22 23:36:13 +01:00
c94ac68f44 Remove all json tags, simply not needed 2020-07-22 19:55:38 +01:00
075a502103 Pull the repair function out 2020-07-22 19:25:47 +01:00
9e42764398 Update the rover list to a list of pointers 2020-07-22 19:25:32 +01:00
447dbe3582 Fix a test comment 2020-07-22 19:24:55 +01:00
6b5d5abea1 Rename the world tick function and set the tick rate back to default 2020-07-22 19:24:36 +01:00
8667f55143 Simplify by making command streams pointer lists like in proto 2020-07-21 23:52:14 +01:00
f78efd1223 Add SailPosition to the rover and implement toggle command
This also converts the commands to use the proto type for simplicity
2020-07-21 23:44:06 +01:00
6f30b665c7 Make the bearings 8 directional 2020-07-21 22:58:59 +01:00
6c75f07aff Remove move and recharge commands in favor of toggle command for the sails 2020-07-21 22:57:43 +01:00
89123394cd
Merge pull request #28 from mdiluz/dormant-rovers
Dormant rovers
2020-07-19 19:20:03 +01:00
77212c7258 Fix logic for rover marshal test 2020-07-19 18:57:22 +01:00
bffad84181 Don't use noise for rover spawns for now 2020-07-19 18:57:12 +01:00
04d7a5a4ca Fill in the dormant rover log 2020-07-19 18:47:54 +01:00
211771121f Extract rover naming to rover.go 2020-07-19 18:47:44 +01:00
d3c480cb04 Add dormant rover data marshalled into obj data 2020-07-19 18:39:16 +01:00
1281713211 Clear locations before warp in tests 2020-07-19 18:35:12 +01:00
9130cf2517 Move atlas package into rove 2020-07-19 18:30:07 +01:00
c48274eb23 Small refactor in GetObject 2020-07-19 18:27:58 +01:00
ddbbdce1f8 Move default rover params to function 2020-07-19 18:23:11 +01:00
faa1271c5a Try and very rarely spawn a dormant rover 2020-07-19 13:57:45 +01:00
37d828c457 Rename the rock noise 2020-07-19 13:53:38 +01:00
959cbfa15a Combine the two noise functions, we only need one 2020-07-19 13:51:49 +01:00
c637ed37b9 Make the dormat rover blocking 2020-07-19 13:49:43 +01:00
87a9abcd12 Add a glyph for the dormant rover 2020-07-19 13:49:34 +01:00
1eba9a8652 Pull world gen out into interface 2020-07-19 13:41:47 +01:00
4f1a9c2c2b Re-order object types 2020-07-19 13:27:59 +01:00
713699687f Add a dormat rover data type 2020-07-19 13:27:38 +01:00
fd0992353d Add data to objects 2020-07-19 13:27:29 +01:00
e27398dbc0
Merge pull request #27 from mdiluz/bearings
Refactor bearing out to proto file
2020-07-19 13:16:59 +01:00
4b7510ffa1 Merge remote-tracking branch 'origin/master' into bearings 2020-07-19 13:13:36 +01:00
57f668ae54 Reinstate BearingFromString function 2020-07-19 13:13:09 +01:00
c13151b60f
Merge pull request #26 from mdiluz/glyphs
Refactor to move object/tile types into the proto file
2020-07-19 13:02:47 +01:00
db8ed0302d Merge glyphs branch 2020-07-19 13:01:25 +01:00
cd6a275bb9 Move code to internal cmd/main 2020-07-19 12:59:36 +01:00
4e0e55af88 Move bearing into proto file 2020-07-19 12:54:41 +01:00
3796ee09a3 Merge remote-tracking branch 'origin/master' into glyphs 2020-07-19 12:38:46 +01:00
0a8b8d5979
Merge pull request #25 from mdiluz/remove-diagonal-moves
Remove diagonal moves
2020-07-19 12:37:59 +01:00
e9188dbbf6 Auto-format proto file 2020-07-19 12:37:36 +01:00
da91d31649 MOve glyph code into client 2020-07-19 12:36:48 +01:00
4a89cb9d6e Move glyph functions out to the glyph file 2020-07-19 12:34:54 +01:00
7bdfa44fb6 Fix up the concept of "None" tiles and objects
Replace with "Unknown" which is effectively an invalid value
2020-07-19 12:33:11 +01:00
305f64ec38 Large refactor, move object and tile types out into the proto 2020-07-19 12:26:57 +01:00
24d4fe9273 Convert tiles and object types to typed consts 2020-07-19 11:59:14 +01:00
7e41ac0028 Rename the glyphs 2020-07-19 11:57:41 +01:00
f665436007 Convert objects and tiles to base ints 2020-07-19 11:54:11 +01:00
acdd019093 Add Glyph methods to convert to a glyph 2020-07-19 11:50:19 +01:00
53d6ad08d9 Rename Type to ObjectType 2020-07-19 11:47:19 +01:00
a0b811a659 Move glyph definitions into a central type 2020-07-19 11:46:37 +01:00
5814ac95b8 Make sure we fallthrough for the NES cases 2020-07-19 11:29:28 +01:00
c2e3c9f090 Reject move commands in non-cardinal directions 2020-07-19 11:26:08 +01:00
0e731df1a3
Merge pull request #24 from mdiluz/fix-southwest
Fix SW direction to go south rather than north
2020-07-11 17:31:40 +01:00
105d69bd7c Fix SW direction to go south rather than north 2020-07-11 17:25:38 +01:00
79598c3373
Merge pull request #23 from mdiluz/update-page
Small clean up
2020-07-10 21:43:25 +01:00
9593602dc5 Move flatpak file to data 2020-07-10 21:38:16 +01:00
327c246c8e Remove vscode viles 2020-07-10 21:37:19 +01:00
346bc940e2
Merge pull request #22 from mdiluz/update-page
Update the github page markdown
2020-07-10 21:36:00 +01:00
427db95b5b Fix grammar 2020-07-10 21:32:02 +01:00
a0946f9e58
Merge pull request #21 from mdiluz/mdiluz-patch-1
Update README.md
2020-07-10 21:30:46 +01:00
5c4d7469bb Update the github page markdown 2020-07-10 21:30:15 +01:00
3bad9f0122
Update README.md
Fix proto link and amend text
2020-07-10 20:59:26 +01:00
3f2ae97048
Merge pull request #20 from mdiluz/more-improvements
Various refactors and improvements
2020-07-10 20:39:40 +01:00
737534f739 Move roveapi into the proto dir 2020-07-10 19:01:41 +01:00
46f81abbd7 Move accounts into rove-server.internal 2020-07-10 18:57:57 +01:00
9ccb7ac019 Remove google proto files, no longer needed 2020-07-10 18:48:03 +01:00
f0ab2abf6e Move object into atlas 2020-07-10 18:39:33 +01:00
f40f7123d4 Move bearing into maths 2020-07-10 18:24:54 +01:00
5b1fe61097 Move vector into maths package 2020-07-10 18:22:59 +01:00
97d3be000b Re-order some World members 2020-07-10 18:14:32 +01:00
065f79cbb3 Fix warping to non-empty space 2020-07-10 18:11:38 +01:00
b534ac0516 Rename generated rove package to roveapi and the game package to rove 2020-07-10 18:09:51 +01:00
b451ea519d Make sure the accounts are saved as well 2020-07-10 17:21:59 +01:00
dc2800fa54 Move Accountant behind an interface 2020-07-10 17:09:47 +01:00
c1267829ac Clear out genproto mod require 2020-07-10 17:00:30 +01:00
d6349d081e Clear the tile before warping to it 2020-07-10 16:59:55 +01:00
9a7c48ae78 Make chunkBasedAtlas private 2020-07-10 16:56:17 +01:00
a0be8a463c Pull out chunk based atlas into new file 2020-07-10 16:54:43 +01:00
655e00b41f Don't expose Chunk externally 2020-07-10 16:52:31 +01:00
fb2ffc5252 Convert Atlas to an interface 2020-07-10 16:52:00 +01:00
5ac44d85cb Add a warning to missing DATA_PATH env 2020-07-10 16:38:49 +01:00
3665a62c6e
Merge pull request #19 from mdiluz/clean-proto
De-scope and proto clean
2020-07-10 09:38:33 +01:00
6c1ee311cd Delete unused files 2020-07-10 00:29:06 +01:00
fe6dae4c52 Update the generated file for rove.pb.go 2020-07-10 00:27:14 +01:00
0be6aa7c12 Clean, format and comment the rove.proto file 2020-07-10 00:26:49 +01:00
96a137ad2f Simplify - remove duplicate command types in favor of a better defined Command type in proto 2020-07-10 00:12:54 +01:00
7d780d05bd De-scope - remove swagger docs and http proxy
HTTP proxy was becoming annoying to maintain, and gRPC is easier to use anyway

swagger docs are just part of the fallout
2020-07-10 00:12:35 +01:00
59a1bdc14b
Update launch.json
Remove old comments
2020-07-09 23:15:33 +01:00
bffe539d77
Merge pull request #18 from mdiluz/fix-missing-message-in-reply
Fix missing broadcast message in status reply
2020-07-09 22:57:42 +01:00
b032fdbfe2 Fix missing broadcast message in status reply 2020-07-09 22:52:58 +01:00
23764a3fc3
Merge pull request #17 from mdiluz/add-broadcast-to-cmdline
Add broadcast command to the cmdline client
2020-07-09 22:44:51 +01:00
091469dd91 Add broadcast command to the cmdline client 2020-07-09 22:37:55 +01:00
db19e4a657
Merge pull request #16 from mdiluz/rover-logs-and-communication
Add "broadcast" command
2020-07-09 22:19:42 +01:00
e21023ec25 Update generated files 2020-07-09 22:12:13 +01:00
d4d82c38e0 Add "broadcast" command
This will send a readable ascii triplet to all rovers in range
2020-07-09 22:05:12 +01:00
2671398593
Merge pull request #15 from mdiluz/rover-logs-and-communication
Add rover logs
2020-07-09 19:42:48 +01:00
30ca488890 Use string for the timestamp, proto uses this under the hood anyway
https://github.com/grpc-ecosystem/grpc-gateway/issues/438
2020-07-09 19:38:23 +01:00
b748846c55 Use a unix timestamp rather than a timestamppb 2020-07-09 19:29:04 +01:00
55c85d2a22 Add logs to the rover status output 2020-07-09 19:01:09 +01:00
b2f169d99f Remove Warped log, unneeded 2020-07-09 18:31:51 +01:00
8866f28bf5 Add test coverage checks for logging additions 2020-07-09 18:26:24 +01:00
0dc3cab9c0 Store log entries for actions in the rover 2020-07-09 18:19:49 +01:00
84be8bff05
Merge pull request #14 from mdiluz/improved-world-gen
Noise based world gen
2020-07-09 00:11:46 +01:00
9682cfa7ea Spawn objects using OpenSimplex noise as well 2020-07-09 00:04:46 +01:00
4b715bdff3 Move to OpenSimplex noise
Apart from other benefits, this produces much nicer direction agnostic noise
2020-07-08 23:58:11 +01:00
7b4541716a Add gravel tiles 2020-07-08 23:45:52 +01:00
ed9ecef80a Add perlin based generation for the terrain tiles 2020-07-08 23:38:08 +01:00
10959ef726 Refactor populate to be an Atlas function
This simplifies usage greatly
2020-07-08 19:40:15 +01:00
2ff4bcded7
Merge pull request #12 from mdiluz/fix-unstable-radar
Fix unstable radar
2020-07-07 23:05:47 +01:00
0386617c51 Add error checks in TestWorld_RadarFromRover 2020-07-07 23:01:28 +01:00
089f5e5337 Fix chunk empty chunk population in QueryPosition 2020-07-07 22:57:55 +01:00
3e1e3a5456 Amend to TestWorld_RadarFromRover to show the issue 2020-07-07 22:49:34 +01:00
d9e97ea468 Add some additional logging to requests and world resizes 2020-07-07 22:47:34 +01:00
47df02ec7e
Merge pull request #10 from mdiluz/basic-account-security
WIP: Add basic account security
2020-07-07 22:27:06 +01:00
92222127a6 Add basic account security
This adds a secret token associated with each account

	The token must then be sent with follow-up requests to ensure they get accepted

	This is _very_ basic security, and without TLS is completely vulnerable to MITM attacks, as well as brute force guessing (though it'd take a while to guess the a correct UUID)
2020-07-07 22:20:23 +01:00
df30a0d689
Merge pull request #1 from mdiluz/day-night-cycle
Add a day-night cycle for solar charging
2020-07-07 21:38:37 +01:00
5980de5ba7 Fix lint check 2020-07-07 21:33:32 +01:00
254957cde5 Add a test to check daytime and rover recharge 2020-07-07 21:30:51 +01:00
3ba7652c74 Add current tick information to the server-status 2020-07-07 18:40:38 +01:00
1412579c6c Only charge during the day 2020-07-07 18:37:59 +01:00
526e9c69eb Ensure world tick properties are properly named in json 2020-07-07 18:37:45 +01:00
20385c5ae7 Add tick tracking to the world 2020-07-07 18:36:20 +01:00
5928dfdb20 Rename the tick variable 2020-07-07 18:24:16 +01:00
c9af3772da
Merge pull request #5 from mdiluz/clean-out-status
Remove status doc, now tracked on GitHub
2020-07-07 18:07:37 +01:00
3faf709f10 Remove status doc, now tracked on GitHub 2020-07-07 17:58:41 +01:00
3493d51d36 Set the server tick rate to 5 2020-07-07 17:45:36 +01:00
b6d47833f6 Merge branch 'master' of github.com:mdiluz/rove 2020-07-07 13:13:25 +01:00
ad13ed8ee2
Fix git diff check 2020-07-07 13:13:17 +01:00
5b4b9c30eb Update rove.pb.go 2020-07-07 13:13:02 +01:00
a1b79a8df5
Display diff change 2020-07-07 13:08:33 +01:00
c66e61921f Add generated files check to github actions 2020-07-07 12:59:10 +01:00
fc4fa3decf Remove go dependencies fetch to test need for it 2020-07-06 18:10:02 +01:00
75910efbe5 Apply all golangci-lint fixes 2020-07-06 18:04:10 +01:00
945b3299ac Add golangci-lint from https://github.com/actions-contrib/golangci-lint 2020-07-06 17:53:35 +01:00
ed6de9eac4 Remove swagger install from tests 2020-07-06 17:49:01 +01:00
718252731b Give up on getting protoc and lint to work in the action 2020-07-06 17:46:39 +01:00
2fbe2dc1a8 Re-order code checkout 2020-07-06 17:18:40 +01:00
408fffb0c6 Fix to use the new golangci-lint action 2020-07-06 17:16:09 +01:00
62741caf5e Add check for golang lint 2020-07-06 17:12:32 +01:00
7563896a59 Merge branch 'master' of github.com:mdiluz/rove 2020-07-06 17:12:27 +01:00
771c95da14 Update makefile with more echos 2020-07-06 17:10:14 +01:00
2c6aba6897 Remove Gopkg usage in github action 2020-07-06 17:10:04 +01:00
9eb2e08667 Add check to github action that we've run make gen 2020-07-06 17:09:49 +01:00
35d7400a83 Also build the rove command line client into the docker 2020-07-05 13:37:56 +01:00
a112c3ed47 Override incoming commands rather than appending 2020-07-05 13:16:19 +01:00
233a6b3281 Add incoming and queued commands to status output 2020-07-05 13:16:08 +01:00
ea4b7de4ac Rename "commands" to "command" 2020-07-05 12:55:01 +01:00
1554b7c21b Update README.md
Fix the link to the proto
Add the logo
2020-07-04 23:38:59 +01:00
31c0753341 Fix InnerMain test 2020-07-04 23:15:12 +01:00
28639b4cac Fix up a comment and the help text 2020-07-04 23:11:22 +01:00
894359142b Rename "rover" to "status" 2020-07-04 23:11:12 +01:00
f8e594cb39 Rename "status" command to "server-status" 2020-07-04 23:05:08 +01:00
98eea89484 Adjust goals to focus on core rover gameplay 2020-07-04 22:59:27 +01:00
31308db6b5 Tick off energy in upcoming features and simplify current features 2020-07-04 22:57:19 +01:00
87af905bc8 Rename charge command to recharge 2020-07-04 22:56:58 +01:00
7272749614 Enable command line client to accept new commands 2020-07-04 22:42:37 +01:00
e875f82b13 Add command "charge" to charge up the rover's energy store 2020-07-04 22:42:20 +01:00
15c337c067 Make moving and stashing cost rover charge 2020-07-04 22:35:25 +01:00
8b83672dcc Fix Atlas gen with simplification
Only track lower and upper bounds in world space, and speak in terms of world space and chunks
2020-07-04 22:34:28 +01:00
dbe944bb4e Add charge and apply it to rover actions 2020-07-04 12:30:40 +01:00
143fba505e Add Charge and Max Charge attributes to the rover 2020-07-04 12:26:42 +01:00
b066277ddf Add MaximumIntegrity to the rover 2020-07-04 12:26:42 +01:00
2eaed1447d Add rover inventory capacity and test 2020-07-04 12:26:42 +01:00
e6ff453ff1 Add note for intelligent world-gen 2020-07-04 12:26:42 +01:00
9cd5324465 Fix small and large rock spawning 2020-07-03 17:13:52 +01:00
1a1ef9a376 go mod tidy and update 2020-07-03 17:06:28 +01:00
c4b0762ebe Fix up the tile print now that the radar returns objects 2020-07-03 17:05:31 +01:00
062f9cfec8 Split Atlas chunks into tiles and objects 2020-07-03 17:00:04 +01:00
74dcae6542
Fix badges 2020-07-01 13:46:39 +01:00
e6bfc7a8fc Set up Github docker auth 2020-07-01 13:39:47 +01:00
821c83549b
Rename the docker action 2020-07-01 13:20:11 +01:00
c0726a2345
Rename the tests file 2020-07-01 13:19:50 +01:00
13482c1893
Add a docker build action
Created from workflow template
2020-07-01 13:19:01 +01:00
b5707ab71c Fix all go vet issues 2020-07-01 00:01:20 +01:00
204c786103 Rename rove-reverse-proxy to rove-server-rest-proxy 2020-06-30 23:37:38 +01:00
abcebcebb6 Simplify - remove rove-accountant
This was a fun little gRPC experiment but it's simply not needed
2020-06-30 23:34:49 +01:00
984ff56664 Add flatpak file, unused but functional 2020-06-30 22:43:53 +01:00
c07a9b2659 Merge branch 'master' of github.com:mdiluz/rove 2020-06-30 17:41:16 +01:00
77bde53a52 Rename the main design doc 2020-06-30 17:41:08 +01:00
Marc Di Luzio
1f2669b643 Set theme jekyll-theme-merlot 2020-06-30 17:40:23 +01:00
e3169cdbdd Update the docs and status pages 2020-06-30 16:50:16 +01:00
e09cea328b Refactor into singular account in the config 2020-06-28 15:52:46 +01:00
b9198c546c Update the status
HTTPS makes sense for the docs, but is not essential, the real goal is to have token security
2020-06-28 12:30:32 +01:00
0d3aac49b1 Don't expose the rove-accountant 2020-06-28 12:26:51 +01:00
06cf44f129 Increase the chunk size to 1kb per chunk 2020-06-28 11:02:56 +01:00
e5ee0eaece Rename a couple of Atlas variables
Sometimes names can be too long
2020-06-28 11:01:01 +01:00
9bb91920c9 Make Atlas grow in X and Y dimensions independently
Fixes exponential growth
2020-06-28 00:18:39 +01:00
b116cdf291 Convert Atlas to infinite lazy growth
The atlas will now expand as needed for any query, but only initialise the chunk tile memory when requested

	While this may still be a pre-mature optimisation, it does simplify some code and ensures that our memory footprint stays small, for the most part
2020-06-27 14:48:21 +01:00
2556c0d049 Call rand.Seed to end current determinism 2020-06-27 02:08:52 +01:00
6ba6584ae1 Default to a much faster tick rate for now 2020-06-27 02:03:12 +01:00
5b5f80be7d Clean up logging a little 2020-06-27 02:02:18 +01:00
5bbb2ff37f Fix help text for commands 2020-06-27 01:41:19 +01:00
693b8a54f1 Add repair command to repair using inventory item 2020-06-27 01:39:10 +01:00
7957454ec1 Add rover integrity
Rovers are damaged by bumping into solid objects
2020-06-27 01:18:18 +01:00
12dc9e478d Remove usage of os arg in help, it's confusing for snaps 2020-06-27 00:56:28 +01:00
adf3def488 Small Status update 2020-06-27 00:51:51 +01:00
1ed1c60de0 Simplify - remove RoverAttributes and rover UUIDs 2020-06-27 00:32:27 +01:00
f9c30f541c Rename USER_DATA to ROVE_USER_DATA 2020-06-27 00:02:07 +01:00
4a343f36a8 Remove ROVE_HOST
No need for two ways to set this
2020-06-26 23:58:58 +01:00
b2f6c1a0b1 Update status doc 2020-06-26 23:45:31 +01:00
e6a25a5310 Add the rover inventory to the "rover" response 2020-06-26 23:44:52 +01:00
e1bff92a56 Remove Item type in favor of just byte 2020-06-26 23:41:36 +01:00
d08a15e201 De-scope - Remove unused rover capacity 2020-06-26 23:39:07 +01:00
71c2c09270 Write test to check rover has item in inventory 2020-06-26 23:37:10 +01:00
f0d40cc46c Change help print to standard format 2020-06-26 23:31:57 +01:00
6c09ee3826 Refactor main to accept commands and arguments 2020-06-26 23:31:06 +01:00
d624a3ca21 Add verification for "stash" command 2020-06-26 23:30:42 +01:00
2f6465987d More de-scope - remove duration on move command
This isn't even needed, as commands can just be queued up
2020-06-26 22:26:27 +01:00
383e834cef Add RoverStash test 2020-06-26 20:14:00 +01:00
00bdad6b40 Fix stashing
Now checks if object is stashable and clears the tile
2020-06-26 19:47:01 +01:00
2846ed796e Refactor tiles to objects to be re-used 2020-06-26 19:45:24 +01:00
8b1eca0aee Implement basic stash command 2020-06-26 18:59:12 +01:00
db3c2c2c2e De-scope, remove rover speed 2020-06-26 18:48:07 +01:00
a84709564c Minor refactor to move name to top of attributes class 2020-06-26 18:24:03 +01:00
7ee340e976 Move Rover position into main class 2020-06-26 18:22:37 +01:00
8019ea4e25 Add an inventory to the rover 2020-06-26 18:13:23 +01:00
15b8f0a427 Update the PoC doc to a status doc 2020-06-26 18:06:41 +01:00
75 changed files with 5949 additions and 6212 deletions

View file

@ -1,45 +0,0 @@
name: Build and Test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Install swagger
run: |
sudo curl -o /usr/local/bin/swagger -L'#' https://github.com/go-swagger/go-swagger/releases/download/v0.23.0/swagger_linux_amd64
sudo chmod +x /usr/local/bin/swagger
- name: Check out repo
uses: actions/checkout@v2
- name: Get go dependencies
run: |
go get -v -t -d ./...
if [ -f Gopkg.toml ]; then
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
dep ensure
fi
- name: Build and Test
run: make test
- name: Upload test coverage result
uses: actions/upload-artifact@v1
with:
name: Coverage
path: /tmp/coverage.html

25
.github/workflows/docker.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: Docker
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Docker Login
uses: azure/docker-login@v1
with:
login-server: docker.pkg.github.com
username: $GITHUB_ACTOR
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build the Docker image
run: |
VERSION=$(git describe --always --long --dirty --tags)
docker build . --tag docker.pkg.github.com/mdiluz/rove/rove:$VERSION --tag docker.pkg.github.com/mdiluz/rove/rove:latest
docker push docker.pkg.github.com/mdiluz/rove/rove:$VERSION
docker push docker.pkg.github.com/mdiluz/rove/rove:latest

44
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,44 @@
name: Tests
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run golangci-lint
uses: actions-contrib/golangci-lint@v1
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.13
id: go
- name: Check generated files
run: |
PROTOC_ZIP=protoc-3.6.1-linux-x86_64.zip
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/$PROTOC_ZIP
sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc
sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*'
sudo chmod -R o+r /usr/local/include/google/
rm -f $PROTOC_ZIP
make gen
git update-index --refresh || (git diff; exit 1)
- name: Build and Test
run: make test
- name: Upload test coverage result
uses: actions/upload-artifact@v1
with:
name: Coverage
path: /tmp/coverage.html

28
.vscode/launch.json vendored
View file

@ -1,28 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Rove Server",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/rove-server/main.go",
"cwd": "${workspaceFolder}",
"env": {},
"args": [],
},
{
"name": "Launch Rove",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/rove/main.go",
"cwd": "${workspaceFolder}",
"env": {},
"args": ["radar"],
}
]
}

View file

@ -1,5 +0,0 @@
{
"gopls": {
"buildFlags": ["-tags=integration"]
},
}

View file

@ -6,9 +6,8 @@ COPY . .
RUN go mod download
# Build the executables
RUN go build -o rove -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=$(git describe --always --long --dirty --tags)'" cmd/rove/main.go
RUN go build -o rove-server -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=$(git describe --always --long --dirty --tags)'" cmd/rove-server/main.go
RUN go build -o rove-accountant cmd/rove-accountant/main.go
RUN go build -o rove-reverse-proxy cmd/rove-reverse-proxy/main.go
CMD [ "./rove-server" ]

View file

@ -1,8 +0,0 @@
FROM quay.io/goswagger/swagger:latest
LABEL maintainer="Marc Di Luzio <marc.diluzio@gmail.com>"
WORKDIR /app
COPY . .
CMD [ "serve", "pkg/rove/rove.swagger.json", "--no-open" ]

View file

@ -1,28 +1,26 @@
VERSION := $(shell git describe --always --long --dirty --tags)
build:
@echo Running no-output build
go mod download
go build -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=${VERSION}'" ./...
go build -ldflags="-X 'github.com/mdiluz/rove/cmd/version.Version=${VERSION}'" ./...
install:
@echo Installing to GOPATH
go mod download
go install -ldflags="-X 'github.com/mdiluz/rove/pkg/version.Version=${VERSION}'" ./...
gen:
@echo Generating accountant gRPC
protoc --proto_path proto --go_out=plugins=grpc:pkg/ --go_opt=paths=source_relative proto/accounts/accounts.proto
@echo Installing go dependencies
go install github.com/golang/protobuf/protoc-gen-go
go mod download
@echo Generating rove server gRPC
protoc --proto_path proto --go_out=plugins=grpc:pkg/ --go_opt=paths=source_relative proto/rove/rove.proto
protoc --proto_path proto --grpc-gateway_out=paths=source_relative:pkg/ proto/rove/rove.proto
protoc --proto_path proto --swagger_out=logtostderr=true:pkg/ proto/rove/rove.proto
protoc --proto_path proto --go_out=plugins=grpc,paths=source_relative:proto/ proto/roveapi/roveapi.proto
test:
@echo Unit tests
go test -v ./...
@echo Integration tests
docker-compose up --build --exit-code-from=rove-tests --abort-on-container-exit rove-tests
docker-compose down
@echo Run unit and integration tests
docker-compose -f docker-compose-test.yml up --build --exit-code-from=rove-tests --abort-on-container-exit rove-tests
docker-compose -f docker-compose-test.yml down
go tool cover -html=/tmp/coverage-data/c.out -o /tmp/coverage.html
@echo Done, coverage data can be found in /tmp/coverage.html

View file

@ -1,13 +1,8 @@
Rove
====
![Rove](data/icon.svg)
Rove is an asynchronous nomadic game about exploring as part of a loose community.
This repository is a [living document](https://github.com/mdiluz/rove/tree/master/docs) of current game design, as well as source code for the `rove-server` deployment and the `rove` command line client.
See [api.go](https://github.com/mdiluz/rove/blob/master/pkg/rove/api.go) for the current server-client API.
Build Status
------------
![Build and Test](https://github.com/mdiluz/rove/workflows/Build%20and%20Test/badge.svg) [![rove](https://snapcraft.io//rove/badge.svg)](https://snapcraft.io/rove)
This repository contains the source code for the `rove-server` deployment and the `rove` command line client. See [mdiluz.github.io/rove](https://mdiluz.github.io/rove/) for game details, and [roveapi.proto](proto/roveapi/roveapi.proto) for the current server-client API.

View file

@ -1,80 +0,0 @@
package internal
import (
"fmt"
"time"
)
const kAccountsFileName = "rove-accounts.json"
// Account represents a registered user
type Account struct {
// Name simply describes the account and must be unique
Name string `json:"name"`
// Data represents internal account data
Data map[string]string `json:"data"`
}
// Represents the accountant data to store
type accountantData struct {
}
// Accountant manages a set of accounts
type Accountant struct {
Accounts map[string]Account `json:"accounts"`
}
// NewAccountant creates a new accountant
func NewAccountant() *Accountant {
return &Accountant{
Accounts: make(map[string]Account),
}
}
// RegisterAccount adds an account to the set of internal accounts
func (a *Accountant) RegisterAccount(name string) (acc Account, err error) {
// Set the account name
acc.Name = name
acc.Data = make(map[string]string)
// Verify this acount isn't already registered
for _, a := range a.Accounts {
if a.Name == acc.Name {
return Account{}, fmt.Errorf("account name already registered: %s", a.Name)
}
}
// Set the creation time
acc.Data["created"] = time.Now().String()
// Simply add the account to the map
a.Accounts[acc.Name] = acc
return
}
// AssignRover assigns data to an account
func (a *Accountant) AssignData(account string, key string, value string) error {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
this.Data[key] = value
a.Accounts[account] = this
} else {
return fmt.Errorf("no account found for id: %s", account)
}
return nil
}
// GetRover gets the rover rover for the account
func (a *Accountant) GetValue(account string, key string) (string, error) {
// Find the account matching the ID
if this, ok := a.Accounts[account]; !ok {
return "", fmt.Errorf("no account found for id: %s", account)
} else {
return this.Data[key], nil
}
}

View file

@ -1,135 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"github.com/mdiluz/rove/cmd/rove-accountant/internal"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/persistence"
"google.golang.org/grpc"
)
var data = os.Getenv("DATA_PATH")
// accountantServer is the internal object to manage the requests
type accountantServer struct {
accountant *internal.Accountant
sync sync.RWMutex
}
// Register will register an account
func (a *accountantServer) Register(ctx context.Context, in *accounts.RegisterInfo) (*accounts.RegisterResponse, error) {
a.sync.Lock()
defer a.sync.Unlock()
// Try and register the account itself
log.Printf("Registering account: %s\n", in.Name)
if _, err := a.accountant.RegisterAccount(in.Name); err != nil {
log.Printf("Error: %s\n", err)
return nil, err
}
// Save out the accounts
if err := persistence.Save("accounts", a.accountant); err != nil {
log.Printf("Error: %s\n", err)
return nil, err
}
return &accounts.RegisterResponse{}, nil
}
// AssignData assigns a key value pair to an account
func (a *accountantServer) AssignValue(_ context.Context, in *accounts.DataKeyValue) (*accounts.DataKeyResponse, error) {
a.sync.RLock()
defer a.sync.RUnlock()
// Try and assign the data
log.Printf("Assigning value for account %s: %s->%s\n", in.Account, in.Key, in.Value)
err := a.accountant.AssignData(in.Account, in.Key, in.Value)
if err != nil {
log.Printf("Error: %s\n", err)
return nil, err
}
return &accounts.DataKeyResponse{}, nil
}
// GetData gets the value for a key
func (a *accountantServer) GetValue(_ context.Context, in *accounts.DataKey) (*accounts.DataResponse, error) {
a.sync.RLock()
defer a.sync.RUnlock()
// Try and fetch the rover
log.Printf("Getting value for account %s: %s\n", in.Account, in.Key)
data, err := a.accountant.GetValue(in.Account, in.Key)
if err != nil {
log.Printf("Error: %s\n", err)
return nil, err
}
return &accounts.DataResponse{Value: data}, nil
}
// main
func main() {
// Get the port
var iport int
var port = os.Getenv("PORT")
if len(port) == 0 {
iport = 9091
} else {
var err error
iport, err = strconv.Atoi(port)
if err != nil {
log.Fatal("$PORT not valid int")
}
}
persistence.SetPath(data)
// Initialise and load the accountant
accountant := internal.NewAccountant()
if err := persistence.Load("accounts", accountant); err != nil {
log.Fatalf("failed to load account data: %s", err)
}
// Set up the RPC server and register
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", iport))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
accounts.RegisterAccountantServer(grpcServer, &accountantServer{
accountant: accountant,
})
// Set up the close handler
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Println("Quit requested, exiting...")
grpcServer.Stop()
}()
// Serve the RPC server
log.Printf("Serving accountant on %s\n", port)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve gRPC: %s", err)
}
// Save out the accountant data
if err := persistence.Save("accounts", accountant); err != nil {
log.Fatalf("failed to save accounts: %s", err)
}
}

View file

@ -1,128 +0,0 @@
// +build integration
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"testing"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/rove"
"github.com/stretchr/testify/assert"
)
// Server is a simple wrapper to a server path
type Server string
// Request performs a HTTP
func (s Server) Request(method, path string, in, out interface{}) error {
u := url.URL{
Scheme: "http",
Host: fmt.Sprintf("%s:8080", string(s)),
Path: path,
}
client := &http.Client{}
// Marshal the input
marshalled, err := json.Marshal(in)
if err != nil {
return err
}
// Set up the request
req, err := http.NewRequest(method, u.String(), bytes.NewReader(marshalled))
if err != nil {
return err
}
// Do the POST
req.Header.Set("Content-Type", "application/json")
if resp, err := client.Do(req); err != nil {
return err
} else if resp.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body to code %d", resp.StatusCode)
}
return fmt.Errorf("http returned status %d: %s", resp.StatusCode, string(body))
} else {
return json.NewDecoder(resp.Body).Decode(out)
}
}
var serv = Server(os.Getenv("ROVE_HTTP"))
func TestServer_Status(t *testing.T) {
req := &rove.StatusRequest{}
resp := &rove.StatusResponse{}
if err := serv.Request("GET", "status", req, resp); err != nil {
log.Fatal(err)
}
}
func TestServer_Register(t *testing.T) {
req := &rove.RegisterRequest{Name: uuid.New().String()}
resp := &rove.RegisterResponse{}
err := serv.Request("POST", "register", req, resp)
assert.NoError(t, err, "First register attempt should pass")
err = serv.Request("POST", "register", req, resp)
assert.Error(t, err, "Second identical register attempt should fail")
}
func TestServer_Command(t *testing.T) {
acc := uuid.New().String()
err := serv.Request("POST", "register", &rove.RegisterRequest{Name: acc}, &rove.RegisterResponse{})
assert.NoError(t, err, "First register attempt should pass")
err = serv.Request("POST", "commands", &rove.CommandsRequest{
Account: acc,
Commands: []*rove.Command{
{
Command: "move",
Bearing: "NE",
Duration: 1,
},
},
}, &rove.CommandsResponse{})
assert.NoError(t, err, "Commands should should pass")
}
func TestServer_Radar(t *testing.T) {
acc := uuid.New().String()
err := serv.Request("POST", "register", &rove.RegisterRequest{Name: acc}, &rove.RegisterResponse{})
assert.NoError(t, err, "First register attempt should pass")
resp := &rove.RadarResponse{}
err = serv.Request("POST", "radar", &rove.RadarRequest{
Account: acc,
}, resp)
assert.NoError(t, err, "Radar sould pass should pass")
assert.NotZero(t, resp.Range, "Radar should return valid range")
w := int(resp.Range*2 + 1)
assert.Equal(t, w*w, len(resp.Tiles), "radar should return correct number of tiles")
}
func TestServer_Rover(t *testing.T) {
acc := uuid.New().String()
err := serv.Request("POST", "register", &rove.RegisterRequest{Name: acc}, &rove.RegisterResponse{})
assert.NoError(t, err, "First register attempt should pass")
resp := &rove.RoverResponse{}
err = serv.Request("POST", "rover", &rove.RoverRequest{
Account: acc,
}, resp)
assert.NoError(t, err, "Rover sould pass should pass")
assert.NotZero(t, resp.Range, "Rover should return valid range")
assert.NotZero(t, len(resp.Name), "Rover should return valid name")
assert.NotZero(t, resp.Speed, "Rover should return valid speed")
assert.NotZero(t, resp.Position, "Rover should return valid position")
}

View file

@ -1,51 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strconv"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
"github.com/mdiluz/rove/pkg/rove"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var endpoint = os.Getenv("ROVE_GRPC")
if len(endpoint) == 0 {
endpoint = "localhost:9090"
}
var iport int
var port = os.Getenv("PORT")
if len(port) == 0 {
iport = 8080
} else {
var err error
iport, err = strconv.Atoi(port)
if err != nil {
log.Fatal("$PORT not valid int")
}
}
// Create a new mux and register it with the gRPC endpoint
fmt.Printf("Hosting reverse-proxy on %d for %s\n", iport, endpoint)
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
if err := rove.RegisterRoveHandlerFromEndpoint(ctx, mux, endpoint, opts); err != nil {
log.Fatal(err)
}
// Start the HTTP server and proxy calls to gRPC endpoint when needed
if err := http.ListenAndServe(fmt.Sprintf(":%d", iport), mux); err != nil {
log.Fatal(err)
}
}

View file

@ -3,24 +3,21 @@ package internal
import (
"context"
"fmt"
"log"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/pkg/version"
"google.golang.org/grpc"
"github.com/mdiluz/rove/proto/roveapi"
)
func (s *Server) Status(context.Context, *rove.StatusRequest) (*rove.StatusResponse, error) {
response := &rove.StatusResponse{
Ready: true,
Version: version.Version,
Tick: int32(s.tick),
// ServerStatus returns the status of the current server to a gRPC request
func (s *Server) ServerStatus(context.Context, *roveapi.ServerStatusRequest) (*roveapi.ServerStatusResponse, error) {
response := &roveapi.ServerStatusResponse{
Ready: true,
Version: version.Version,
TickRate: int32(s.minutesPerTick),
CurrentTick: int32(s.world.CurrentTicks),
}
// TODO: Verify the accountant is up and ready too
// If there's a schedule, respond with it
if len(s.schedule.Entries()) > 0 {
response.NextTick = s.schedule.Entries()[0].Next.Format("15:04:05")
@ -29,108 +26,144 @@ func (s *Server) Status(context.Context, *rove.StatusRequest) (*rove.StatusRespo
return response, nil
}
func (s *Server) Register(ctx context.Context, req *rove.RegisterRequest) (*rove.RegisterResponse, error) {
// Register registers a new account for a gRPC request
func (s *Server) Register(ctx context.Context, req *roveapi.RegisterRequest) (*roveapi.RegisterResponse, error) {
log.Printf("Handling register request: %s\n", req.Name)
if len(req.Name) == 0 {
return nil, fmt.Errorf("empty account name")
}
if _, err := s.accountant.Register(ctx, &accounts.RegisterInfo{Name: req.Name}, grpc.WaitForReady(true)); err != nil {
if acc, err := s.world.Accountant.RegisterAccount(req.Name); err != nil {
return nil, err
} else if _, _, err := s.SpawnRoverForAccount(req.Name); err != nil {
} else if _, err := s.SpawnRoverForAccount(req.Name); err != nil {
return nil, fmt.Errorf("failed to spawn rover for account: %s", err)
} else if err := s.SaveWorld(); err != nil {
return nil, fmt.Errorf("internal server error when saving world: %s", err)
} else {
return &roveapi.RegisterResponse{
Account: &roveapi.Account{
Name: acc.Name,
Secret: acc.Data["secret"],
},
}, nil
}
return &rove.RegisterResponse{}, nil
}
func (s *Server) Rover(ctx context.Context, req *rove.RoverRequest) (*rove.RoverResponse, error) {
response := &rove.RoverResponse{}
if len(req.Account) == 0 {
return nil, fmt.Errorf("empty account name")
// Status returns rover information for a gRPC request
func (s *Server) Status(ctx context.Context, req *roveapi.StatusRequest) (response *roveapi.StatusResponse, err error) {
log.Printf("Handling status request: %s\n", req.Account.Name)
} else if resp, err := s.accountant.GetValue(ctx, &accounts.DataKey{Account: req.Account, Key: "rover"}); err != nil {
return nil, fmt.Errorf("gRPC failed to contact accountant: %s", err)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if id, err := uuid.Parse(resp.Value); err != nil {
return nil, fmt.Errorf("account had invalid rover ID: %s", resp.Value)
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
} else if attrib, err := s.world.RoverAttributes(id); err != nil {
return nil, fmt.Errorf("error getting rover attributes: %s", err)
} else if resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover"); err != nil {
return nil, err
} else if rover, err := s.world.GetRover(resp); err != nil {
return nil, fmt.Errorf("error getting rover: %s", err)
} else {
response = &rove.RoverResponse{
Name: attrib.Name,
Position: &rove.Vector{
X: int32(attrib.Pos.X),
Y: int32(attrib.Pos.Y),
var inv []byte
for _, i := range rover.Inventory {
inv = append(inv, byte(i.Type))
}
queued := s.world.RoverCommands(resp)
var logs []*roveapi.Log
for _, log := range rover.Logs {
logs = append(logs, &roveapi.Log{
Text: log.Text,
Time: fmt.Sprintf("%d", log.Time.Unix()), // proto uses strings under the hood for 64bit ints anyway
})
}
response = &roveapi.StatusResponse{
Readings: &roveapi.RoverReadings{
Position: &roveapi.Vector{
X: int32(rover.Pos.X),
Y: int32(rover.Pos.Y),
},
Logs: logs,
Wind: s.world.Wind,
},
Spec: &roveapi.RoverSpecifications{
Name: rover.Name,
Range: int32(rover.Range),
Capacity: int32(rover.Capacity),
MaximumIntegrity: int32(rover.MaximumIntegrity),
MaximumCharge: int32(rover.MaximumCharge),
},
Status: &roveapi.RoverStatus{
Bearing: rover.Bearing,
Inventory: inv,
Integrity: int32(rover.Integrity),
Charge: int32(rover.Charge),
QueuedCommands: queued,
SailPosition: rover.SailPosition,
},
Speed: int32(attrib.Speed),
Range: int32(attrib.Range),
}
}
return response, nil
}
func (s *Server) Radar(ctx context.Context, req *rove.RadarRequest) (*rove.RadarResponse, error) {
if len(req.Account) == 0 {
return nil, fmt.Errorf("empty account name")
// Radar returns the radar information for a rover
func (s *Server) Radar(ctx context.Context, req *roveapi.RadarRequest) (*roveapi.RadarResponse, error) {
log.Printf("Handling radar request: %s\n", req.Account.Name)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
}
response := &rove.RadarResponse{}
response := &roveapi.RadarResponse{}
resp, err := s.accountant.GetValue(ctx, &accounts.DataKey{Account: req.Account, Key: "rover"})
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, fmt.Errorf("gRPC failed to contact accountant: %s", err)
}
return nil, err
if id, err := uuid.Parse(resp.Value); err != nil {
return nil, fmt.Errorf("account had invalid rover ID: %s", resp.Value)
} else if attrib, err := s.world.RoverAttributes(id); err != nil {
} else if rover, err := s.world.GetRover(resp); err != nil {
return nil, fmt.Errorf("error getting rover attributes: %s", err)
} else if radar, err := s.world.RadarFromRover(id); err != nil {
} else if radar, objs, err := s.world.RadarFromRover(resp); err != nil {
return nil, fmt.Errorf("error getting radar from rover: %s", err)
} else {
response.Objects = objs
response.Tiles = radar
response.Range = int32(attrib.Range)
response.Range = int32(rover.Range)
}
return response, nil
}
func (s *Server) Commands(ctx context.Context, req *rove.CommandsRequest) (*rove.CommandsResponse, error) {
if len(req.Account) == 0 {
return nil, fmt.Errorf("empty account")
}
resp, err := s.accountant.GetValue(ctx, &accounts.DataKey{Account: req.Account, Key: "rover"})
// Command issues commands to the world based on a gRPC request
func (s *Server) Command(ctx context.Context, req *roveapi.CommandRequest) (*roveapi.CommandResponse, error) {
log.Printf("Handling command request: %s and %+v\n", req.Account.Name, req.Commands)
if valid, err := s.world.Accountant.VerifySecret(req.Account.Name, req.Account.Secret); err != nil {
return nil, err
} else if !valid {
return nil, fmt.Errorf("Secret incorrect for account %s", req.Account.Name)
}
resp, err := s.world.Accountant.GetValue(req.Account.Name, "rover")
if err != nil {
return nil, err
}
id, err := uuid.Parse(resp.Value)
if err != nil {
return nil, fmt.Errorf("account had invalid rover ID: %s", resp.Value)
}
var cmds []game.Command
for _, c := range req.Commands {
cmds = append(cmds, game.Command{
Bearing: c.Bearing,
Command: c.Command,
Duration: int(c.Duration)})
}
if err := s.world.Enqueue(id, cmds...); err != nil {
if err := s.world.Enqueue(resp, req.Commands...); err != nil {
return nil, err
}
return &rove.CommandsResponse{}, nil
return &roveapi.CommandResponse{}, nil
}

View file

@ -1,22 +1,24 @@
package internal
import (
"context"
"fmt"
"log"
"net"
"os"
"path"
"sync"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/persistence"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/robfig/cron"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"
)
var cert = os.Getenv("CERT_NAME")
const (
// PersistentData will allow the server to load and save it's state
PersistentData = iota
@ -29,20 +31,16 @@ const (
type Server struct {
// Internal state
world *game.World
// Accountant server
accountant accounts.AccountantClient
clientConn *grpc.ClientConn
world *rove.World
// gRPC server
netListener net.Listener
grpcServ *grpc.Server
// Config settings
address string
persistence int
tick int
address string
persistence int
minutesPerTick int
// sync point for sub-threads
sync sync.WaitGroup
@ -72,7 +70,7 @@ func OptionPersistentData() ServerOption {
// 0 means no automatic server tick
func OptionTick(minutes int) ServerOption {
return func(s *Server) {
s.tick = minutes
s.minutesPerTick = minutes
}
}
@ -84,6 +82,7 @@ func NewServer(opts ...ServerOption) *Server {
address: "",
persistence: EphemeralData,
schedule: cron.New(),
world: rove.NewWorld(32),
}
// Apply all options
@ -91,9 +90,6 @@ func NewServer(opts ...ServerOption) *Server {
o(s)
}
// Start small, we can grow the world later
s.world = game.NewWorld(4, 8)
return s
}
@ -103,23 +99,6 @@ func (s *Server) Initialise(fillWorld bool) (err error) {
// Add to our sync
s.sync.Add(1)
// Connect to the accountant
accountantAddress := os.Getenv("ROVE_ACCOUNTANT_GRPC")
if len(accountantAddress) == 0 {
accountantAddress = "localhost:9091"
}
log.Printf("Dialing accountant on %s\n", accountantAddress)
s.clientConn, err = grpc.Dial(accountantAddress, grpc.WithInsecure())
if err != nil {
return err
}
s.accountant = accounts.NewAccountantClient(s.clientConn)
// Spawn a border on the default world
if err := s.world.SpawnWorld(fillWorld); err != nil {
return err
}
// Load the world file
if err := s.LoadWorld(); err != nil {
return err
@ -130,8 +109,22 @@ func (s *Server) Initialise(fillWorld bool) (err error) {
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s.grpcServ = grpc.NewServer()
rove.RegisterRoveServer(s.grpcServ, s)
// Load TLS
var opts []grpc.ServerOption
if len(os.Getenv("NO_TLS")) == 0 {
pem := path.Join("/etc/letsencrypt/live/", cert, "fullchain.pem")
key := path.Join("/etc/letsencrypt/live/", cert, "privkey.pem")
creds, err := credentials.NewServerTLSFromFile(pem, key)
if err != nil {
log.Fatalf("failed to setup TLS: %v", err)
}
opts = append(opts, grpc.Creds(creds))
}
s.grpcServ = grpc.NewServer(opts...)
roveapi.RegisterRoveServer(s.grpcServ, s)
reflection.Register(s.grpcServ)
return nil
}
@ -146,19 +139,21 @@ func (s *Server) Run() {
defer s.sync.Done()
// Set up the schedule if requested
if s.tick != 0 {
if err := s.schedule.AddFunc(fmt.Sprintf("0 */%d * * *", s.tick), func() {
if s.minutesPerTick != 0 {
if err := s.schedule.AddFunc(fmt.Sprintf("0 */%d * * *", s.minutesPerTick), func() {
// Ensure we don't quit during this function
s.sync.Add(1)
defer s.sync.Done()
log.Println("Executing server tick")
// Run the command queues
s.world.ExecuteCommandQueues()
// Tick the world
s.world.Tick()
// Save out the new world state
s.SaveWorld()
if err := s.SaveWorld(); err != nil {
log.Fatalf("Failed to save the world: %s", err)
}
}); err != nil {
log.Fatal(err)
}
@ -181,11 +176,6 @@ func (s *Server) Stop() error {
// Stop the gRPC
s.grpcServ.Stop()
// Close the accountant connection
if err := s.clientConn.Close(); err != nil {
return err
}
return nil
}
@ -198,7 +188,7 @@ func (s *Server) Close() error {
return s.SaveWorld()
}
// Close waits until the server is finished and closes up shop
// StopAndClose waits until the server is finished and closes up shop
func (s *Server) StopAndClose() error {
// Stop the server
if err := s.Stop(); err != nil {
@ -233,33 +223,12 @@ func (s *Server) LoadWorld() error {
return nil
}
// used as the type for the return struct
type BadRequestError struct {
Error string `json:"error"`
}
// SpawnRoverForAccount spawns the rover rover for an account
func (s *Server) SpawnRoverForAccount(account string) (game.RoverAttributes, uuid.UUID, error) {
if inst, err := s.world.SpawnRover(); err != nil {
return game.RoverAttributes{}, uuid.UUID{}, err
} else if attribs, err := s.world.RoverAttributes(inst); err != nil {
return game.RoverAttributes{}, uuid.UUID{}, fmt.Errorf("no attributes found for created rover: %s", err)
} else {
keyval := accounts.DataKeyValue{Account: account, Key: "rover", Value: inst.String()}
_, err := s.accountant.AssignValue(context.Background(), &keyval)
if err != nil {
log.Printf("Failed to assign rover to account, %s", err)
// Try and clear up the rover
if err := s.world.DestroyRover(inst); err != nil {
log.Printf("Failed to destroy rover after failed rover assign: %s", err)
}
return game.RoverAttributes{}, uuid.UUID{}, err
} else {
return attribs, inst, nil
}
func (s *Server) SpawnRoverForAccount(account string) (string, error) {
inst, err := s.world.SpawnRover(account)
if err != nil {
return "", err
}
return inst, nil
}

View file

@ -31,7 +31,7 @@ func TestNewServer_OptionPersistentData(t *testing.T) {
}
func TestServer_Run(t *testing.T) {
os.Setenv("ROVE_ACCOUNTANT_GRPC", "n/a")
os.Setenv("NO_TLS", "1")
server := NewServer()
if server == nil {
t.Error("Failed to create server")
@ -47,7 +47,7 @@ func TestServer_Run(t *testing.T) {
}
func TestServer_RunPersistentData(t *testing.T) {
os.Setenv("ROVE_ACCOUNTANT_GRPC", "n/a")
os.Setenv("NO_TLS", "1")
server := NewServer(OptionPersistentData())
if server == nil {
t.Error("Failed to create server")

View file

@ -4,10 +4,12 @@ import (
"flag"
"fmt"
"log"
"math/rand"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/mdiluz/rove/cmd/rove-server/internal"
"github.com/mdiluz/rove/pkg/persistence"
@ -22,7 +24,11 @@ var data = os.Getenv("DATA_PATH")
// The tick rate of the server in seconds
var tick = os.Getenv("TICK_RATE")
// InnerMain is our main function so tests can run it
func InnerMain() {
// Ensure we've seeded rand
rand.Seed(time.Now().UTC().UnixNano())
flag.Parse()
// Print the version if requested
@ -46,10 +52,14 @@ func InnerMain() {
log.Printf("Initialising version %s...\n", version.Version)
// Set the persistence path
persistence.SetPath(data)
if len(data) == 0 {
log.Fatal("DATA_PATH not set")
} else if err := persistence.SetPath(data); err != nil {
log.Fatal(err)
}
// Convert the tick rate
tickRate := 5
tickRate := 1
if len(tick) > 0 {
var err error
tickRate, err = strconv.Atoi(tick)

View file

@ -3,10 +3,12 @@ package main
import (
"flag"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_InnerMain_Version(t *testing.T) {
flag.Set("version", "1")
assert.NoError(t, flag.Set("version", "1"))
InnerMain()
flag.Set("version", "0")
assert.NoError(t, flag.Set("version", "0"))
}

View file

@ -0,0 +1,70 @@
package internal
import (
"log"
"github.com/mdiluz/rove/proto/roveapi"
)
// Glyph represents the text representation of something in the game
type Glyph byte
const (
// GlyphGroundRock is solid rock ground
GlyphGroundRock = Glyph('-')
// GlyphGroundGravel is loose rocks
GlyphGroundGravel = Glyph(':')
// GlyphGroundSand is sand
GlyphGroundSand = Glyph('~')
// GlyphRoverLive represents a live rover
GlyphRoverLive = Glyph('R')
// GlyphRoverDormant represents a dormant rover
GlyphRoverDormant = Glyph('r')
// GlyphRoverParts represents spare rover parts
GlyphRoverParts = Glyph('*')
// GlyphRockSmall is a small stashable rock
GlyphRockSmall = Glyph('o')
// GlyphRockLarge is a large blocking rock
GlyphRockLarge = Glyph('O')
)
// TileGlyph returns the glyph for this tile type
func TileGlyph(t roveapi.Tile) Glyph {
switch t {
case roveapi.Tile_Rock:
return GlyphGroundRock
case roveapi.Tile_Gravel:
return GlyphGroundGravel
case roveapi.Tile_Sand:
return GlyphGroundSand
}
log.Fatalf("Unknown tile type: %c", t)
return 0
}
// ObjectGlyph returns the glyph for this object type
func ObjectGlyph(o roveapi.Object) Glyph {
switch o {
case roveapi.Object_RoverLive:
return GlyphRoverLive
case roveapi.Object_RockSmall:
return GlyphRockSmall
case roveapi.Object_RoverDormant:
return GlyphRoverDormant
case roveapi.Object_RockLarge:
return GlyphRockLarge
case roveapi.Object_RoverParts:
return GlyphRoverParts
}
log.Fatalf("Unknown object type: %c", o)
return 0
}

View file

@ -1,230 +1,118 @@
package main
import (
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"math"
"os"
"path"
"path/filepath"
"strconv"
"time"
"github.com/mdiluz/rove/pkg/game"
"github.com/mdiluz/rove/pkg/rove"
"github.com/mdiluz/rove/cmd/rove/internal"
"github.com/mdiluz/rove/pkg/version"
"github.com/mdiluz/rove/proto/roveapi"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// Command usage
func printUsage() {
fmt.Fprintf(os.Stderr, "Usage: %s COMMAND [OPTIONS]...\n", os.Args[0])
fmt.Fprintln(os.Stderr, "\nCommands:")
fmt.Fprintln(os.Stderr, "\tstatus \tprints the server status")
fmt.Fprintln(os.Stderr, "\tregister\tregisters an account and stores it (use with -name)")
fmt.Fprintln(os.Stderr, "\tmove \tissues move command to rover")
fmt.Fprintln(os.Stderr, "\tradar \tgathers radar data for the current rover")
fmt.Fprintln(os.Stderr, "\trover \tgets data for current rover")
fmt.Fprintln(os.Stderr, "\tconfig \toutputs the local config info")
fmt.Fprintln(os.Stderr, "\tversion \toutputs version info")
fmt.Fprintln(os.Stderr, "\nOptions:")
flag.PrintDefaults()
}
var home = os.Getenv("HOME")
var defaultDataPath = path.Join(home, ".local/share/")
// Command usage
func printUsage() {
fmt.Fprintln(os.Stderr, "Usage: rove ARG [OPT...]")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Arguments:")
fmt.Fprintln(os.Stderr, "\tversion outputs version")
fmt.Fprintln(os.Stderr, "\thelp outputs this usage text")
fmt.Fprintln(os.Stderr, "\tconfig [HOST] outputs the local config, optionally sets host")
fmt.Fprintln(os.Stderr, "\tserver-status prints the server status")
fmt.Fprintln(os.Stderr, "\tregister NAME registers an account and spawns a rover")
fmt.Fprintln(os.Stderr, "\tradar prints radar data in ASCII form")
fmt.Fprintln(os.Stderr, "\tstatus gets rover status")
fmt.Fprintln(os.Stderr, "\tcommand CMD [VAL...] [REPEAT] sets the command queue, accepts multiple in sequence")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Rover commands:")
fmt.Fprintln(os.Stderr, "\ttoggle toggles the current sail mode")
fmt.Fprintln(os.Stderr, "\tstash stores the object at the rover location in the inventory")
fmt.Fprintln(os.Stderr, "\trepair repairs the rover using inventory item")
fmt.Fprintln(os.Stderr, "\tbroadcast MSG broadcast a simple ASCII triplet to nearby rovers")
fmt.Fprintln(os.Stderr, "\tsalvage salvages a dormant rover for parts")
fmt.Fprintln(os.Stderr, "\ttransfer transfer's control into a dormant rover")
fmt.Fprintln(os.Stderr, "\tupgrade SPEC spends rover parts to upgrade one rover spec (capacity, range, integrity, charge")
fmt.Fprintln(os.Stderr, "\twait waits before performing the next command")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Environment")
fmt.Fprintln(os.Stderr, "\tROVE_USER_DATA path to user data, defaults to "+defaultDataPath)
}
const gRPCport = 9090
// General usage
var host = flag.String("host", "", "path to game host server")
var data = flag.String("data", defaultDataPath, "data location for storage (or $USER_DATA if set)")
// For register command
var name = flag.String("name", "", "used with status command for the account name")
// For the duration command
var duration = flag.Int("duration", 1, "used for the move command duration")
var bearing = flag.String("bearing", "", "used for the move command bearing (compass direction)")
// Account stores data for an account
type Account struct {
Name string
Secret string
}
// Config is used to store internal data
type Config struct {
Host string `json:"host,omitempty"`
Accounts map[string]string `json:"accounts,omitempty"`
Host string
Account Account
}
// verifyID will verify an account ID
func verifyID(id string) error {
if len(id) == 0 {
return fmt.Errorf("no account ID set, must register first")
}
return nil
}
// InnerMain wraps the main function so we can test it
func InnerMain(command string) error {
// Load in the persistent file
var config = Config{
Accounts: make(map[string]string),
}
// ConfigPath returns the configuration path
func ConfigPath() string {
// Allow overriding the data path
var datapath = *data
var override = os.Getenv("USER_DATA")
var datapath = defaultDataPath
var override = os.Getenv("ROVE_USER_DATA")
if len(override) > 0 {
datapath = override
}
datapath = path.Join(datapath, "rove.json")
datapath = path.Join(datapath, "roveapi.json")
return datapath
}
// LoadConfig loads the config from a chosen path
func LoadConfig() (config Config, err error) {
datapath := ConfigPath()
// Create the path if needed
path := filepath.Dir(datapath)
_, err := os.Stat(path)
if os.IsNotExist(err) {
os.MkdirAll(path, os.ModePerm)
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.MkdirAll(path, os.ModePerm); err != nil {
return Config{}, fmt.Errorf("Failed to create data path %s: %s", path, err)
}
} else {
// Read the file
_, err = os.Stat(datapath)
if !os.IsNotExist(err) {
if b, err := ioutil.ReadFile(datapath); err != nil {
return fmt.Errorf("failed to read file %s error: %s", datapath, err)
return Config{}, fmt.Errorf("failed to read file %s error: %s", datapath, err)
} else if len(b) == 0 {
return fmt.Errorf("file %s was empty, assumin fresh data", datapath)
return Config{}, fmt.Errorf("file %s was empty, assumin fresh data", datapath)
} else if err := json.Unmarshal(b, &config); err != nil {
return fmt.Errorf("failed to unmarshal file %s error: %s", datapath, err)
return Config{}, fmt.Errorf("failed to unmarshal file %s error: %s", datapath, err)
}
}
}
// Early bails
switch command {
case "version":
fmt.Println(version.Version)
return nil
case "config":
fmt.Printf("host: %s\taccount: %s\n", config.Host, config.Accounts[config.Host])
return nil
}
// If there's a host set on the command line, override the one in the config
if len(*host) != 0 {
config.Host = *host
}
// If there's still no host, bail
if len(config.Host) == 0 {
return fmt.Errorf("no host set, please set one with -host")
}
// Set up the server
clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), grpc.WithInsecure())
if err != nil {
return err
}
var client = rove.NewRoveClient(clientConn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Handle all the commands
switch command {
case "status":
response, err := client.Status(ctx, &rove.StatusRequest{})
switch {
case err != nil:
return err
default:
fmt.Printf("Ready: %t\n", response.Ready)
fmt.Printf("Version: %s\n", response.Version)
fmt.Printf("Tick: %d\n", response.Tick)
fmt.Printf("Next Tick: %s\n", response.NextTick)
}
case "register":
if len(*name) == 0 {
return fmt.Errorf("must set name with -name")
}
d := rove.RegisterRequest{
Name: *name,
}
_, err := client.Register(ctx, &d)
switch {
case err != nil:
return err
default:
fmt.Printf("Registered account with id: %s\n", *name)
config.Accounts[config.Host] = *name
}
case "move":
d := rove.CommandsRequest{
Account: config.Accounts[config.Host],
Commands: []*rove.Command{
{
Command: game.CommandMove,
Duration: int32(*duration),
Bearing: *bearing,
},
},
}
if err := verifyID(d.Account); err != nil {
return err
}
_, err := client.Commands(ctx, &d)
switch {
case err != nil:
return err
default:
fmt.Printf("Request succeeded\n")
}
case "radar":
dat := rove.RadarRequest{Account: config.Accounts[config.Host]}
if err := verifyID(dat.Account); err != nil {
return err
}
response, err := client.Radar(ctx, &dat)
switch {
case err != nil:
return err
default:
// Print out the radar
game.PrintTiles(response.Tiles)
}
case "rover":
req := rove.RoverRequest{Account: config.Accounts[config.Host]}
if err := verifyID(req.Account); err != nil {
return err
}
response, err := client.Rover(ctx, &req)
switch {
case err != nil:
return err
default:
fmt.Printf("attributes: %+v\n", response)
}
default:
// Print the usage
fmt.Fprintf(os.Stderr, "Error: unknown command %s\n", command)
printUsage()
os.Exit(1)
}
return
}
// SaveConfig saves the config out
func SaveConfig(config Config) error {
// Save out the persistent file
datapath := ConfigPath()
if b, err := json.MarshalIndent(config, "", "\t"); err != nil {
return fmt.Errorf("failed to marshal data error: %s", err)
} else if err := ioutil.WriteFile(datapath, b, os.ModePerm); err != nil {
@ -234,20 +122,302 @@ func InnerMain(command string) error {
return nil
}
// Simple main
func main() {
flag.Usage = printUsage
// checkAccount will verify an account ID
func checkAccount(a Account) error {
if len(a.Name) == 0 {
return fmt.Errorf("no account ID set, must register first")
} else if len(a.Secret) == 0 {
return fmt.Errorf("empty account secret, must register first")
}
return nil
}
// Bail without any args
if len(os.Args) == 1 {
// BearingFromString converts a string to a bearing
func BearingFromString(s string) roveapi.Bearing {
switch s {
case "N":
return roveapi.Bearing_North
case "NE":
return roveapi.Bearing_NorthEast
case "E":
return roveapi.Bearing_East
case "SE":
return roveapi.Bearing_SouthEast
case "S":
return roveapi.Bearing_South
case "SW":
return roveapi.Bearing_SouthWest
case "W":
return roveapi.Bearing_West
case "NW":
return roveapi.Bearing_NorthWest
}
return roveapi.Bearing_BearingUnknown
}
// InnerMain wraps the main function so we can test it
func InnerMain(command string, args ...string) error {
// Early simple bails
switch command {
case "help":
printUsage()
return nil
case "version":
fmt.Println(version.Version)
return nil
}
// Load in the persistent file
config, err := LoadConfig()
if err != nil {
return err
}
// Run config command before server needed
if command == "config" {
if len(args) > 0 {
config.Host = args[0]
}
fmt.Printf("host: %s\taccount: %s\n", config.Host, config.Account)
return SaveConfig(config)
}
// If there's still no host, bail
if len(config.Host) == 0 {
return fmt.Errorf("no host set in %s, set one with '%s config {HOST}'", ConfigPath(), os.Args[0])
}
var opts []grpc.DialOption
if len(os.Getenv("NO_TLS")) == 0 {
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
} else {
opts = append(opts, grpc.WithInsecure())
}
// Set up the server
clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), opts...)
if err != nil {
return err
}
var client = roveapi.NewRoveClient(clientConn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Handle all the commands
switch command {
case "server-status":
response, err := client.ServerStatus(ctx, &roveapi.ServerStatusRequest{})
switch {
case err != nil:
return err
default:
fmt.Printf("Ready: %t\n", response.Ready)
fmt.Printf("Version: %s\n", response.Version)
fmt.Printf("Tick Rate: %d\n", response.TickRate)
fmt.Printf("Current Tick: %d\n", response.CurrentTick)
fmt.Printf("Next Tick: %s\n", response.NextTick)
}
case "register":
if len(args) == 0 || len(args[0]) == 0 {
return fmt.Errorf("must pass name to 'register'")
}
resp, err := client.Register(ctx, &roveapi.RegisterRequest{
Name: args[0],
})
switch {
case err != nil:
return err
default:
fmt.Printf("Registered account with id: %s\n", resp.Account.Name)
config.Account.Name = resp.Account.Name
config.Account.Secret = resp.Account.Secret
}
case "command":
if err := checkAccount(config.Account); err != nil {
return err
} else if len(args) == 0 {
return fmt.Errorf("must pass commands to 'commands'")
}
// Iterate through each command
var commands []*roveapi.Command
for i := 0; i < len(args); i++ {
var cmd *roveapi.Command
switch args[i] {
case "turn":
i++
if len(args) == i {
return fmt.Errorf("turn command must be passed a compass bearing")
}
b := BearingFromString(args[i])
if b == roveapi.Bearing_BearingUnknown {
return fmt.Errorf("turn command must be given a valid bearing %s", args[i])
}
cmd = &roveapi.Command{
Command: roveapi.CommandType_turn,
Bearing: b,
}
case "broadcast":
i++
if len(args) == i {
return fmt.Errorf("broadcast command must be passed an ASCII triplet")
} else if len(args[i]) > 3 {
return fmt.Errorf("broadcast command must be given ASCII triplet of 3 or less: %s", args[i])
}
cmd = &roveapi.Command{
Command: roveapi.CommandType_broadcast,
Data: []byte(args[i]),
}
case "upgrade":
i++
if len(args) == i {
return fmt.Errorf("upgrade command must be passed a spec to upgrade")
}
var u roveapi.RoverUpgrade
switch args[i] {
case "capacity":
u = roveapi.RoverUpgrade_Capacity
case "range":
u = roveapi.RoverUpgrade_Range
case "integrity":
u = roveapi.RoverUpgrade_MaximumIntegrity
case "charge":
u = roveapi.RoverUpgrade_MaximumCharge
default:
return fmt.Errorf("upgrade command must be passed a known upgrade spec")
}
cmd = &roveapi.Command{
Command: roveapi.CommandType_upgrade,
Upgrade: u,
}
default:
// By default just use the command literally
cmd = &roveapi.Command{
Command: roveapi.CommandType(roveapi.CommandType_value[args[i]]),
}
}
// Try and convert the next command to a number
number := 0
if len(args) > i+1 {
num, err := strconv.Atoi(args[i+1])
if err == nil {
number = num
i++
}
}
cmd.Repeat = int32(number)
commands = append(commands, cmd)
}
_, err := client.Command(ctx, &roveapi.CommandRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
Commands: commands,
})
switch {
case err != nil:
return err
default:
fmt.Printf("Request succeeded\n")
}
case "radar":
if err := checkAccount(config.Account); err != nil {
return err
}
response, err := client.Radar(ctx, &roveapi.RadarRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
})
switch {
case err != nil:
return err
default:
// Print out the radar
num := int(math.Sqrt(float64(len(response.Tiles))))
for j := num - 1; j >= 0; j-- {
for i := 0; i < num; i++ {
t := response.Tiles[i+num*j]
o := response.Objects[i+num*j]
if o != roveapi.Object_ObjectUnknown {
fmt.Printf("%c", internal.ObjectGlyph(o))
} else {
fmt.Printf("%c", internal.TileGlyph(t))
}
}
fmt.Print("\n")
}
}
case "status":
if err := checkAccount(config.Account); err != nil {
return err
}
response, err := client.Status(ctx, &roveapi.StatusRequest{
Account: &roveapi.Account{
Name: config.Account.Name,
Secret: config.Account.Secret,
},
})
switch {
case err != nil:
return err
default:
fmt.Printf("rover info: %+v\n", response)
}
default:
// Print the usage
fmt.Fprintf(os.Stderr, "Error: unknown command %s\n", command)
printUsage()
os.Exit(1)
}
flag.CommandLine.Parse(os.Args[2:])
return SaveConfig(config)
}
// Simple main
func main() {
// Bail without any args
if len(os.Args) == 1 {
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "m mm mmm m m mmm")
fmt.Fprintln(os.Stderr, "#\" \" #\" \"# \"m m\" #\" #")
fmt.Fprintln(os.Stderr, "# # # #m# #\"\"\"\"")
fmt.Fprintln(os.Stderr, "# \"#m#\" # \"#mm\"")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintln(os.Stderr, "Rove is an asychronous nomadic game about exploring a planet as part of a loose community.")
fmt.Fprintln(os.Stderr, "Visit https://mdiluz.github.io/rove/ for more information.")
fmt.Fprintf(os.Stderr, "\n")
printUsage()
os.Exit(1)
}
// Run the inner main
if err := InnerMain(os.Args[1]); err != nil {
if err := InnerMain(os.Args[1], os.Args[2:]...); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}

View file

@ -3,10 +3,9 @@
package main
import (
"flag"
"io/ioutil"
"log"
"os"
"path"
"testing"
"github.com/google/uuid"
@ -14,44 +13,55 @@ import (
)
func Test_InnerMain(t *testing.T) {
os.Setenv("NO_TLS", "1")
// Use temporary local user data
tmp, err := ioutil.TempDir(os.TempDir(), "rove-")
assert.NoError(t, err)
os.Setenv("ROVE_USER_DATA", tmp)
// Used for configuring this test
var address = os.Getenv("ROVE_GRPC")
if len(address) == 0 {
log.Fatal("Must set $ROVE_GRPC")
}
// Set up the flags to act locally and use a temporary file
flag.Set("data", path.Join(os.TempDir(), uuid.New().String()))
// First attempt should error without a host
assert.Error(t, InnerMain("server-status"))
// First attempt should error
assert.Error(t, InnerMain("status"))
// Now set the host
flag.Set("host", address)
// No error now as we have a host
assert.NoError(t, InnerMain("status"))
// Set the host in the config
assert.NoError(t, InnerMain("config", address))
assert.NoError(t, InnerMain("server-status"))
// Register should fail without a name
assert.Error(t, InnerMain("register"))
// These methods should fail without an account
assert.Error(t, InnerMain("move"))
assert.Error(t, InnerMain("radar"))
assert.Error(t, InnerMain("rover"))
assert.Error(t, InnerMain("status"))
// Now set the name
flag.Set("name", uuid.New().String())
// Perform the register
assert.NoError(t, InnerMain("register"))
assert.NoError(t, InnerMain("register", uuid.New().String()))
// These should now work
assert.NoError(t, InnerMain("radar"))
assert.NoError(t, InnerMain("rover"))
assert.NoError(t, InnerMain("status"))
// Move should work with arguments
flag.Set("bearing", "N")
flag.Set("duration", "1")
assert.NoError(t, InnerMain("move"))
// Commands should fail with no commands
assert.Error(t, InnerMain("command"))
// Give it commands
assert.NoError(t, InnerMain("command", "toggle"))
assert.NoError(t, InnerMain("command", "stash"))
assert.NoError(t, InnerMain("command", "repair"))
assert.NoError(t, InnerMain("command", "upgrade", "capacity"))
assert.NoError(t, InnerMain("command", "broadcast", "abc"))
assert.NoError(t, InnerMain("command", "wait", "10"))
assert.NoError(t, InnerMain("command", "wait", "1", "turn", "NW", "toggle", "broadcast", "zyx"))
// Give it malformed commands
assert.Error(t, InnerMain("command", "unknown"))
assert.Error(t, InnerMain("command", "broadcast"))
assert.Error(t, InnerMain("command", "upgrade"))
assert.Error(t, InnerMain("command", "1"))
}

View file

@ -0,0 +1,36 @@
{
"app-id": "io.github.mdiluz.Rove",
"runtime": "org.freedesktop.Platform",
"runtime-version": "19.08",
"sdk": "org.freedesktop.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.golang"
],
"finish-args" : [
"--share=network"
],
"command": "rove",
"modules": [
{
"name": "rove",
"buildsystem": "simple",
"build-options": {
"env": {
"GOBIN": "/app/bin/"
},
"build-args": [
"--share=network"
]
},
"build-commands" : [
". /usr/lib/sdk/golang/enable.sh; make install"
],
"sources": [
{
"type": "dir",
"path": ".."
}
]
}
]
}

32
docker-compose-test.yml Normal file
View file

@ -0,0 +1,32 @@
version: '3'
services:
rove-test-server:
build:
context: .
dockerfile: Dockerfile
image: rove:latest
ports:
- "9090:9090"
environment:
- PORT=9090
- DATA_PATH=/tmp/
- WORDS_FILE=data/words_alpha.txt
- TICK_RATE=10
- NO_TLS=1
command: [ "./rove-server"]
rove-tests:
depends_on: [ rove-test-server ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
environment:
- ROVE_GRPC=rove-test-server
command: [ "./script/wait-for-it.sh", "rove-test-server:9090", "--", "go", "test", "-v", "./...", "--tags=integration", "-cover", "-coverprofile=/mnt/coverage-data/c.out", "-count", "1" ]
volumes:
- /tmp/coverage-data:/mnt/coverage-data:rw

View file

@ -4,32 +4,7 @@ volumes:
persistent-data:
services:
rove-accountant:
build:
context: .
dockerfile: Dockerfile
image: rove:latest
ports:
- "9091:9091"
environment:
- PORT=9091
- DATA_PATH=/mnt/rove-server
volumes:
- persistent-data:/mnt/rove-server:rw
command: [ ./rove-accountant ]
rove-docs:
build:
context: .
dockerfile: Dockerfile.docs
image: rove-docs:latest
ports:
- "80:80"
environment:
- PORT=80
rove-server:
depends_on: [ rove-accountant ]
build:
context: .
dockerfile: Dockerfile
@ -39,38 +14,12 @@ services:
environment:
- PORT=9090
- DATA_PATH=/mnt/rove-server
- ROVE_ACCOUNTANT_GRPC=rove-accountant:9091
- WORDS_FILE=data/words_alpha.txt
- TICK_RATE=3
- CERT_NAME=${CERT_NAME}
volumes:
- persistent-data:/mnt/rove-server:rw
command: [ "./script/wait-for-it.sh", "rove-accountant:9091", "--", "./rove-server"]
rove:
depends_on: [ rove-server, rove-docs ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
ports:
- "8080:8080"
environment:
- PORT=8080
- ROVE_GRPC=rove-server:9090
command: [ "./script/wait-for-it.sh", "rove-server:9090", "--", "./rove-reverse-proxy" ]
rove-tests:
depends_on: [ rove ]
build:
context: .
dockerfile: Dockerfile
image: rove:latest
environment:
- ROVE_ACCOUNTANT_GRPC=rove-accountant:9091
- ROVE_HTTP=rove
- ROVE_GRPC=rove-server
command: [ "./script/wait-for-it.sh", "rove:8080", "--", "go", "test", "-v", "./...", "--tags=integration", "-cover", "-coverprofile=/mnt/coverage-data/c.out", "-count", "1" ]
volumes:
- /tmp/coverage-data:/mnt/coverage-data:rw
- /etc/letsencrypt/:/etc/letsencrypt/
command: [ "./rove-server"]

61
docs/README.md Normal file
View file

@ -0,0 +1,61 @@
Rove
=====
Rove is an asynchronous nomadic game about exploring a planet as part of a loose community.
-------------------------------------------
## Core gameplay
Remotely explore the surface of a planet with an upgradable and customisable rover. Send commands to be executed asynchronously, view the rover's radar, and communicate and coordinate with other nearby rovers.
### Key Components
* Navigate an expansive world
* Collect resources to repair and upgrade
* Keep the rover batteries charged as you explore
* Help other players on their journey
* Explore north to discover more
-------------------------------------------
## Installing
On Ubuntu:
```
$ snap install rove
```
Elsewhere (with [go](https://golang.org/doc/install) installed)
```
go get github.com/mdiluz/rove
cd $GOPATH/src/github.com/mdiluz/rove/
make install
```
-------------------------------------------
### Implementation Details
`rove-server` hosts the game world and a gRPC server to allow users to interact from any client.
`rove` is a basic example command-line client that allows for simple play, to explore it's usage, see the output of `rove help`
-------------------------------------------
### "Find the fun" issues to solve
* What kinds of progression/upgrades exist?
* How does the game encourage cooperation?
* How would the gameplay prevent griefing?
* What drives the exploration?
-------------------------------------------
### Key ideas left to integrate
* Feeling “known for” something - the person who did X thing. Limit number of X things that can be done, possibly over time.
* A significant aspect of failure - failing must be a focus of the game. Winning the game might actually be failing in some specific way.
* A clear and well defined investment vs. payoff curve.
* Not an infinite game, let the game have a point where youre done and can move on.

1
docs/_config.yml Normal file
View file

@ -0,0 +1 @@
theme: jekyll-theme-merlot

View file

@ -1,86 +0,0 @@
Rove
=====
An asynchronous nomadic game about exploring a planet as part of a loose
community.
-------------------------------------------
## The Basics
### Core
Control a rover on the surface of the planet using a remote control interface.
Commands are sent and happen asynchronously, and the rover feeds back information about position and surroundings, as well as photos.
### Goal
To reach the pole.
### General
Movement is slow and sometimes dangerous. Hazards damage the rover.
Resources can be collected to fix and upgrade the rover.
Rovers recharge power during the day.
Enough collected resources allow you to create and fire a new rover a significant distance in any direction, losing control of the current one and leaving it dormant.
Finding a dormant rover gives you a choice - scrap it to gain minor resources, or fire it a distance just like a new rover, taking control of it.
“Dying” triggers a self destruct and fires a new basic rover in a random direction towards the equator
## Multiplayer
The planet itself and things that happen on it are persistent. Players can view each other, and use very rudimentary signals.
Dormant rovers store full history of travel, owners, and keep damage, improvements and resources.
Players have no other forms of direct communication.
Players can view progress of all rovers attached to their name.
Limit too many players in one location with a simple interference mechanic - only a certain density can exist at once to operate properly, additional players cant move within range.
-------------------------------------------
### Implementation
Two functional parts
A server that receives the commands, sends out data, and handles interactions between players.
An app, or apps, that interface with the server to let you control and view rover information
-------------------------------------------
### To Solve
#### What kinds of progression/upgrades exist?
Needs a very simple set of rover internals defined, each of which can be upgraded.
#### How does the game encourage lateral movement?
Could simply be the terrain is constructed in very lateral ways, blocking progress frequently
#### How does the game encourage cooperation?
How exactly would a time delay mechanic enhance the experience?
Currently its just to make the multiplayer easier to use, and to make interactions a little more complicated. The game could limit the number of bytes (commands) you can send over time.
#### How would the gameplay prevent griefing?
-------------------------------------------
### Key ideas left to integrate
Feeling “known for” something - the person who did X thing. Limit number of X things that can be done, possibly over time.
Build up a certain level of knowledge and ownership of a place, but then destroy it or give it up. Or build up a character and then leave it behind.
A significant aspect of failure - failing must be a focus of the game. Winning the game might actually be failing in some specific way.
A clear and well defined investment vs. payoff curve.
Not an infinite game, let the game have a point where youre done and can move on.

View file

@ -1,29 +0,0 @@
Proof Of Concept
================
For a proof of concept we'll implement a very small subset of the final features
### Core Features (done)
* Create an account
* Control an rover in a 2D map
* Query for what the rover can see
* Persistent accounts and world
* Multiple users
* Populate map with locations/objects
* Commands happen in "real time"
### Stretch goals
* Rover inventory
* Rover internals
* Basic survival mechanics
### Key missing features
* No token security (very hackable)
* No time dilation effects
* No global coordinates
* No render of rover camera view
* No rover replacement mechanic
* No advanced app/interface

17
go.mod
View file

@ -3,16 +3,19 @@ module github.com/mdiluz/rove
go 1.14
require (
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.4.2
github.com/google/uuid v1.1.1
github.com/grpc-ecosystem/grpc-gateway v1.14.6
github.com/onsi/ginkgo v1.12.3 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/ojrac/opensimplex-go v1.0.1
github.com/robfig/cron v1.2.0
github.com/stretchr/testify v1.6.0
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
google.golang.org/grpc v1.29.1
google.golang.org/protobuf v1.23.0
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/grpc v1.30.0
google.golang.org/protobuf v1.25.0
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)

66
go.sum
View file

@ -1,19 +1,23 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@ -27,6 +31,7 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -34,20 +39,19 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.14.6 h1:8ERzHx8aj1Sc47mu9n/AksaKCSWrMchFtkdrS4BIj5o=
github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.12.3 h1:+RYp9QczoWz9zfUyLP/5SLXQVhfr6gZOoKGfQqHuLZQ=
github.com/onsi/ginkgo v1.12.3/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/ojrac/opensimplex-go v1.0.1 h1:XslvpLP6XqQSATUtsOnGBYtFPw7FQ6h6y0ihjVeOLHo=
github.com/ojrac/opensimplex-go v1.0.1/go.mod h1:MoSgj04tZpH8U0RefZabnHV2AbLgv/2mo3hLJtWqSEs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -57,8 +61,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d h1:b7oHBI6TgTdCDuqTijsVldzlh+6cfQpdYLz1EKtCAoY=
github.com/tjarratt/babble v0.0.0-20191209142150-eecdf8c2339d/go.mod h1:O5hBrCGqzfb+8WyY8ico2AyQau7XQwAfEQeEQ5/5V9E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -66,34 +68,31 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -109,29 +108,36 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2El
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884 h1:fiNLklpBwWK1mth30Hlwk+fcdBmIALlgF5iy77O37Ig=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

28
pkg/accounts/accounts.go Normal file
View file

@ -0,0 +1,28 @@
package accounts
// Accountant decribes something that stores accounts and account values
type Accountant interface {
// RegisterAccount will register a new account and return it's info
RegisterAccount(name string) (acc Account, err error)
// AssignData stores a custom account key value pair
AssignData(account string, key string, value string) error
// GetValue returns custom account data for a specific key
GetValue(account string, key string) (string, error)
// VerifySecret will verify whether the account secret matches with the
VerifySecret(account string, secret string) (bool, error)
// GetSecret gets the secret associated with an account
GetSecret(account string) (string, error)
}
// Account represents a registered user
type Account struct {
// Name simply describes the account and must be unique
Name string
// Data represents internal account data
Data map[string]string
}

View file

@ -1,663 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.23.0
// protoc v3.6.1
// source: accounts/accounts.proto
package accounts
import (
context "context"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// This is a compile-time assertion that a sufficiently up-to-date version
// of the legacy proto package is being used.
const _ = proto.ProtoPackageIsVersion4
// RegisterInfo contains the information needed to register an account
type RegisterInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The name for the account, must be unique
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *RegisterInfo) Reset() {
*x = RegisterInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_accounts_accounts_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RegisterInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RegisterInfo) ProtoMessage() {}
func (x *RegisterInfo) ProtoReflect() protoreflect.Message {
mi := &file_accounts_accounts_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RegisterInfo.ProtoReflect.Descriptor instead.
func (*RegisterInfo) Descriptor() ([]byte, []int) {
return file_accounts_accounts_proto_rawDescGZIP(), []int{0}
}
func (x *RegisterInfo) GetName() string {
if x != nil {
return x.Name
}
return ""
}
// RegisterResponse is the response information from registering an account
type RegisterResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *RegisterResponse) Reset() {
*x = RegisterResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_accounts_accounts_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RegisterResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RegisterResponse) ProtoMessage() {}
func (x *RegisterResponse) ProtoReflect() protoreflect.Message {
mi := &file_accounts_accounts_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead.
func (*RegisterResponse) Descriptor() ([]byte, []int) {
return file_accounts_accounts_proto_rawDescGZIP(), []int{1}
}
// DataKeyValue represents a simple key value pair to assign to an account
type DataKeyValue struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The account to assign the new key value pair to
Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"`
// The key value pair to assign
Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *DataKeyValue) Reset() {
*x = DataKeyValue{}
if protoimpl.UnsafeEnabled {
mi := &file_accounts_accounts_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DataKeyValue) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DataKeyValue) ProtoMessage() {}
func (x *DataKeyValue) ProtoReflect() protoreflect.Message {
mi := &file_accounts_accounts_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DataKeyValue.ProtoReflect.Descriptor instead.
func (*DataKeyValue) Descriptor() ([]byte, []int) {
return file_accounts_accounts_proto_rawDescGZIP(), []int{2}
}
func (x *DataKeyValue) GetAccount() string {
if x != nil {
return x.Account
}
return ""
}
func (x *DataKeyValue) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *DataKeyValue) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
// DataKeyResponse is a simple response
type DataKeyResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *DataKeyResponse) Reset() {
*x = DataKeyResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_accounts_accounts_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DataKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DataKeyResponse) ProtoMessage() {}
func (x *DataKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_accounts_accounts_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DataKeyResponse.ProtoReflect.Descriptor instead.
func (*DataKeyResponse) Descriptor() ([]byte, []int) {
return file_accounts_accounts_proto_rawDescGZIP(), []int{3}
}
// DataKey describes a simple key value with an account, for fetching
type DataKey struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The account to fetch data for
Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"`
// The key to fetch
Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"`
}
func (x *DataKey) Reset() {
*x = DataKey{}
if protoimpl.UnsafeEnabled {
mi := &file_accounts_accounts_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DataKey) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DataKey) ProtoMessage() {}
func (x *DataKey) ProtoReflect() protoreflect.Message {
mi := &file_accounts_accounts_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DataKey.ProtoReflect.Descriptor instead.
func (*DataKey) Descriptor() ([]byte, []int) {
return file_accounts_accounts_proto_rawDescGZIP(), []int{4}
}
func (x *DataKey) GetAccount() string {
if x != nil {
return x.Account
}
return ""
}
func (x *DataKey) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
// DataResponse describes a data fetch response
type DataResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// The value of the key
Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *DataResponse) Reset() {
*x = DataResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_accounts_accounts_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *DataResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DataResponse) ProtoMessage() {}
func (x *DataResponse) ProtoReflect() protoreflect.Message {
mi := &file_accounts_accounts_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DataResponse.ProtoReflect.Descriptor instead.
func (*DataResponse) Descriptor() ([]byte, []int) {
return file_accounts_accounts_proto_rawDescGZIP(), []int{5}
}
func (x *DataResponse) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
var File_accounts_accounts_proto protoreflect.FileDescriptor
var file_accounts_accounts_proto_rawDesc = []byte{
0x0a, 0x17, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x73, 0x22, 0x22, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49,
0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x12, 0x0a, 0x10, 0x52, 0x65, 0x67, 0x69, 0x73,
0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x50, 0x0a, 0x0c, 0x44,
0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61,
0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x11, 0x0a,
0x0f, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x35, 0x0a, 0x07, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x61,
0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x24, 0x0a, 0x0c, 0x44, 0x61, 0x74, 0x61, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0xcb, 0x01,
0x0a, 0x0a, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x61, 0x6e, 0x74, 0x12, 0x40, 0x0a, 0x08,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x16, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f,
0x1a, 0x1a, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x52, 0x65, 0x67, 0x69,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42,
0x0a, 0x0b, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x2e,
0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x19, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73,
0x2e, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x12, 0x37, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x11,
0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x4b, 0x65,
0x79, 0x1a, 0x16, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x44, 0x61, 0x74,
0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x25, 0x5a, 0x23, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x64, 0x69, 0x6c, 0x75, 0x7a,
0x2f, 0x72, 0x6f, 0x76, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_accounts_accounts_proto_rawDescOnce sync.Once
file_accounts_accounts_proto_rawDescData = file_accounts_accounts_proto_rawDesc
)
func file_accounts_accounts_proto_rawDescGZIP() []byte {
file_accounts_accounts_proto_rawDescOnce.Do(func() {
file_accounts_accounts_proto_rawDescData = protoimpl.X.CompressGZIP(file_accounts_accounts_proto_rawDescData)
})
return file_accounts_accounts_proto_rawDescData
}
var file_accounts_accounts_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_accounts_accounts_proto_goTypes = []interface{}{
(*RegisterInfo)(nil), // 0: accounts.RegisterInfo
(*RegisterResponse)(nil), // 1: accounts.RegisterResponse
(*DataKeyValue)(nil), // 2: accounts.DataKeyValue
(*DataKeyResponse)(nil), // 3: accounts.DataKeyResponse
(*DataKey)(nil), // 4: accounts.DataKey
(*DataResponse)(nil), // 5: accounts.DataResponse
}
var file_accounts_accounts_proto_depIdxs = []int32{
0, // 0: accounts.Accountant.Register:input_type -> accounts.RegisterInfo
2, // 1: accounts.Accountant.AssignValue:input_type -> accounts.DataKeyValue
4, // 2: accounts.Accountant.GetValue:input_type -> accounts.DataKey
1, // 3: accounts.Accountant.Register:output_type -> accounts.RegisterResponse
3, // 4: accounts.Accountant.AssignValue:output_type -> accounts.DataKeyResponse
5, // 5: accounts.Accountant.GetValue:output_type -> accounts.DataResponse
3, // [3:6] is the sub-list for method output_type
0, // [0:3] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_accounts_accounts_proto_init() }
func file_accounts_accounts_proto_init() {
if File_accounts_accounts_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_accounts_accounts_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RegisterInfo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_accounts_accounts_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*RegisterResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_accounts_accounts_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DataKeyValue); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_accounts_accounts_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DataKeyResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_accounts_accounts_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DataKey); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_accounts_accounts_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*DataResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_accounts_accounts_proto_rawDesc,
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_accounts_accounts_proto_goTypes,
DependencyIndexes: file_accounts_accounts_proto_depIdxs,
MessageInfos: file_accounts_accounts_proto_msgTypes,
}.Build()
File_accounts_accounts_proto = out.File
file_accounts_accounts_proto_rawDesc = nil
file_accounts_accounts_proto_goTypes = nil
file_accounts_accounts_proto_depIdxs = nil
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConnInterface
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion6
// AccountantClient is the client API for Accountant service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type AccountantClient interface {
// Register should create a new account in the database
// It will return an error if the account already exists
Register(ctx context.Context, in *RegisterInfo, opts ...grpc.CallOption) (*RegisterResponse, error)
// AssignValue assigns a key-value pair to an account, or overwrites an existing key
AssignValue(ctx context.Context, in *DataKeyValue, opts ...grpc.CallOption) (*DataKeyResponse, error)
// GetValue will get the value for a key for an account
GetValue(ctx context.Context, in *DataKey, opts ...grpc.CallOption) (*DataResponse, error)
}
type accountantClient struct {
cc grpc.ClientConnInterface
}
func NewAccountantClient(cc grpc.ClientConnInterface) AccountantClient {
return &accountantClient{cc}
}
func (c *accountantClient) Register(ctx context.Context, in *RegisterInfo, opts ...grpc.CallOption) (*RegisterResponse, error) {
out := new(RegisterResponse)
err := c.cc.Invoke(ctx, "/accounts.Accountant/Register", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *accountantClient) AssignValue(ctx context.Context, in *DataKeyValue, opts ...grpc.CallOption) (*DataKeyResponse, error) {
out := new(DataKeyResponse)
err := c.cc.Invoke(ctx, "/accounts.Accountant/AssignValue", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *accountantClient) GetValue(ctx context.Context, in *DataKey, opts ...grpc.CallOption) (*DataResponse, error) {
out := new(DataResponse)
err := c.cc.Invoke(ctx, "/accounts.Accountant/GetValue", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AccountantServer is the server API for Accountant service.
type AccountantServer interface {
// Register should create a new account in the database
// It will return an error if the account already exists
Register(context.Context, *RegisterInfo) (*RegisterResponse, error)
// AssignValue assigns a key-value pair to an account, or overwrites an existing key
AssignValue(context.Context, *DataKeyValue) (*DataKeyResponse, error)
// GetValue will get the value for a key for an account
GetValue(context.Context, *DataKey) (*DataResponse, error)
}
// UnimplementedAccountantServer can be embedded to have forward compatible implementations.
type UnimplementedAccountantServer struct {
}
func (*UnimplementedAccountantServer) Register(context.Context, *RegisterInfo) (*RegisterResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Register not implemented")
}
func (*UnimplementedAccountantServer) AssignValue(context.Context, *DataKeyValue) (*DataKeyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AssignValue not implemented")
}
func (*UnimplementedAccountantServer) GetValue(context.Context, *DataKey) (*DataResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetValue not implemented")
}
func RegisterAccountantServer(s *grpc.Server, srv AccountantServer) {
s.RegisterService(&_Accountant_serviceDesc, srv)
}
func _Accountant_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RegisterInfo)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountantServer).Register(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/accounts.Accountant/Register",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountantServer).Register(ctx, req.(*RegisterInfo))
}
return interceptor(ctx, in, info, handler)
}
func _Accountant_AssignValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DataKeyValue)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountantServer).AssignValue(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/accounts.Accountant/AssignValue",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountantServer).AssignValue(ctx, req.(*DataKeyValue))
}
return interceptor(ctx, in, info, handler)
}
func _Accountant_GetValue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DataKey)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AccountantServer).GetValue(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/accounts.Accountant/GetValue",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AccountantServer).GetValue(ctx, req.(*DataKey))
}
return interceptor(ctx, in, info, handler)
}
var _Accountant_serviceDesc = grpc.ServiceDesc{
ServiceName: "accounts.Accountant",
HandlerType: (*AccountantServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Register",
Handler: _Accountant_Register_Handler,
},
{
MethodName: "AssignValue",
Handler: _Accountant_AssignValue_Handler,
},
{
MethodName: "GetValue",
Handler: _Accountant_GetValue_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "accounts/accounts.proto",
}

View file

@ -1,4 +1,4 @@
package internal
package accounts
import (
"testing"
@ -8,7 +8,7 @@ import (
func TestNewAccountant(t *testing.T) {
// Very basic verify here for now
accountant := NewAccountant()
accountant := NewSimpleAccountant()
if accountant == nil {
t.Error("Failed to create accountant")
}
@ -16,7 +16,7 @@ func TestNewAccountant(t *testing.T) {
func TestAccountant_RegisterAccount(t *testing.T) {
accountant := NewAccountant()
accountant := NewSimpleAccountant()
// Start by making two accounts
@ -44,10 +44,7 @@ func TestAccountant_RegisterAccount(t *testing.T) {
}
func TestAccountant_AssignGetData(t *testing.T) {
accountant := NewAccountant()
if len(accountant.Accounts) != 0 {
t.Error("New accountant created with non-zero account number")
}
accountant := NewSimpleAccountant()
name := uuid.New().String()
a, err := accountant.RegisterAccount(name)

View file

@ -0,0 +1,90 @@
package accounts
import (
"fmt"
"time"
"github.com/google/uuid"
)
// SimpleAccountant manages a set of accounts
type SimpleAccountant struct {
Accounts map[string]Account
}
// NewSimpleAccountant creates a new accountant
func NewSimpleAccountant() Accountant {
return &SimpleAccountant{
Accounts: make(map[string]Account),
}
}
// RegisterAccount adds an account to the set of internal accounts
func (a *SimpleAccountant) RegisterAccount(name string) (acc Account, err error) {
// Set up the account info
acc.Name = name
acc.Data = make(map[string]string)
// Verify this acount isn't already registered
for _, a := range a.Accounts {
if a.Name == acc.Name {
return Account{}, fmt.Errorf("account name already registered: %s", a.Name)
}
}
// Set the creation time
acc.Data["created"] = time.Now().String()
// Create a secret
acc.Data["secret"] = uuid.New().String()
// Simply add the account to the map
a.Accounts[acc.Name] = acc
return
}
// VerifySecret verifies if an account secret is correct
func (a *SimpleAccountant) VerifySecret(account string, secret string) (bool, error) {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
return this.Data["secret"] == secret, nil
}
return false, fmt.Errorf("no account found for id: %s", account)
}
// GetSecret gets the internal secret
func (a *SimpleAccountant) GetSecret(account string) (string, error) {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
return this.Data["secret"], nil
}
return "", fmt.Errorf("no account found for id: %s", account)
}
// AssignData assigns data to an account
func (a *SimpleAccountant) AssignData(account string, key string, value string) error {
// Find the account matching the ID
if this, ok := a.Accounts[account]; ok {
this.Data[key] = value
a.Accounts[account] = this
} else {
return fmt.Errorf("no account found for id: %s", account)
}
return nil
}
// GetValue gets the rover rover for the account
func (a *SimpleAccountant) GetValue(account string, key string) (string, error) {
// Find the account matching the ID
this, ok := a.Accounts[account]
if !ok {
return "", fmt.Errorf("no account found for id: %s", account)
}
return this.Data[key], nil
}

View file

@ -1,195 +0,0 @@
package atlas
import (
"fmt"
"log"
"math/rand"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/pkg/vector"
)
// Chunk represents a fixed square grid of tiles
type Chunk struct {
// Tiles represents the tiles within the chunk
Tiles []byte `json:"tiles"`
}
// Atlas represents a grid of Chunks
type Atlas struct {
// Chunks represents all chunks in the world
// This is intentionally not a 2D array so it can be expanded in all directions
Chunks []Chunk `json:"chunks"`
// size is the current width/height of the given atlas
Size int `json:"size"`
// ChunkSize is the dimensions of each chunk
ChunkSize int `json:"chunksize"`
}
// NewAtlas creates a new empty atlas
func NewAtlas(size, chunkSize int) Atlas {
if size%2 != 0 {
log.Fatal("atlas size must always be even")
}
a := Atlas{
Size: size,
Chunks: make([]Chunk, size*size),
ChunkSize: chunkSize,
}
// Initialise all the chunks
for i := range a.Chunks {
tiles := make([]byte, chunkSize*chunkSize)
for i := 0; i < len(tiles); i++ {
tiles[i] = TileEmpty
}
a.Chunks[i] = Chunk{
Tiles: tiles,
}
}
return a
}
// SpawnRocks peppers the world with rocks
func (a *Atlas) SpawnRocks() error {
extent := a.ChunkSize * (a.Size / 2)
// Pepper the current world with rocks
for i := -extent; i < extent; i++ {
for j := -extent; j < extent; j++ {
if rand.Intn(16) == 0 {
if err := a.SetTile(vector.Vector{X: i, Y: j}, TileSmallRock); err != nil {
return err
}
}
}
}
return nil
}
// SpawnWalls spawns the around the world
func (a *Atlas) SpawnWalls() error {
extent := a.ChunkSize * (a.Size / 2)
// Surround the atlas in walls
for i := -extent; i < extent; i++ {
if err := a.SetTile(vector.Vector{X: i, Y: extent - 1}, TileLargeRock); err != nil { // N
return err
} else if err := a.SetTile(vector.Vector{X: extent - 1, Y: i}, TileLargeRock); err != nil { // E
return err
} else if err := a.SetTile(vector.Vector{X: i, Y: -extent}, TileLargeRock); err != nil { // S
return err
} else if err := a.SetTile(vector.Vector{X: -extent, Y: i}, TileLargeRock); err != nil { // W
return err
}
}
return nil
}
// SetTile sets an individual tile's kind
func (a *Atlas) SetTile(v vector.Vector, tile byte) error {
chunk := a.toChunk(v)
if chunk >= len(a.Chunks) {
return fmt.Errorf("location outside of allocated atlas")
}
local := a.toChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize
if tileId >= len(a.Chunks[chunk].Tiles) {
return fmt.Errorf("location outside of allocated chunk")
}
a.Chunks[chunk].Tiles[tileId] = tile
return nil
}
// GetTile will return an individual tile
func (a *Atlas) GetTile(v vector.Vector) (byte, error) {
chunk := a.toChunk(v)
if chunk >= len(a.Chunks) {
return 0, fmt.Errorf("location outside of allocated atlas")
}
local := a.toChunkLocal(v)
tileId := local.X + local.Y*a.ChunkSize
if tileId >= len(a.Chunks[chunk].Tiles) {
return 0, fmt.Errorf("location outside of allocated chunk")
}
return a.Chunks[chunk].Tiles[tileId], nil
}
// toChunkLocal gets a chunk local coordinate for a tile
func (a *Atlas) toChunkLocal(v vector.Vector) vector.Vector {
return vector.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
}
// GetChunkID gets the chunk ID for a position in the world
func (a *Atlas) toChunk(v vector.Vector) int {
local := a.toChunkLocal(v)
// Get the chunk origin itself
origin := v.Added(local.Negated())
// Divided it by the number of chunks
origin = origin.Divided(a.ChunkSize)
// Shift it by our size (our origin is in the middle)
origin = origin.Added(vector.Vector{X: a.Size / 2, Y: a.Size / 2})
// Get the ID based on the final values
return (a.Size * origin.Y) + origin.X
}
// chunkOrigin gets the chunk origin for a given chunk index
func (a *Atlas) chunkOrigin(chunk int) vector.Vector {
v := vector.Vector{
X: maths.Pmod(chunk, a.Size) - (a.Size / 2),
Y: (chunk / a.Size) - (a.Size / 2),
}
return v.Multiplied(a.ChunkSize)
}
// GetWorldExtent gets the min and max valid coordinates of world
func (a *Atlas) GetWorldExtents() (min, max vector.Vector) {
min = vector.Vector{
X: -(a.Size / 2) * a.ChunkSize,
Y: -(a.Size / 2) * a.ChunkSize,
}
max = vector.Vector{
X: -min.X - 1,
Y: -min.Y - 1,
}
return
}
// Grow will return a grown copy of the current atlas
func (a *Atlas) Grow(size int) error {
if size%2 != 0 {
return fmt.Errorf("atlas size must always be even")
}
delta := size - a.Size
if delta < 0 {
return fmt.Errorf("cannot shrink an atlas")
} else if delta == 0 {
return nil
}
// Create a new atlas
newAtlas := NewAtlas(size, a.ChunkSize)
// Copy old chunks into new chunks
for index, chunk := range a.Chunks {
// Calculate the new chunk location and copy over the data
newAtlas.Chunks[newAtlas.toChunk(a.chunkOrigin(index))] = chunk
}
// Copy the new atlas data into this one
*a = newAtlas
// Return the new atlas
return nil
}

View file

@ -1,169 +0,0 @@
package atlas
import (
"testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestAtlas_NewAtlas(t *testing.T) {
a := NewAtlas(2, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
assert.Equal(t, 4, len(a.Chunks))
a = NewAtlas(4, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
assert.Equal(t, 16, len(a.Chunks))
}
func TestAtlas_toChunk(t *testing.T) {
a := NewAtlas(2, 1)
assert.NotNil(t, a)
// Tiles should look like: 2 | 3
// -----
// 0 | 1
tile := a.toChunk(vector.Vector{X: 0, Y: 0})
assert.Equal(t, 3, tile)
tile = a.toChunk(vector.Vector{X: 0, Y: -1})
assert.Equal(t, 1, tile)
tile = a.toChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 0, tile)
tile = a.toChunk(vector.Vector{X: -1, Y: 0})
assert.Equal(t, 2, tile)
a = NewAtlas(2, 2)
assert.NotNil(t, a)
// Tiles should look like:
// 2 | 3
// -----
// 0 | 1
tile = a.toChunk(vector.Vector{X: 1, Y: 1})
assert.Equal(t, 3, tile)
tile = a.toChunk(vector.Vector{X: 1, Y: -2})
assert.Equal(t, 1, tile)
tile = a.toChunk(vector.Vector{X: -2, Y: -2})
assert.Equal(t, 0, tile)
tile = a.toChunk(vector.Vector{X: -2, Y: 1})
assert.Equal(t, 2, tile)
a = NewAtlas(4, 2)
assert.NotNil(t, a)
// Tiles should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
tile = a.toChunk(vector.Vector{X: 1, Y: 3})
assert.Equal(t, 14, tile)
tile = a.toChunk(vector.Vector{X: 1, Y: -3})
assert.Equal(t, 2, tile)
tile = a.toChunk(vector.Vector{X: -1, Y: -1})
assert.Equal(t, 5, tile)
tile = a.toChunk(vector.Vector{X: -2, Y: 2})
assert.Equal(t, 13, tile)
}
func TestAtlas_GetSetTile(t *testing.T) {
a := NewAtlas(4, 10)
assert.NotNil(t, a)
// Set the origin tile to 1 and test it
assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err)
assert.Equal(t, byte(1), tile)
// Set another tile to 1 and test it
assert.NoError(t, a.SetTile(vector.Vector{X: 5, Y: -2}, 2))
tile, err = a.GetTile(vector.Vector{X: 5, Y: -2})
assert.NoError(t, err)
assert.Equal(t, byte(2), tile)
}
func TestAtlas_Grown(t *testing.T) {
// Start with a small example
a := NewAtlas(2, 2)
assert.NotNil(t, a)
assert.Equal(t, 4, len(a.Chunks))
// Set a few tiles to values
assert.NoError(t, a.SetTile(vector.Vector{X: 0, Y: 0}, 1))
assert.NoError(t, a.SetTile(vector.Vector{X: -1, Y: -1}, 2))
assert.NoError(t, a.SetTile(vector.Vector{X: 1, Y: -2}, 3))
// Grow once to just double it
err := a.Grow(4)
assert.NoError(t, err)
assert.Equal(t, 16, len(a.Chunks))
tile, err := a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err)
assert.Equal(t, byte(1), tile)
tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
assert.NoError(t, err)
assert.Equal(t, byte(2), tile)
tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
assert.NoError(t, err)
assert.Equal(t, byte(3), tile)
// Grow it again even bigger
err = a.Grow(10)
assert.NoError(t, err)
assert.Equal(t, 100, len(a.Chunks))
tile, err = a.GetTile(vector.Vector{X: 0, Y: 0})
assert.NoError(t, err)
assert.Equal(t, byte(1), tile)
tile, err = a.GetTile(vector.Vector{X: -1, Y: -1})
assert.NoError(t, err)
assert.Equal(t, byte(2), tile)
tile, err = a.GetTile(vector.Vector{X: 1, Y: -2})
assert.NoError(t, err)
assert.Equal(t, byte(3), tile)
}
func TestAtlas_SpawnWorld(t *testing.T) {
// Start with a small example
a := NewAtlas(2, 4)
assert.NotNil(t, a)
assert.Equal(t, 4, len(a.Chunks))
assert.NoError(t, a.SpawnWalls())
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: i, Y: -4})
assert.NoError(t, err)
assert.Equal(t, TileLargeRock, tile)
}
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: -4, Y: i})
assert.NoError(t, err)
assert.Equal(t, TileLargeRock, tile)
}
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: 3, Y: i})
assert.NoError(t, err)
assert.Equal(t, TileLargeRock, tile)
}
for i := -4; i < 4; i++ {
tile, err := a.GetTile(vector.Vector{X: i, Y: 3})
assert.NoError(t, err)
assert.Equal(t, TileLargeRock, tile)
}
}

View file

@ -1,23 +0,0 @@
package atlas
const (
TileEmpty = byte(' ')
TileRover = byte('R')
TileSmallRock = byte('o')
TileLargeRock = byte('O')
)
// BlockingTiles describes any tiles that block
var BlockingTiles = [...]byte{
TileLargeRock,
}
// Check if a tile is a blocking tile
func IsBlocking(tile byte) bool {
for _, t := range BlockingTiles {
if tile == t {
return true
}
}
return false
}

View file

@ -1,76 +0,0 @@
package bearing
import (
"fmt"
"strings"
"github.com/mdiluz/rove/pkg/vector"
)
// Bearing describes a compass direction
type Bearing int
const (
North Bearing = iota
NorthEast
East
SouthEast
South
SouthWest
West
NorthWest
)
// bearingString simply describes the strings associated with a direction
type bearingString struct {
Long string
Short string
}
// bearingStrings is the set of strings for each direction
var bearingStrings = []bearingString{
{"North", "N"},
{"NorthEast", "NE"},
{"East", "E"},
{"SouthEast", "SE"},
{"South", "S"},
{"SouthWest", "SW"},
{"West", "W"},
{"NorthWest", "NW"},
}
// String converts a Direction to a String
func (d Bearing) String() string {
return bearingStrings[d].Long
}
// ShortString converts a Direction to a short string version
func (d Bearing) ShortString() string {
return bearingStrings[d].Short
}
// FromString gets the Direction from a string
func FromString(s string) (Bearing, error) {
for i, d := range bearingStrings {
if strings.ToLower(d.Long) == strings.ToLower(s) || strings.ToLower(d.Short) == strings.ToLower(s) {
return Bearing(i), nil
}
}
return -1, fmt.Errorf("unknown bearing: %s", s)
}
var bearingVectors = []vector.Vector{
{X: 0, Y: 1}, // N
{X: 1, Y: 1}, // NE
{X: 1, Y: 0}, // E
{X: 1, Y: -1}, // SE
{X: 0, Y: -1}, // S
{X: -1, Y: 1}, // SW
{X: -1, Y: 0}, // W
{X: -1, Y: 1}, // NW
}
// Vector converts a Direction to a Vector
func (d Bearing) Vector() vector.Vector {
return bearingVectors[d]
}

View file

@ -1,32 +0,0 @@
package bearing
import (
"testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestDirection(t *testing.T) {
dir := North
assert.Equal(t, "North", dir.String())
assert.Equal(t, "N", dir.ShortString())
assert.Equal(t, vector.Vector{X: 0, Y: 1}, dir.Vector())
dir, err := FromString("N")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = FromString("n")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = FromString("north")
assert.NoError(t, err)
assert.Equal(t, North, dir)
dir, err = FromString("NorthWest")
assert.NoError(t, err)
assert.Equal(t, NorthWest, dir)
}

View file

@ -1,17 +0,0 @@
package game
const (
CommandMove = "move"
)
// Command represends a single command to execute
type Command struct {
Command string `json:"command"`
// Used in the move command
Bearing string `json:"bearing,omitempty"`
Duration int `json:"duration,omitempty"`
}
// CommandStream is a list of commands to execute in order
type CommandStream []Command

View file

@ -1,38 +0,0 @@
package game
import (
"testing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestCommand_Move(t *testing.T) {
world := NewWorld(2, 8)
a, err := world.SpawnRover()
assert.NoError(t, err)
pos := vector.Vector{
X: 1.0,
Y: 2.0,
}
attribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
var duration = 1
// Try the move command
moveCommand := Command{Command: CommandMove, Bearing: "N", Duration: duration}
assert.NoError(t, world.Enqueue(a, moveCommand), "Failed to execute move command")
// Tick the world
world.EnqueueAllIncoming()
world.ExecuteCommandQueues()
newatributes, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(vector.Vector{X: 0.0, Y: int(duration) * int(attribs.Speed)}) // We should have moved duration*speed north
assert.Equal(t, pos, newatributes.Pos, "Failed to correctly set position for rover")
}

View file

@ -1,30 +0,0 @@
package game
import (
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/vector"
)
// RoverAttributes contains attributes of a rover
type RoverAttributes struct {
// Speed represents the Speed that the rover will move per second
Speed int `json:"speed"`
// Range represents the distance the unit's radar can see
Range int `json:"range"`
// Name of this rover
Name string `json:"name"`
// Pos represents where this rover is in the world
Pos vector.Vector `json:"pos"`
}
// Rover describes a single rover in the world
type Rover struct {
// Id is a unique ID for this rover
Id uuid.UUID `json:"id"`
// Attributes represents the physical attributes of the rover
Attributes RoverAttributes `json:"attributes"`
}

View file

@ -1,434 +0,0 @@
package game
import (
"bufio"
"fmt"
"log"
"math"
"math/rand"
"os"
"sync"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/pkg/vector"
)
// World describes a self contained universe and everything in it
type World struct {
// Rovers is a id->data map of all the rovers in the game
Rovers map[uuid.UUID]Rover `json:"rovers"`
// Atlas represends the world map of chunks and tiles
Atlas atlas.Atlas `json:"atlas"`
// Mutex to lock around all world operations
worldMutex sync.RWMutex
// Commands is the set of currently executing command streams per rover
CommandQueue map[uuid.UUID]CommandStream `json:"commands"`
// Incoming represents the set of commands to add to the queue at the end of the current tick
Incoming map[uuid.UUID]CommandStream `json:"incoming"`
// Mutex to lock around command operations
cmdMutex sync.RWMutex
// Set of possible words to use for names
words []string
}
var wordsFile = os.Getenv("WORDS_FILE")
// NewWorld creates a new world object
func NewWorld(size, chunkSize int) *World {
// Try and load the words file
var lines []string
if file, err := os.Open(wordsFile); err != nil {
log.Printf("Couldn't read words file [%s], running without words: %s\n", wordsFile, err)
} else {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if scanner.Err() != nil {
log.Printf("Failure during word file scan: %s\n", scanner.Err())
}
}
return &World{
Rovers: make(map[uuid.UUID]Rover),
CommandQueue: make(map[uuid.UUID]CommandStream),
Incoming: make(map[uuid.UUID]CommandStream),
Atlas: atlas.NewAtlas(size, chunkSize),
words: lines,
}
}
// SpawnWorld spawns a border at the edge of the world atlas
func (w *World) SpawnWorld(fillWorld bool) error {
if fillWorld {
if err := w.Atlas.SpawnRocks(); err != nil {
return err
}
}
return w.Atlas.SpawnWalls()
}
// SpawnRover adds an rover to the game
func (w *World) SpawnRover() (uuid.UUID, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
// Initialise the rover
rover := Rover{
Id: uuid.New(),
Attributes: RoverAttributes{
Speed: 1.0,
Range: 5.0,
Name: "rover",
},
}
// Assign a random name if we have words
if len(w.words) > 0 {
rover.Attributes.Name = fmt.Sprintf("%s-%s", w.words[rand.Intn(len(w.words))], w.words[rand.Intn(len(w.words))])
}
// Spawn in a random place near the origin
rover.Attributes.Pos = vector.Vector{
X: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
Y: w.Atlas.ChunkSize/2 - rand.Intn(w.Atlas.ChunkSize),
}
// Seach until we error (run out of world)
for {
if tile, err := w.Atlas.GetTile(rover.Attributes.Pos); err != nil {
return uuid.Nil, err
} else {
if !atlas.IsBlocking(tile) {
break
} else {
// Try and spawn to the east of the blockage
rover.Attributes.Pos.Add(vector.Vector{X: 1, Y: 0})
}
}
}
log.Printf("Spawned rover at %+v\n", rover.Attributes.Pos)
// Append the rover to the list
w.Rovers[rover.Id] = rover
return rover.Id, nil
}
// Removes an rover from the game
func (w *World) DestroyRover(id uuid.UUID) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok {
// Clear the tile
if err := w.Atlas.SetTile(i.Attributes.Pos, atlas.TileEmpty); err != nil {
return fmt.Errorf("coudln't clear old rover tile: %s", err)
}
delete(w.Rovers, id)
} else {
return fmt.Errorf("no rover matching id")
}
return nil
}
// RoverAttributes returns the attributes of a requested rover
func (w *World) RoverAttributes(id uuid.UUID) (RoverAttributes, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if i, ok := w.Rovers[id]; ok {
return i.Attributes, nil
} else {
return RoverAttributes{}, fmt.Errorf("no rover matching id")
}
}
// SetRoverAttributes sets the attributes of a requested rover
func (w *World) SetRoverAttributes(id uuid.UUID, attributes RoverAttributes) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok {
i.Attributes = attributes
w.Rovers[id] = i
return nil
} else {
return fmt.Errorf("no rover matching id")
}
}
// WarpRover sets an rovers position
func (w *World) WarpRover(id uuid.UUID, pos vector.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok {
// Nothing to do if these positions match
if i.Attributes.Pos == pos {
return nil
}
// Check the tile is not blocked
if tile, err := w.Atlas.GetTile(pos); err != nil {
return fmt.Errorf("coudln't get state of destination rover tile: %s", err)
} else if atlas.IsBlocking(tile) {
return fmt.Errorf("can't warp rover to occupied tile, check before warping")
}
i.Attributes.Pos = pos
w.Rovers[id] = i
return nil
} else {
return fmt.Errorf("no rover matching id")
}
}
// SetPosition sets an rovers position
func (w *World) MoveRover(id uuid.UUID, b bearing.Bearing) (RoverAttributes, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
if i, ok := w.Rovers[id]; ok {
// Calculate the distance
distance := i.Attributes.Speed
// Calculate the full movement based on the bearing
move := b.Vector().Multiplied(distance)
// Try the new move position
newPos := i.Attributes.Pos.Added(move)
// Get the tile and verify it's empty
if tile, err := w.Atlas.GetTile(newPos); err != nil {
return i.Attributes, fmt.Errorf("couldn't get tile for new position: %s", err)
} else if !atlas.IsBlocking(tile) {
// Perform the move
i.Attributes.Pos = newPos
w.Rovers[id] = i
}
return i.Attributes, nil
} else {
return RoverAttributes{}, fmt.Errorf("no rover matching id")
}
}
// RadarFromRover can be used to query what a rover can currently see
func (w *World) RadarFromRover(id uuid.UUID) ([]byte, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
if r, ok := w.Rovers[id]; ok {
// The radar should span in range direction on each axis, plus the row/column the rover is currently on
radarSpan := (r.Attributes.Range * 2) + 1
roverPos := r.Attributes.Pos
// Get the radar min and max values
radarMin := vector.Vector{
X: roverPos.X - r.Attributes.Range,
Y: roverPos.Y - r.Attributes.Range,
}
radarMax := vector.Vector{
X: roverPos.X + r.Attributes.Range,
Y: roverPos.Y + r.Attributes.Range,
}
// Make sure we only query within the actual world
worldMin, worldMax := w.Atlas.GetWorldExtents()
scanMin := vector.Vector{
X: maths.Max(radarMin.X, worldMin.X),
Y: maths.Max(radarMin.Y, worldMin.Y),
}
scanMax := vector.Vector{
X: maths.Min(radarMax.X, worldMax.X),
Y: maths.Min(radarMax.Y, worldMax.Y),
}
// Gather up all tiles within the range
var radar = make([]byte, radarSpan*radarSpan)
for j := scanMin.Y; j <= scanMax.Y; j++ {
for i := scanMin.X; i <= scanMax.X; i++ {
q := vector.Vector{X: i, Y: j}
if tile, err := w.Atlas.GetTile(q); err != nil {
return nil, fmt.Errorf("failed to query tile: %s", err)
} else {
// Get the position relative to the bottom left of the radar
relative := q.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
radar[index] = tile
}
}
}
// Add all rovers to the radar
for _, r := range w.Rovers {
// If the rover is in range
dist := r.Attributes.Pos.Added(roverPos.Negated())
dist = dist.Abs()
if dist.X <= r.Attributes.Range && dist.Y <= r.Attributes.Range {
relative := r.Attributes.Pos.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
radar[index] = atlas.TileRover
}
}
// Add this rover
radar[len(radar)/2] = atlas.TileRover
return radar, nil
} else {
return nil, fmt.Errorf("no rover matching id")
}
}
// Enqueue will queue the commands given
func (w *World) Enqueue(rover uuid.UUID, commands ...Command) error {
// First validate the commands
for _, c := range commands {
switch c.Command {
case "move":
if _, err := bearing.FromString(c.Bearing); err != nil {
return fmt.Errorf("unknown bearing: %s", c.Bearing)
}
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
}
// Lock our commands edit
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Append the commands to the incoming set
if cmds, ok := w.Incoming[rover]; ok {
w.Incoming[rover] = append(cmds, commands...)
} else {
w.Incoming[rover] = commands
}
return nil
}
// EnqueueAllIncoming will enqueue the incoming commands
func (w *World) EnqueueAllIncoming() {
// Add any incoming commands from this tick and clear that queue
for id, incoming := range w.Incoming {
commands := w.CommandQueue[id]
commands = append(commands, incoming...)
w.CommandQueue[id] = commands
}
w.Incoming = make(map[uuid.UUID]CommandStream)
}
// Execute will execute any commands in the current command queue
func (w *World) ExecuteCommandQueues() {
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Iterate through all the current commands
for rover, cmds := range w.CommandQueue {
if len(cmds) != 0 {
// Extract the first command in the queue
c := cmds[0]
// Execute the command and clear up if requested
if done, err := w.ExecuteCommand(&c, rover); err != nil {
w.CommandQueue[rover] = cmds[1:]
log.Println(err)
} else if done {
w.CommandQueue[rover] = cmds[1:]
} else {
w.CommandQueue[rover][0] = c
}
// If there was an error
} else {
// Clean out the empty entry
delete(w.CommandQueue, rover)
}
}
// Add any incoming commands from this tick and clear that queue
w.EnqueueAllIncoming()
}
// ExecuteCommand will execute a single command
func (w *World) ExecuteCommand(c *Command, rover uuid.UUID) (finished bool, err error) {
log.Printf("Executing command: %+v\n", *c)
switch c.Command {
case "move":
if dir, err := bearing.FromString(c.Bearing); err != nil {
return true, fmt.Errorf("unknown bearing in command %+v, skipping: %s\n", c, err)
} else if _, err := w.MoveRover(rover, dir); err != nil {
return true, fmt.Errorf("error moving rover in command %+v, skipping: %s\n", c, err)
} else {
// If we've successfully moved, reduce the duration by 1
c.Duration -= 1
// If we've used up the full duration, remove it, otherwise update
if c.Duration == 0 {
finished = true
}
}
default:
return true, fmt.Errorf("unknown command: %s", c.Command)
}
return
}
// PrintTiles simply prints the input tiles directly for debug
func PrintTiles(tiles []byte) {
num := int(math.Sqrt(float64(len(tiles))))
for j := num - 1; j >= 0; j-- {
for i := 0; i < num; i++ {
fmt.Printf("%c", tiles[i+num*j])
}
fmt.Print("\n")
}
}
// RLock read locks the world
func (w *World) RLock() {
w.worldMutex.RLock()
w.cmdMutex.RLock()
}
// RUnlock read unlocks the world
func (w *World) RUnlock() {
w.worldMutex.RUnlock()
w.cmdMutex.RUnlock()
}
// Lock locks the world
func (w *World) Lock() {
w.worldMutex.Lock()
w.cmdMutex.Lock()
}
// Unlock unlocks the world
func (w *World) Unlock() {
w.worldMutex.Unlock()
w.cmdMutex.Unlock()
}

View file

@ -1,146 +0,0 @@
package game
import (
"testing"
"github.com/mdiluz/rove/pkg/atlas"
"github.com/mdiluz/rove/pkg/bearing"
"github.com/mdiluz/rove/pkg/vector"
"github.com/stretchr/testify/assert"
)
func TestNewWorld(t *testing.T) {
// Very basic for now, nothing to verify
world := NewWorld(4, 4)
if world == nil {
t.Error("Failed to create world")
}
}
func TestWorld_CreateRover(t *testing.T) {
world := NewWorld(2, 8)
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
// Basic duplicate check
if a == b {
t.Errorf("Created identical rovers")
} else if len(world.Rovers) != 2 {
t.Errorf("Incorrect number of rovers created")
}
}
func TestWorld_RoverAttributes(t *testing.T) {
world := NewWorld(2, 4)
a, err := world.SpawnRover()
assert.NoError(t, err)
attribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
assert.NotZero(t, attribs.Range, "Rover should not be spawned blind")
assert.NotZero(t, attribs.Speed, "Rover should not be spawned unable to move")
}
func TestWorld_DestroyRover(t *testing.T) {
world := NewWorld(4, 1)
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
err = world.DestroyRover(a)
assert.NoError(t, err, "Error returned from rover destroy")
// Basic duplicate check
if len(world.Rovers) != 1 {
t.Error("Too many rovers left in world")
} else if _, ok := world.Rovers[b]; !ok {
t.Error("Remaining rover is incorrect")
}
}
func TestWorld_GetSetMovePosition(t *testing.T) {
world := NewWorld(4, 4)
a, err := world.SpawnRover()
assert.NoError(t, err)
attribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
pos := vector.Vector{
X: 0.0,
Y: 0.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
newAttribs, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly set position for rover")
b := bearing.North
duration := 1
newAttribs, err = world.MoveRover(a, b)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(vector.Vector{X: 0, Y: attribs.Speed * duration}) // We should have move one unit of the speed north
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly move position for rover")
// Place a tile in front of the rover
assert.NoError(t, world.Atlas.SetTile(vector.Vector{X: 0, Y: 2}, atlas.TileLargeRock))
newAttribs, err = world.MoveRover(a, b)
assert.Equal(t, pos, newAttribs.Pos, "Failed to correctly not move position for rover into wall")
}
func TestWorld_RadarFromRover(t *testing.T) {
// Create world that should have visible walls on the radar
world := NewWorld(4, 2)
a, err := world.SpawnRover()
assert.NoError(t, err)
b, err := world.SpawnRover()
assert.NoError(t, err)
// Set the rover range to a predictable value
attrib, err := world.RoverAttributes(a)
assert.NoError(t, err, "Failed to get rover attribs")
attrib.Range = 4 // Set the range to 4 so we can predict the radar fully
err = world.SetRoverAttributes(a, attrib)
assert.NoError(t, err, "Failed to set rover attribs")
// Warp the rovers into position
bpos := vector.Vector{X: -3, Y: -3}
assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover")
assert.NoError(t, world.WarpRover(a, vector.Vector{X: 0, Y: 0}), "Failed to warp rover")
// Spawn the world wall
err = world.Atlas.SpawnWalls()
assert.NoError(t, err)
radar, err := world.RadarFromRover(a)
assert.NoError(t, err, "Failed to get radar from rover")
fullRange := 4 + 4 + 1
assert.Equal(t, fullRange*fullRange, len(radar), "Radar returned wrong length")
// It should look like:
// ---------
// OOOOOOOO-
// O------O-
// O------O-
// O---R--O-
// O------O-
// O------O-
// OR-----O-
// OOOOOOOO-
PrintTiles(radar)
// Test all expected values
assert.Equal(t, atlas.TileRover, radar[1+fullRange])
assert.Equal(t, atlas.TileRover, radar[4+4*fullRange])
for i := 0; i < 8; i++ {
assert.Equal(t, atlas.TileLargeRock, radar[i])
assert.Equal(t, atlas.TileLargeRock, radar[i+(7*9)])
assert.Equal(t, atlas.TileLargeRock, radar[i*9])
assert.Equal(t, atlas.TileLargeRock, radar[(i*9)+7])
}
}

View file

@ -8,7 +8,7 @@ func Abs(x int) int {
return x
}
// pmod is a mositive modulo
// Pmod is a mositive modulo
// golang's % is a "remainder" function si misbehaves for negative modulus inputs
func Pmod(x, d int) int {
if x == 0 || d == 0 {
@ -39,3 +39,19 @@ func Min(x, y int) int {
}
return x
}
// RoundUp rounds a value up to the nearest multiple
func RoundUp(toRound int, multiple int) int {
remainder := Pmod(toRound, multiple)
if remainder == 0 {
return toRound
}
return (multiple - remainder) + toRound
}
// RoundDown rounds a value down to the nearest multiple
func RoundDown(toRound int, multiple int) int {
remainder := Pmod(toRound, multiple)
return toRound - remainder
}

View file

@ -29,5 +29,19 @@ func TestMin(t *testing.T) {
assert.Equal(t, 100, Min(100, 500))
assert.Equal(t, -4, Min(-4, 1))
assert.Equal(t, -4, Min(-4, -2))
}
func TestRoundUp(t *testing.T) {
assert.Equal(t, 10, RoundUp(10, 5))
assert.Equal(t, 12, RoundUp(10, 4))
assert.Equal(t, -8, RoundUp(-8, 4))
assert.Equal(t, -4, RoundUp(-7, 4))
}
func TestRoundDown(t *testing.T) {
assert.Equal(t, 10, RoundDown(10, 5))
assert.Equal(t, 8, RoundDown(10, 4))
assert.Equal(t, -8, RoundDown(-8, 4))
assert.Equal(t, -8, RoundDown(-7, 4))
}

119
pkg/maths/vector.go Normal file
View file

@ -0,0 +1,119 @@
package maths
import (
"math"
"github.com/mdiluz/rove/proto/roveapi"
)
// Vector desribes a 3D vector
type Vector struct {
X int
Y int
}
// Add adds one vector to another
func (v *Vector) Add(v2 Vector) {
v.X += v2.X
v.Y += v2.Y
}
// Added calculates a new vector
func (v Vector) Added(v2 Vector) Vector {
v.Add(v2)
return v
}
// Negated returns a negated vector
func (v Vector) Negated() Vector {
return Vector{-v.X, -v.Y}
}
// Length returns the length of the vector
func (v Vector) Length() float64 {
return math.Sqrt(float64(v.X*v.X + v.Y*v.Y))
}
// Distance returns the distance between two vectors
func (v Vector) Distance(v2 Vector) float64 {
// Negate the two vectors and calciate the length
return v.Added(v2.Negated()).Length()
}
// Multiplied returns the vector multiplied by an int
func (v Vector) Multiplied(val int) Vector {
return Vector{v.X * val, v.Y * val}
}
// Divided returns the vector divided by an int
func (v Vector) Divided(val int) Vector {
return Vector{v.X / val, v.Y / val}
}
// DividedFloor returns the vector divided but floors the value regardless
func (v Vector) DividedFloor(val int) Vector {
x := float64(v.X) / float64(val)
if x < 0 {
x = math.Floor(x)
} else {
x = math.Floor(x)
}
y := float64(v.Y) / float64(val)
if y < 0 {
y = math.Floor(y)
} else {
y = math.Floor(y)
}
return Vector{X: int(x), Y: int(y)}
}
// Abs returns an absolute version of the vector
func (v Vector) Abs() Vector {
return Vector{Abs(v.X), Abs(v.Y)}
}
// Min2 returns the minimum values in both vectors
func Min2(v1 Vector, v2 Vector) Vector {
return Vector{Min(v1.X, v2.X), Min(v1.Y, v2.Y)}
}
// Max2 returns the max values in both vectors
func Max2(v1 Vector, v2 Vector) Vector {
return Vector{Max(v1.X, v2.X), Max(v1.Y, v2.Y)}
}
// BearingToVector converts a bearing to a vector
func BearingToVector(b roveapi.Bearing) Vector {
switch b {
case roveapi.Bearing_North:
return Vector{Y: 1}
case roveapi.Bearing_NorthEast:
return Vector{X: 1, Y: 1}
case roveapi.Bearing_East:
return Vector{X: 1}
case roveapi.Bearing_SouthEast:
return Vector{X: 1, Y: -1}
case roveapi.Bearing_South:
return Vector{Y: -1}
case roveapi.Bearing_SouthWest:
return Vector{X: -1, Y: -1}
case roveapi.Bearing_West:
return Vector{X: -1}
case roveapi.Bearing_NorthWest:
return Vector{X: -1, Y: 1}
}
return Vector{}
}
// Dot returns the dot product of two vectors
func Dot(a Vector, b Vector) int {
return a.X*b.X + a.Y*b.Y
}
// AngleCos returns the cosine of the angle between two vectors
func AngleCos(a Vector, b Vector) float64 {
return float64(Dot(a, b)) / a.Length() * b.Length()
}

View file

@ -1,4 +1,4 @@
package vector
package maths
import (
"math"

View file

@ -31,12 +31,13 @@ func jsonPath(name string) string {
// Save will serialise the interface into a json file
func Save(name string, data interface{}) error {
p := jsonPath(name)
if b, err := json.MarshalIndent(data, "", " "); err != nil {
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
if err := ioutil.WriteFile(p, b, os.ModePerm); err != nil {
return err
} else {
if err := ioutil.WriteFile(p, b, os.ModePerm); err != nil {
return err
}
}
log.Printf("Saved %s\n", p)

18
pkg/rove/atlas.go Normal file
View file

@ -0,0 +1,18 @@
package rove
import (
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
// Atlas represents a 2D world atlas of tiles and objects
type Atlas interface {
// SetTile sets a location on the Atlas to a type of tile
SetTile(v maths.Vector, tile roveapi.Tile)
// SetObject will set a location on the Atlas to contain an object
SetObject(v maths.Vector, obj Object)
// QueryPosition queries a position on the atlas
QueryPosition(v maths.Vector) (roveapi.Tile, Object)
}

257
pkg/rove/atlas_test.go Normal file
View file

@ -0,0 +1,257 @@
package rove
import (
"testing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/stretchr/testify/assert"
)
func TestAtlas_NewAtlas(t *testing.T) {
a := NewChunkAtlas(1).(*chunkBasedAtlas)
assert.NotNil(t, a)
assert.Equal(t, 1, a.ChunkSize)
assert.Equal(t, 1, len(a.Chunks)) // Should start empty
}
func TestAtlas_toChunk(t *testing.T) {
a := NewChunkAtlas(1).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(maths.Vector{X: -1, Y: -1})
a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
chunkID := a.worldSpaceToChunkIndex(maths.Vector{X: 0, Y: 0})
assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 0, Y: -1})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -1, Y: -1})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -1, Y: 0})
assert.Equal(t, 2, chunkID)
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(maths.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 3, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: -2})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -2, Y: -2})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -2, Y: 1})
assert.Equal(t, 2, chunkID)
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 3})
assert.Equal(t, 14, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: -3})
assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -1, Y: -1})
assert.Equal(t, 5, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: -2, Y: 2})
assert.Equal(t, 13, chunkID)
a = NewChunkAtlas(3).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 0, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 3, Y: 1})
assert.Equal(t, 1, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 1, Y: 4})
assert.Equal(t, 2, chunkID)
chunkID = a.worldSpaceToChunkIndex(maths.Vector{X: 5, Y: 5})
assert.Equal(t, 3, chunkID)
}
func TestAtlas_toWorld(t *testing.T) {
a := NewChunkAtlas(1).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn some chunks
a.QueryPosition(maths.Vector{X: -1, Y: -1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, maths.Vector{X: -1, Y: -1}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, maths.Vector{X: 0, Y: -1}, a.chunkOriginInWorldSpace(1))
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn the chunks
a.QueryPosition(maths.Vector{X: -2, Y: -2})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: 1, Y: 1})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// 2 | 3
// -----
// 0 | 1
assert.Equal(t, maths.Vector{X: -2, Y: -2}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, maths.Vector{X: -2, Y: 0}, a.chunkOriginInWorldSpace(2))
a = NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
a.QueryPosition(maths.Vector{X: -3, Y: -3})
assert.Equal(t, 4*4, len(a.Chunks))
// Chunks should look like:
// 12| 13|| 14| 15
// ----------------
// 8 | 9 || 10| 11
// ================
// 4 | 5 || 6 | 7
// ----------------
// 0 | 1 || 2 | 3
assert.Equal(t, maths.Vector{X: -4, Y: -4}, a.chunkOriginInWorldSpace(0))
assert.Equal(t, maths.Vector{X: 2, Y: -2}, a.chunkOriginInWorldSpace(7))
a = NewChunkAtlas(3).(*chunkBasedAtlas)
assert.NotNil(t, a)
// Get a tile to spawn a 4x4 grid of chunks
a.QueryPosition(maths.Vector{X: 3, Y: 3})
assert.Equal(t, 2*2, len(a.Chunks))
// Chunks should look like:
// || 2| 3
// -------
// || 0| 1
// =======
assert.Equal(t, maths.Vector{X: 0, Y: 0}, a.chunkOriginInWorldSpace(0))
}
func TestAtlas_GetSetTile(t *testing.T) {
a := NewChunkAtlas(10)
assert.NotNil(t, a)
// Set the origin tile and test it
a.SetTile(maths.Vector{X: 0, Y: 0}, roveapi.Tile_Gravel)
tile, _ := a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, roveapi.Tile_Gravel, tile)
// Set another tile and test it
a.SetTile(maths.Vector{X: 5, Y: -2}, roveapi.Tile_Rock)
tile, _ = a.QueryPosition(maths.Vector{X: 5, Y: -2})
assert.Equal(t, roveapi.Tile_Rock, tile)
}
func TestAtlas_GetSetObject(t *testing.T) {
a := NewChunkAtlas(10)
assert.NotNil(t, a)
// Set the origin tile to 1 and test it
a.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_RockLarge})
_, obj := a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, Object{Type: roveapi.Object_RockLarge}, obj)
// Set another tile to 1 and test it
a.SetObject(maths.Vector{X: 5, Y: -2}, Object{Type: roveapi.Object_RockSmall})
_, obj = a.QueryPosition(maths.Vector{X: 5, Y: -2})
assert.Equal(t, Object{Type: roveapi.Object_RockSmall}, obj)
}
func TestAtlas_Grown(t *testing.T) {
// Start with a small example
a := NewChunkAtlas(2).(*chunkBasedAtlas)
assert.NotNil(t, a)
assert.Equal(t, 1, len(a.Chunks))
// Set a few tiles to values
a.SetTile(maths.Vector{X: 0, Y: 0}, roveapi.Tile_Gravel)
a.SetTile(maths.Vector{X: -1, Y: -1}, roveapi.Tile_Rock)
a.SetTile(maths.Vector{X: 1, Y: -2}, roveapi.Tile_Sand)
// Check tile values
tile, _ := a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, roveapi.Tile_Gravel, tile)
tile, _ = a.QueryPosition(maths.Vector{X: -1, Y: -1})
assert.Equal(t, roveapi.Tile_Rock, tile)
tile, _ = a.QueryPosition(maths.Vector{X: 1, Y: -2})
assert.Equal(t, roveapi.Tile_Sand, tile)
tile, _ = a.QueryPosition(maths.Vector{X: 0, Y: 0})
assert.Equal(t, roveapi.Tile_Gravel, tile)
tile, _ = a.QueryPosition(maths.Vector{X: -1, Y: -1})
assert.Equal(t, roveapi.Tile_Rock, tile)
tile, _ = a.QueryPosition(maths.Vector{X: 1, Y: -2})
assert.Equal(t, roveapi.Tile_Sand, tile)
}
func TestAtlas_GetSetCorrect(t *testing.T) {
// Big stress test to ensure we do actually properly expand for all reasonable values
for i := 1; i <= 4; i++ {
for x := -i * 2; x < i*2; x++ {
for y := -i * 2; y < i*2; y++ {
a := NewChunkAtlas(i).(*chunkBasedAtlas)
assert.NotNil(t, a)
assert.Equal(t, 1, len(a.Chunks))
pos := maths.Vector{X: x, Y: y}
a.SetTile(pos, roveapi.Tile_Rock)
a.SetObject(pos, Object{Type: roveapi.Object_RockLarge})
tile, obj := a.QueryPosition(pos)
assert.Equal(t, roveapi.Tile_Rock, roveapi.Tile(tile))
assert.Equal(t, Object{Type: roveapi.Object_RockLarge}, obj)
}
}
}
}
func TestAtlas_WorldGen(t *testing.T) {
a := NewChunkAtlas(8)
// Spawn a large world
_, _ = a.QueryPosition(maths.Vector{X: 20, Y: 20})
}

231
pkg/rove/chunkAtlas.go Normal file
View file

@ -0,0 +1,231 @@
package rove
import (
"log"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
// chunk represents a fixed square grid of tiles
type chunk struct {
// Tiles represents the tiles within the chunk
Tiles []byte
// Objects represents the objects within the chunk
// only one possible object per tile for now
Objects map[int]Object
}
// chunkBasedAtlas represents a grid of Chunks
type chunkBasedAtlas struct {
// Chunks represents all chunks in the world
// This is intentionally not a 2D array so it can be expanded in all directions
Chunks []chunk
// LowerBound is the origin of the bottom left corner of the current chunks in world space (current chunks cover >= this value)
LowerBound maths.Vector
// UpperBound is the top left corner of the current chunks (curent chunks cover < this value)
UpperBound maths.Vector
// ChunkSize is the x/y dimensions of each square chunk
ChunkSize int
// worldGen is the internal world generator
worldGen WorldGen
}
const (
noiseSeed = 1024
)
// NewChunkAtlas creates a new empty atlas
func NewChunkAtlas(chunkSize int) Atlas {
// Start up with one chunk
a := chunkBasedAtlas{
ChunkSize: chunkSize,
Chunks: make([]chunk, 1),
LowerBound: maths.Vector{X: 0, Y: 0},
UpperBound: maths.Vector{X: chunkSize, Y: chunkSize},
worldGen: NewNoiseWorldGen(noiseSeed),
}
// Initialise the first chunk
a.populate(0)
return &a
}
// SetTile sets an individual tile's kind
func (a *chunkBasedAtlas) SetTile(v maths.Vector, tile roveapi.Tile) {
c := a.worldSpaceToChunkWithGrow(v)
local := a.worldSpaceToChunkLocal(v)
a.setTile(c, local, byte(tile))
}
// SetObject sets the object on a tile
func (a *chunkBasedAtlas) SetObject(v maths.Vector, obj Object) {
c := a.worldSpaceToChunkWithGrow(v)
local := a.worldSpaceToChunkLocal(v)
a.setObject(c, local, obj)
}
// QueryPosition will return information for a specific position
func (a *chunkBasedAtlas) QueryPosition(v maths.Vector) (roveapi.Tile, Object) {
c := a.worldSpaceToChunkWithGrow(v)
local := a.worldSpaceToChunkLocal(v)
a.populate(c)
chunk := a.Chunks[c]
i := a.chunkTileIndex(local)
return roveapi.Tile(chunk.Tiles[i]), chunk.Objects[i]
}
// chunkTileID returns the tile index within a chunk
func (a *chunkBasedAtlas) chunkTileIndex(local maths.Vector) int {
return local.X + local.Y*a.ChunkSize
}
// populate will fill a chunk with data
func (a *chunkBasedAtlas) populate(chunk int) {
c := a.Chunks[chunk]
if c.Tiles != nil {
return
}
c.Tiles = make([]byte, a.ChunkSize*a.ChunkSize)
c.Objects = make(map[int]Object)
origin := a.chunkOriginInWorldSpace(chunk)
for i := 0; i < a.ChunkSize; i++ {
for j := 0; j < a.ChunkSize; j++ {
loc := maths.Vector{X: origin.X + i, Y: origin.Y + j}
// Set the tile
c.Tiles[j*a.ChunkSize+i] = byte(a.worldGen.GetTile(loc))
// Set the object
obj := a.worldGen.GetObject(loc)
if obj.Type != roveapi.Object_ObjectUnknown {
c.Objects[j*a.ChunkSize+i] = obj
}
}
}
a.Chunks[chunk] = c
}
// setTile sets a tile in a specific chunk
func (a *chunkBasedAtlas) setTile(chunk int, local maths.Vector, tile byte) {
a.populate(chunk)
c := a.Chunks[chunk]
c.Tiles[a.chunkTileIndex(local)] = tile
a.Chunks[chunk] = c
}
// setObject sets an object in a specific chunk
func (a *chunkBasedAtlas) setObject(chunk int, local maths.Vector, object Object) {
a.populate(chunk)
c := a.Chunks[chunk]
i := a.chunkTileIndex(local)
if object.Type != roveapi.Object_ObjectUnknown {
c.Objects[i] = object
} else {
delete(c.Objects, i)
}
a.Chunks[chunk] = c
}
// worldSpaceToChunkLocal gets a chunk local coordinate for a tile
func (a *chunkBasedAtlas) worldSpaceToChunkLocal(v maths.Vector) maths.Vector {
return maths.Vector{X: maths.Pmod(v.X, a.ChunkSize), Y: maths.Pmod(v.Y, a.ChunkSize)}
}
// worldSpaceToChunkID gets the current chunk ID for a position in the world
func (a *chunkBasedAtlas) worldSpaceToChunkIndex(v maths.Vector) int {
// Shift the vector by our current min
v = v.Added(a.LowerBound.Negated())
// Divide by the current size and floor, to get chunk-scaled vector from the lower bound
v = v.DividedFloor(a.ChunkSize)
// Calculate the width
width := a.UpperBound.X - a.LowerBound.X
widthInChunks := width / a.ChunkSize
// Along the corridor and up the stairs
return (v.Y * widthInChunks) + v.X
}
// chunkOriginInWorldSpace returns the origin of the chunk in world space
func (a *chunkBasedAtlas) chunkOriginInWorldSpace(chunk int) maths.Vector {
// Calculate the width
width := a.UpperBound.X - a.LowerBound.X
widthInChunks := width / a.ChunkSize
// Reverse the along the corridor and up the stairs
v := maths.Vector{
X: chunk % widthInChunks,
Y: chunk / widthInChunks,
}
// Multiply up to world scale
v = v.Multiplied(a.ChunkSize)
// Shift by the lower bound
return v.Added(a.LowerBound)
}
// getNewBounds gets new lower and upper bounds for the world space given a vector
func (a *chunkBasedAtlas) getNewBounds(v maths.Vector) (lower maths.Vector, upper maths.Vector) {
lower = maths.Min2(v, a.LowerBound)
upper = maths.Max2(v.Added(maths.Vector{X: 1, Y: 1}), a.UpperBound)
lower = maths.Vector{
X: maths.RoundDown(lower.X, a.ChunkSize),
Y: maths.RoundDown(lower.Y, a.ChunkSize),
}
upper = maths.Vector{
X: maths.RoundUp(upper.X, a.ChunkSize),
Y: maths.RoundUp(upper.Y, a.ChunkSize),
}
return
}
// worldSpaceToTrunkWithGrow will expand the current atlas for a given world space position if needed
func (a *chunkBasedAtlas) worldSpaceToChunkWithGrow(v maths.Vector) int {
// If we're within bounds, just return the current chunk
if v.X >= a.LowerBound.X && v.Y >= a.LowerBound.Y && v.X < a.UpperBound.X && v.Y < a.UpperBound.Y {
return a.worldSpaceToChunkIndex(v)
}
// Calculate the new bounds
lower, upper := a.getNewBounds(v)
size := upper.Added(lower.Negated())
size = size.Divided(a.ChunkSize)
// Create the new empty atlas
newAtlas := chunkBasedAtlas{
ChunkSize: a.ChunkSize,
LowerBound: lower,
UpperBound: upper,
Chunks: make([]chunk, size.X*size.Y),
worldGen: a.worldGen,
}
// Log that we're resizing
log.Printf("Re-allocating world, old: %+v,%+v new: %+v,%+v\n", a.LowerBound, a.UpperBound, newAtlas.LowerBound, newAtlas.UpperBound)
// Copy all old chunks into the new atlas
for chunk, chunkData := range a.Chunks {
// Calculate the chunk ID in the new atlas
origin := a.chunkOriginInWorldSpace(chunk)
newChunk := newAtlas.worldSpaceToChunkIndex(origin)
// Copy over the old chunk to the new atlas
newAtlas.Chunks[newChunk] = chunkData
}
// Overwrite the old atlas with this one
*a = newAtlas
return a.worldSpaceToChunkIndex(v)
}

305
pkg/rove/command_test.go Normal file
View file

@ -0,0 +1,305 @@
package rove
import (
"encoding/json"
"testing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/stretchr/testify/assert"
)
func TestCommand_Invalid(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_none})
assert.Error(t, err)
}
func TestCommand_Toggle(t *testing.T) {
w := NewWorld(8)
a, err := w.SpawnRover("")
assert.NoError(t, err)
r, err := w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_toggle})
assert.NoError(t, err)
w.Tick()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_CatchingWind, r.SailPosition)
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_toggle})
assert.NoError(t, err)
w.Tick()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
}
func TestCommand_Turn(t *testing.T) {
w := NewWorld(8)
a, err := w.SpawnRover("")
assert.NoError(t, err)
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_turn, Bearing: roveapi.Bearing_NorthWest})
assert.NoError(t, err)
w.Tick()
r, err := w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_NorthWest, r.Bearing)
}
func TestCommand_Stash(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
info, err := w.GetRover(name)
assert.NoError(t, err)
assert.Empty(t, info.Inventory)
// Drop a pickup below us
w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RockSmall})
// Try and stash it
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_stash})
assert.NoError(t, err)
w.Tick()
// Check we now have it in the inventory
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, 1, len(info.Inventory))
assert.Equal(t, Object{Type: roveapi.Object_RockSmall}, info.Inventory[0])
// Check it's no longer on the atlas
_, obj := w.Atlas.QueryPosition(info.Pos)
assert.Equal(t, Object{Type: roveapi.Object_ObjectUnknown}, obj)
}
func TestCommand_Repair(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
info, err := w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, info.MaximumIntegrity, info.Integrity)
// Put a blocking rock to the north
w.Atlas.SetObject(info.Pos.Added(maths.Vector{X: 0, Y: 1}), Object{Type: roveapi.Object_RockLarge})
// Try and move and make sure we're blocked
newpos, err := w.TryMoveRover(name, roveapi.Bearing_North)
assert.NoError(t, err)
assert.Equal(t, info.Pos, newpos)
// Check we're damaged
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, info.MaximumIntegrity-1, info.Integrity)
// Stash a repair object
w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RoverParts})
obj, err := w.RoverStash(name)
assert.NoError(t, err)
assert.Equal(t, roveapi.Object_RoverParts, obj)
// Enqueue the repair and tick
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_repair})
assert.NoError(t, err)
w.Tick()
// Check we're repaired
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, info.MaximumIntegrity, info.Integrity)
assert.Equal(t, 0, len(info.Inventory))
}
func TestCommand_Broadcast(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
// Enqueue the broadcast and tick
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_broadcast, Data: []byte("ABC")})
assert.NoError(t, err)
w.Tick()
info, err := w.GetRover(name)
assert.NoError(t, err)
assert.Contains(t, info.Logs[len(info.Logs)-1].Text, "ABC")
}
func TestCommand_Salvage(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
info, err := w.GetRover(name)
assert.NoError(t, err)
w.Atlas.SetObject(info.Pos, Object{Type: roveapi.Object_RoverDormant})
// Enqueue the broadcast and tick
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_salvage})
assert.NoError(t, err)
w.Tick()
// Check we now have some rover parts
info, err = w.GetRover(name)
assert.NoError(t, err)
assert.NotEmpty(t, info.Inventory)
for _, i := range info.Inventory {
assert.Equal(t, roveapi.Object_RoverParts, i.Type)
}
// Check the dormant rover is gone
_, obj := w.Atlas.QueryPosition(info.Pos)
assert.Equal(t, roveapi.Object_ObjectUnknown, obj.Type)
}
func TestCommand_Transfer(t *testing.T) {
w := NewWorld(8)
acc, err := w.Accountant.RegisterAccount("tmp")
assert.NoError(t, err)
nameA, err := w.SpawnRover(acc.Name)
assert.NoError(t, err)
infoA, err := w.GetRover(nameA)
assert.NoError(t, err)
// Drop a dormant rover on the current position
infoB := DefaultRover()
infoB.Name = "abc"
infoB.Pos = infoA.Pos
data, err := json.Marshal(infoB)
assert.NoError(t, err)
w.Atlas.SetObject(infoA.Pos, Object{Type: roveapi.Object_RoverDormant, Data: data})
// Enqueue a transfer as well as a dud command
err = w.Enqueue(nameA,
&roveapi.Command{Command: roveapi.CommandType_transfer},
&roveapi.Command{Command: roveapi.CommandType_broadcast, Data: []byte("xyz")})
assert.NoError(t, err)
w.Tick()
// Ensure both command queues are empty
assert.Empty(t, w.CommandQueue[nameA])
assert.Empty(t, w.CommandQueue[infoB.Name])
// Verify the account now controls the new rover
accountRover, err := w.Accountant.GetValue(acc.Name, "rover")
assert.NoError(t, err)
assert.Equal(t, infoB.Name, accountRover)
// Verify the position now has a dormant rover
_, obj := w.Atlas.QueryPosition(infoA.Pos)
assert.Equal(t, roveapi.Object_RoverDormant, obj.Type)
// Verify the stored data matches
var stored Rover
err = json.Unmarshal(obj.Data, &stored)
assert.NoError(t, err)
assert.Equal(t, infoA.Name, stored.Name)
// Verify the new rover data matches what we put in
infoB2, err := w.GetRover(infoB.Name)
assert.NoError(t, err)
assert.Equal(t, infoB.Name, infoB2.Name)
}
func TestCommand_Wait(t *testing.T) {
w := NewWorld(8)
a, err := w.SpawnRover("")
assert.NoError(t, err)
r, err := w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
err = w.Enqueue(a, &roveapi.Command{Command: roveapi.CommandType_wait, Repeat: 4}, &roveapi.Command{Command: roveapi.CommandType_toggle})
assert.NoError(t, err)
// Tick 5 times during the wait (1 normal execute + 4)
for i := 0; i < 5; i++ {
w.Tick()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_SolarCharging, r.SailPosition)
}
// One last tick to do the toggle
w.Tick()
r, err = w.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_CatchingWind, r.SailPosition)
}
func TestCommand_Upgrade(t *testing.T) {
w := NewWorld(8)
name, err := w.SpawnRover("")
assert.NoError(t, err)
rover, ok := w.Rovers[name]
assert.True(t, ok)
// Try an invalid upgrade
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade})
assert.Error(t, err)
// Try a valid command but without the parts
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade, Upgrade: roveapi.RoverUpgrade_Capacity})
assert.NoError(t, err)
// Ensure nothing changed and we logged the attempt
pre := rover.Capacity
w.Tick()
assert.Equal(t, pre, rover.Capacity)
assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "tried")
// One non-part item
rover.Inventory = []Object{
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RockSmall,
},
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RoverParts,
},
{
Type: roveapi.Object_RoverParts,
},
}
// Try a valid command again
err = w.Enqueue(name, &roveapi.Command{Command: roveapi.CommandType_upgrade, Upgrade: roveapi.RoverUpgrade_Capacity})
assert.NoError(t, err)
// Check that the capacity increases on the tick and all the parts are used
pre = rover.Capacity
w.Tick()
assert.Equal(t, pre+1, rover.Capacity)
assert.Equal(t, 1, len(rover.Inventory))
assert.Equal(t, roveapi.Object_RockSmall, rover.Inventory[0].Type)
}

44
pkg/rove/objects.go Normal file
View file

@ -0,0 +1,44 @@
package rove
import (
"github.com/mdiluz/rove/proto/roveapi"
)
// Object represents an object in the world
type Object struct {
// The type of the object
Type roveapi.Object
// Data is an internal type used for certain types of object
Data []byte
}
// IsBlocking checks if an object is a blocking object
func (o *Object) IsBlocking() bool {
var blocking = [...]roveapi.Object{
roveapi.Object_RoverLive,
roveapi.Object_RockLarge,
}
for _, t := range blocking {
if o.Type == t {
return true
}
}
return false
}
// IsStashable checks if an object is stashable
func (o *Object) IsStashable() bool {
var stashable = [...]roveapi.Object{
roveapi.Object_RockSmall,
roveapi.Object_RoverParts,
}
for _, t := range stashable {
if o.Type == t {
return true
}
}
return false
}

File diff suppressed because it is too large Load diff

View file

@ -1,459 +0,0 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: rove/rove.proto
/*
Package rove is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package rove
import (
"context"
"io"
"net/http"
"github.com/golang/protobuf/descriptor"
"github.com/golang/protobuf/proto"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/status"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = descriptor.ForMessage
func request_Rove_Status_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StatusRequest
var metadata runtime.ServerMetadata
msg, err := client.Status(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Status_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq StatusRequest
var metadata runtime.ServerMetadata
msg, err := server.Status(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Register_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RegisterRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Register(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Register_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RegisterRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Register(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Commands_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CommandsRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Commands(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Commands_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq CommandsRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Commands(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Radar_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RadarRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Radar(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Radar_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RadarRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Radar(ctx, &protoReq)
return msg, metadata, err
}
func request_Rove_Rover_0(ctx context.Context, marshaler runtime.Marshaler, client RoveClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RoverRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.Rover(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_Rove_Rover_0(ctx context.Context, marshaler runtime.Marshaler, server RoveServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq RoverRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.Rover(ctx, &protoReq)
return msg, metadata, err
}
// RegisterRoveHandlerServer registers the http handlers for service Rove to "mux".
// UnaryRPC :call RoveServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
func RegisterRoveHandlerServer(ctx context.Context, mux *runtime.ServeMux, server RoveServer) error {
mux.Handle("GET", pattern_Rove_Status_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Status_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Status_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Register_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Register_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Register_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Commands_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Commands_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Commands_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Radar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Radar_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Radar_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Rover_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_Rove_Rover_0(rctx, inboundMarshaler, server, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Rover_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterRoveHandlerFromEndpoint is same as RegisterRoveHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterRoveHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterRoveHandler(ctx, mux, conn)
}
// RegisterRoveHandler registers the http handlers for service Rove to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterRoveHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterRoveHandlerClient(ctx, mux, NewRoveClient(conn))
}
// RegisterRoveHandlerClient registers the http handlers for service Rove
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "RoveClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "RoveClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "RoveClient" to call the correct interceptors.
func RegisterRoveHandlerClient(ctx context.Context, mux *runtime.ServeMux, client RoveClient) error {
mux.Handle("GET", pattern_Rove_Status_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Status_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Status_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Register_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Register_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Register_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Commands_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Commands_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Commands_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Radar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Radar_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Radar_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_Rove_Rover_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Rove_Rover_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Rove_Rover_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
return nil
}
var (
pattern_Rove_Status_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"status"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Register_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"register"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Commands_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"commands"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Radar_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"radar"}, "", runtime.AssumeColonVerbOpt(true)))
pattern_Rove_Rover_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0}, []string{"rover"}, "", runtime.AssumeColonVerbOpt(true)))
)
var (
forward_Rove_Status_0 = runtime.ForwardResponseMessage
forward_Rove_Register_0 = runtime.ForwardResponseMessage
forward_Rove_Commands_0 = runtime.ForwardResponseMessage
forward_Rove_Radar_0 = runtime.ForwardResponseMessage
forward_Rove_Rover_0 = runtime.ForwardResponseMessage
)

View file

@ -1,354 +0,0 @@
{
"swagger": "2.0",
"info": {
"title": "Rove",
"description": "Rove is an asychronous nomadic game about exploring a planet as part of a loose community",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/commands": {
"post": {
"summary": "Send commands to rover",
"description": "Sending commands to this endpoint will queue them to be executed during the following ticks, in the order sent",
"operationId": "Rove_Commands",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveCommandsResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveCommandsRequest"
}
}
],
"tags": [
"Rove"
]
}
},
"/radar": {
"post": {
"summary": "Get radar information",
"description": "Gets the radar output for the given rover",
"operationId": "Rove_Radar",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveRadarResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveRadarRequest"
}
}
],
"tags": [
"Rove"
]
}
},
"/register": {
"post": {
"summary": "Register an account",
"description": "Tries to register an account with the given name",
"operationId": "Rove_Register",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveRegisterResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveRegisterRequest"
}
}
],
"tags": [
"Rove"
]
}
},
"/rover": {
"post": {
"summary": "Get rover information",
"description": "Gets information for the account's rover",
"operationId": "Rove_Rover",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveRoverResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/roveRoverRequest"
}
}
],
"tags": [
"Rove"
]
}
},
"/status": {
"get": {
"summary": "Server status",
"description": "Responds with various details about the current server status",
"operationId": "Rove_Status",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/roveStatusResponse"
}
},
"default": {
"description": "An unexpected error response",
"schema": {
"$ref": "#/definitions/gatewayruntimeError"
}
}
},
"tags": [
"Rove"
]
}
}
},
"definitions": {
"gatewayruntimeError": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"roveCommand": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The command to execute, currently only accepts move, which requires a bearing and a duration."
},
"bearing": {
"type": "string",
"title": "The bearing, example: NE"
},
"duration": {
"type": "integer",
"format": "int32",
"title": "The duration in ticks, example: 5"
}
}
},
"roveCommandsRequest": {
"type": "object",
"properties": {
"account": {
"type": "string",
"title": "The account to execture these commands"
},
"commands": {
"type": "array",
"items": {
"$ref": "#/definitions/roveCommand"
},
"title": "The set of desired commands"
}
}
},
"roveCommandsResponse": {
"type": "object",
"title": "Empty placeholder"
},
"roveRadarRequest": {
"type": "object",
"properties": {
"account": {
"type": "string",
"title": "The account for this request"
}
}
},
"roveRadarResponse": {
"type": "object",
"properties": {
"range": {
"type": "integer",
"format": "int32",
"title": "The range in tiles from the rover of the radar data"
},
"tiles": {
"type": "string",
"format": "byte",
"title": "A 1D array representing range*2 + 1 squared set of tiles, origin bottom left and in row-\u003ecolumn order"
}
}
},
"roveRegisterRequest": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "The desired account name"
}
}
},
"roveRegisterResponse": {
"type": "object",
"title": "Empty placeholder"
},
"roveRoverRequest": {
"type": "object",
"properties": {
"account": {
"type": "string",
"title": "The account for this request"
}
}
},
"roveRoverResponse": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "The name of the rover"
},
"position": {
"$ref": "#/definitions/roveVector",
"title": "Position of the rover in world coordinates"
},
"range": {
"type": "integer",
"format": "int32",
"title": "The range of this rover's radar"
},
"speed": {
"type": "integer",
"format": "int32",
"title": "The speed the rover can move per tick"
}
}
},
"roveStatusResponse": {
"type": "object",
"properties": {
"next_tick": {
"type": "string",
"title": "The time the next tick will occur"
},
"ready": {
"type": "boolean",
"format": "boolean",
"title": "Whether the server is ready to accept requests"
},
"tick": {
"type": "integer",
"format": "int32",
"title": "The tick rate of the server in minutes (how many minutes per tick)"
},
"version": {
"type": "string",
"title": "The version of the server in v{major}.{minor}-{delta}-{sha} form"
}
}
},
"roveVector": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"format": "int32"
},
"y": {
"type": "integer",
"format": "int32"
}
}
}
}
}

137
pkg/rove/rover.go Normal file
View file

@ -0,0 +1,137 @@
package rove
import (
"bufio"
"fmt"
"log"
"math/rand"
"os"
"time"
"github.com/google/uuid"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
const (
maxLogEntries = 16
)
// RoverLogEntry describes a single log entry for the rover
type RoverLogEntry struct {
// Time is the timestamp of the entry
Time time.Time
// Text contains the information in this log entry
Text string
}
// Rover describes a single rover in the world
type Rover struct {
// Unique name of this rover
Name string
// Pos represents where this rover is in the world
Pos maths.Vector
// Bearing is the current direction the rover is facing
Bearing roveapi.Bearing
// Range represents the distance the unit's radar can see
Range int
// Inventory represents any items the rover is carrying
Inventory []Object
// Capacity is the maximum number of inventory items
Capacity int
// Integrity represents current rover health
Integrity int
// MaximumIntegrity is the full integrity of the rover
MaximumIntegrity int
// Charge is the amount of energy the rover has
Charge int
// MaximumCharge is the maximum charge able to be stored
MaximumCharge int
// SailPosition is the current position of the sails
SailPosition roveapi.SailPosition
// Current number of ticks in this move, used for sailing speeds
MoveTicks int
// Logs Stores log of information
Logs []RoverLogEntry
// The account that owns this rover
Owner string
}
// DefaultRover returns a default rover object with default settings
func DefaultRover() *Rover {
return &Rover{
Range: 10,
Integrity: 10,
MaximumIntegrity: 10,
Capacity: 10,
Charge: 10,
MaximumCharge: 10,
Bearing: roveapi.Bearing_North,
SailPosition: roveapi.SailPosition_SolarCharging,
Name: GenerateRoverName(),
}
}
// AddLogEntryf adds an entry to the rovers log
func (r *Rover) AddLogEntryf(format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Printf("%s log entry: %s", r.Name, text)
r.Logs = append(r.Logs,
RoverLogEntry{
Time: time.Now(),
Text: text,
},
)
// Limit the number of logs
if len(r.Logs) > maxLogEntries {
r.Logs = r.Logs[len(r.Logs)-maxLogEntries:]
}
}
var wordsFile = os.Getenv("WORDS_FILE")
var roverWords []string
// GenerateRoverName generates a new rover name
func GenerateRoverName() string {
// Try and load the rover words file
if len(roverWords) == 0 {
// Try and load the words file
if file, err := os.Open(wordsFile); err != nil {
log.Printf("Couldn't read words file [%s], running without words: %s\n", wordsFile, err)
} else {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
roverWords = append(roverWords, scanner.Text())
}
if scanner.Err() != nil {
log.Printf("Failure during word file scan: %s\n", scanner.Err())
}
}
}
// Assign a random name if we have words
if len(roverWords) > 0 {
// Loop until we find a unique name
return fmt.Sprintf("%s-%s", roverWords[rand.Intn(len(roverWords))], roverWords[rand.Intn(len(roverWords))])
}
// Default to a unique string
return uuid.New().String()
}

840
pkg/rove/world.go Normal file
View file

@ -0,0 +1,840 @@
package rove
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"sync"
"github.com/mdiluz/rove/pkg/accounts"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
)
const (
// ticksPerNormalMove defines the number of ticks it should take for a "normal" speed move
ticksPerNormalMove = 4
// upgradeCost is the cost in rover parts needed to upgrade a rover specification
upgradeCost = 5
)
// CommandStream is a list of commands to execute in order
type CommandStream []*roveapi.Command
// World describes a self contained universe and everything in it
type World struct {
// TicksPerDay is the amount of ticks in a single day
TicksPerDay int
// Current number of ticks from the start
CurrentTicks int
// Rovers is a id->data map of all the rovers in the game
Rovers map[string]*Rover
// Atlas represends the world map of chunks and tiles
Atlas Atlas
// Wind is the current wind direction
Wind roveapi.Bearing
// Commands is the set of currently executing command streams per rover
CommandQueue map[string]CommandStream
// Accountant
Accountant accounts.Accountant
// Mutex to lock around all world operations
worldMutex sync.RWMutex
// Mutex to lock around command operations
cmdMutex sync.RWMutex
}
// NewWorld creates a new world object
func NewWorld(chunkSize int) *World {
return &World{
Rovers: make(map[string]*Rover),
CommandQueue: make(map[string]CommandStream),
Atlas: NewChunkAtlas(chunkSize),
TicksPerDay: 24,
CurrentTicks: 0,
Accountant: accounts.NewSimpleAccountant(),
Wind: roveapi.Bearing_North,
}
}
// SpawnRover adds an rover to the game (without lock)
func (w *World) SpawnRover(account string) (string, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
// Initialise the rover
rover := DefaultRover()
// Assign the owner
rover.Owner = account
// Spawn in a random place near the origin
rover.Pos = maths.Vector{
X: 10 - rand.Intn(20),
Y: 10 - rand.Intn(20),
}
// Seach until we error (run out of world)
for {
_, obj := w.Atlas.QueryPosition(rover.Pos)
if !obj.IsBlocking() {
break
} else {
// Try and spawn to the east of the blockage
rover.Pos.Add(maths.Vector{X: 1, Y: 0})
}
}
// Add a log entry for robot creation
rover.AddLogEntryf("created at %+v", rover.Pos)
// Append the rover to the list
w.Rovers[rover.Name] = rover
var err error
// Only assign if we've been given an account
if len(account) > 0 {
err = w.Accountant.AssignData(account, "rover", rover.Name)
}
return rover.Name, err
}
// GetRover gets a specific rover by name
func (w *World) GetRover(rover string) (Rover, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return Rover{}, fmt.Errorf("Failed to find rover with name: %s", rover)
}
return *i, nil
}
// RoverRecharge charges up a rover
func (w *World) RoverRecharge(rover string) (int, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return 0, fmt.Errorf("Failed to find rover with name: %s", rover)
}
// We can only recharge during the day
if !w.Daytime() {
return i.Charge, nil
}
// Add one charge
if i.Charge < i.MaximumCharge {
i.Charge++
i.AddLogEntryf("recharged to %d", i.Charge)
}
return i.Charge, nil
}
// RoverBroadcast broadcasts a message to nearby rovers
func (w *World) RoverBroadcast(rover string, message []byte) (err error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("Failed to find rover with name: %s", rover)
}
// Use up a charge as needed, if available
if i.Charge == 0 {
return
}
i.Charge--
// Check all rovers
for r, rover := range w.Rovers {
if rover.Name == i.Name {
continue
}
// Check if this rover is within range
if i.Pos.Distance(rover.Pos) < float64(i.Range) {
rover.AddLogEntryf("recieved %s from %s", string(message), i.Name)
w.Rovers[r] = rover
}
}
i.AddLogEntryf("broadcasted %s", string(message))
return
}
// DestroyRover Removes an rover from the game
func (w *World) DestroyRover(rover string) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
// Remove this rover from tracked rovers
delete(w.Rovers, rover)
r.Owner = ""
r.AddLogEntryf("rover destroyed")
// Marshal the rover data
data, err := json.Marshal(r)
if err != nil {
return err
}
// Place the dormant rover down
w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_RoverDormant, Data: data})
return nil
}
// RoverPosition returns the position of the rover
func (w *World) RoverPosition(rover string) (maths.Vector, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return maths.Vector{}, fmt.Errorf("no rover matching id")
}
return i.Pos, nil
}
// SetRoverPosition sets the position of the rover
func (w *World) SetRoverPosition(rover string, pos maths.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
i.Pos = pos
return nil
}
// RoverInventory returns the inventory of a requested rover
func (w *World) RoverInventory(rover string) ([]Object, error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
i, ok := w.Rovers[rover]
if !ok {
return nil, fmt.Errorf("no rover matching id")
}
return i.Inventory, nil
}
// WarpRover sets an rovers position
func (w *World) WarpRover(rover string, pos maths.Vector) error {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return fmt.Errorf("no rover matching id")
}
// Nothing to do if these positions match
if i.Pos == pos {
return nil
}
// Check the tile is not blocked
_, obj := w.Atlas.QueryPosition(pos)
if obj.IsBlocking() {
return fmt.Errorf("can't warp rover to occupied tile, check before warping")
}
i.Pos = pos
return nil
}
// TryMoveRover attempts to move a rover in a specific direction
func (w *World) TryMoveRover(rover string, b roveapi.Bearing) (maths.Vector, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
i, ok := w.Rovers[rover]
if !ok {
return maths.Vector{}, fmt.Errorf("no rover matching id")
}
// Try the new move position
newPos := i.Pos.Added(maths.BearingToVector(b))
// Get the tile and verify it's empty
_, obj := w.Atlas.QueryPosition(newPos)
if !obj.IsBlocking() {
i.AddLogEntryf("moved %s to %+v", b.String(), newPos)
// Perform the move
i.Pos = newPos
} else {
// If it is a blocking tile, reduce the rover integrity
i.AddLogEntryf("tried to move %s to %+v", b.String(), newPos)
i.Integrity = i.Integrity - 1
i.AddLogEntryf("had a collision, new integrity %d", i.Integrity)
}
return i.Pos, nil
}
// RoverStash will stash an item at the current rovers position
func (w *World) RoverStash(rover string) (roveapi.Object, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return roveapi.Object_ObjectUnknown, fmt.Errorf("no rover matching id")
}
// Can't pick up when full
if len(r.Inventory) >= r.Capacity {
r.AddLogEntryf("tried to stash object but inventory was full")
return roveapi.Object_ObjectUnknown, nil
}
// Ensure the rover has energy
if r.Charge <= 0 {
r.AddLogEntryf("tried to stash object but had no charge")
return roveapi.Object_ObjectUnknown, nil
}
r.Charge--
_, obj := w.Atlas.QueryPosition(r.Pos)
if !obj.IsStashable() {
return roveapi.Object_ObjectUnknown, nil
}
r.AddLogEntryf("stashed %c", obj.Type)
r.Inventory = append(r.Inventory, obj)
w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_ObjectUnknown})
return obj.Type, nil
}
// RoverSalvage will salvage a rover for parts
func (w *World) RoverSalvage(rover string) (roveapi.Object, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return roveapi.Object_ObjectUnknown, fmt.Errorf("no rover matching id")
}
// Can't pick up when full
if len(r.Inventory) >= r.Capacity {
r.AddLogEntryf("tried to salvage dormant rover but inventory was full")
return roveapi.Object_ObjectUnknown, nil
}
// Ensure the rover has energy
if r.Charge <= 0 {
r.AddLogEntryf("tried to salvage dormant rover but had no charge")
return roveapi.Object_ObjectUnknown, nil
}
r.Charge--
_, obj := w.Atlas.QueryPosition(r.Pos)
if obj.Type != roveapi.Object_RoverDormant {
r.AddLogEntryf("tried to salvage dormant rover but found no rover to salvage")
return roveapi.Object_ObjectUnknown, nil
}
r.AddLogEntryf("salvaged dormant rover")
for i := 0; i < 5; i++ {
if len(r.Inventory) == r.Capacity {
break
}
r.Inventory = append(r.Inventory, Object{Type: roveapi.Object_RoverParts})
}
w.Atlas.SetObject(r.Pos, Object{Type: roveapi.Object_ObjectUnknown})
return obj.Type, nil
}
// RoverTransfer will transfer rover control to dormant rover
func (w *World) RoverTransfer(rover string) (string, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
oldRover, ok := w.Rovers[rover]
if !ok {
return "", fmt.Errorf("no rover matching id")
}
_, obj := w.Atlas.QueryPosition(oldRover.Pos)
if obj.Type != roveapi.Object_RoverDormant {
oldRover.AddLogEntryf("tried to transfer to dormant rover but found no rover")
return "", nil
}
// Unmarshal the dormant rover
var newRover Rover
err := json.Unmarshal(obj.Data, &newRover)
if err != nil {
return "", err
}
// Add logs
oldRover.AddLogEntryf("transferring to dormant rover %s", newRover.Name)
newRover.AddLogEntryf("transferred from rover %s", oldRover.Name)
// Transfer the ownership
err = w.Accountant.AssignData(oldRover.Owner, "rover", newRover.Name)
if err != nil {
return "", err
}
newRover.Owner = oldRover.Owner
oldRover.Owner = ""
// Place the old rover in the world
oldRoverData, err := json.Marshal(oldRover)
if err != nil {
return "", err
}
w.Atlas.SetObject(oldRover.Pos, Object{Type: roveapi.Object_RoverDormant, Data: oldRoverData})
// Swap the rovers in the tracking
w.Rovers[newRover.Name] = &newRover
delete(w.Rovers, oldRover.Name)
// Clear the command queues for both rovers
delete(w.CommandQueue, oldRover.Name)
delete(w.CommandQueue, newRover.Name)
return newRover.Name, nil
}
// RoverToggle will toggle the sail position
func (w *World) RoverToggle(rover string) (roveapi.SailPosition, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return roveapi.SailPosition_UnknownSailPosition, fmt.Errorf("no rover matching id")
}
// Swap the sail position
switch r.SailPosition {
case roveapi.SailPosition_CatchingWind:
r.SailPosition = roveapi.SailPosition_SolarCharging
case roveapi.SailPosition_SolarCharging:
r.SailPosition = roveapi.SailPosition_CatchingWind
}
// Reset the movement ticks
r.MoveTicks = 0
return r.SailPosition, nil
}
// RoverUpgrade will try to upgrade the rover
func (w *World) RoverUpgrade(rover string, upgrade roveapi.RoverUpgrade) (int, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return 0, fmt.Errorf("no rover matching id")
}
cost := upgradeCost
num := 0
for i := range r.Inventory {
if r.Inventory[i].Type == roveapi.Object_RoverParts {
num++
}
}
if num < cost {
r.AddLogEntryf("tried to upgrade but lacked rover parts")
return 0, nil
}
// Apply the upgrade
var ret int
switch upgrade {
case roveapi.RoverUpgrade_Capacity:
r.Capacity++
ret = r.Capacity
case roveapi.RoverUpgrade_Range:
r.Range++
ret = r.Range
case roveapi.RoverUpgrade_MaximumCharge:
r.MaximumCharge++
ret = r.MaximumCharge
case roveapi.RoverUpgrade_MaximumIntegrity:
r.MaximumIntegrity++
ret = r.MaximumIntegrity
default:
return 0, fmt.Errorf("unknown upgrade: %s", upgrade)
}
// Remove the cost in rover parts
var n []Object
for _, o := range r.Inventory {
if o.Type == roveapi.Object_RoverParts && cost > 0 {
cost--
} else {
n = append(n, o)
}
}
// Assign back the inventory
r.Inventory = n
r.AddLogEntryf("upgraded %s to %d", upgrade, ret)
return ret, nil
}
// RoverTurn will turn the rover
func (w *World) RoverTurn(rover string, bearing roveapi.Bearing) (roveapi.Bearing, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return roveapi.Bearing_BearingUnknown, fmt.Errorf("no rover matching id")
}
// Set the new bearing
r.Bearing = bearing
// Reset the movement ticks
r.MoveTicks = 0
return r.Bearing, nil
}
// RoverRepair will turn the rover
func (w *World) RoverRepair(rover string) (int, error) {
w.worldMutex.Lock()
defer w.worldMutex.Unlock()
r, ok := w.Rovers[rover]
if !ok {
return 0, fmt.Errorf("no rover matching id")
}
// Can't repair past max
if r.Integrity >= r.MaximumIntegrity {
return r.Integrity, nil
}
// Find rover parts in inventory
for i, o := range r.Inventory {
if o.Type == roveapi.Object_RoverParts {
// Copy-erase from slice
r.Inventory[i] = r.Inventory[len(r.Inventory)-1]
r.Inventory = r.Inventory[:len(r.Inventory)-1]
// Repair
r.Integrity = r.Integrity + 1
r.AddLogEntryf("repaired self to %d", r.Integrity)
break
}
}
return r.Integrity, nil
}
// RadarFromRover can be used to query what a rover can currently see
func (w *World) RadarFromRover(rover string) (radar []roveapi.Tile, objs []roveapi.Object, err error) {
w.worldMutex.RLock()
defer w.worldMutex.RUnlock()
r, ok := w.Rovers[rover]
if !ok {
err = fmt.Errorf("no rover matching id")
return
}
// The radar should span in range direction on each axis, plus the row/column the rover is currently on
radarSpan := (r.Range * 2) + 1
roverPos := r.Pos
// Get the radar min and max values
radarMin := maths.Vector{
X: roverPos.X - r.Range,
Y: roverPos.Y - r.Range,
}
radarMax := maths.Vector{
X: roverPos.X + r.Range,
Y: roverPos.Y + r.Range,
}
// Gather up all tiles within the range
radar = make([]roveapi.Tile, radarSpan*radarSpan)
objs = make([]roveapi.Object, radarSpan*radarSpan)
for j := radarMin.Y; j <= radarMax.Y; j++ {
for i := radarMin.X; i <= radarMax.X; i++ {
q := maths.Vector{X: i, Y: j}
tile, obj := w.Atlas.QueryPosition(q)
// Get the position relative to the bottom left of the radar
relative := q.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
radar[index] = tile
objs[index] = obj.Type
}
}
// Add all rovers to the radar
for _, r := range w.Rovers {
// If the rover is in range
dist := r.Pos.Added(roverPos.Negated())
dist = dist.Abs()
if dist.X <= r.Range && dist.Y <= r.Range {
relative := r.Pos.Added(radarMin.Negated())
index := relative.X + relative.Y*radarSpan
objs[index] = roveapi.Object_RoverLive
}
}
return radar, objs, nil
}
// RoverCommands returns current commands for the given rover
func (w *World) RoverCommands(rover string) (queued CommandStream) {
if c, ok := w.CommandQueue[rover]; ok {
queued = c
}
return
}
// Enqueue will queue the commands given
func (w *World) Enqueue(rover string, commands ...*roveapi.Command) error {
// First validate the commands
for _, c := range commands {
switch c.Command {
case roveapi.CommandType_broadcast:
if len(c.GetData()) > 3 {
return fmt.Errorf("too many characters in message (limit 3): %d", len(c.GetData()))
}
for _, b := range c.GetData() {
if b < 37 || b > 126 {
return fmt.Errorf("invalid message character: %c", b)
}
}
case roveapi.CommandType_turn:
if c.GetBearing() == roveapi.Bearing_BearingUnknown {
return fmt.Errorf("turn command given unknown bearing")
}
case roveapi.CommandType_upgrade:
if c.GetUpgrade() == roveapi.RoverUpgrade_RoverUpgradeUnknown {
return fmt.Errorf("upgrade command given unknown upgrade")
}
case roveapi.CommandType_wait:
case roveapi.CommandType_toggle:
case roveapi.CommandType_stash:
case roveapi.CommandType_repair:
case roveapi.CommandType_salvage:
case roveapi.CommandType_transfer:
// Nothing to verify
default:
return fmt.Errorf("unknown command: %s", c.Command)
}
}
// Lock our commands edit
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
w.CommandQueue[rover] = commands
return nil
}
// Tick will execute any commands in the current command queue and tick the world
func (w *World) Tick() {
w.cmdMutex.Lock()
defer w.cmdMutex.Unlock()
// Iterate through all the current commands
for rover, cmds := range w.CommandQueue {
if len(cmds) != 0 {
// Execute the command
if done, err := w.ExecuteCommand(cmds[0], rover); err != nil {
log.Println(err)
// TODO: Report this error somehow
} else if done {
// Extract the first command in the queue
// Only if the command queue still has entries (the command may have modified this queue)
if _, ok := w.CommandQueue[rover]; ok {
w.CommandQueue[rover] = cmds[1:]
}
}
} else {
// Clean out the empty entry
delete(w.CommandQueue, rover)
}
}
// Move all the rovers based on current wind and sails
for n, r := range w.Rovers {
// Skip if we're not catching the wind
if r.SailPosition != roveapi.SailPosition_CatchingWind {
continue
}
// Increment the current move ticks
r.MoveTicks++
// Get the difference between the two bearings
// Normalise, we don't care about clockwise/anticlockwise
diff := maths.Abs(int(w.Wind - r.Bearing))
if diff > 4 {
diff = 8 - diff
}
// Calculate the travel "ticks"
var ticksToMove int
switch diff {
case 0:
// Going with the wind, travel at base speed of once every 4 ticks
ticksToMove = ticksPerNormalMove
case 1:
// At a slight angle, we can go a little faster
ticksToMove = ticksPerNormalMove / 2
case 2:
// Perpendicular to wind, max speed
ticksToMove = 1
case 3:
// Heading at 45 degrees into the wind, back to min speed
ticksToMove = ticksPerNormalMove
case 4:
// Heading durectly into the wind, no movement at all
default:
log.Fatalf("bearing difference of %d should be impossible", diff)
}
// If we've incremented over the current move ticks on the rover, we can try and make the move
if ticksToMove != 0 && r.MoveTicks >= ticksToMove {
_, err := w.TryMoveRover(n, r.Bearing)
if err != nil {
log.Println(err)
// TODO: Report this error somehow
}
// Reset the move ticks
r.MoveTicks = 0
}
}
// Check all rover integrities
for _, r := range w.Rovers {
if r.Integrity <= 0 {
// The rover has died destroy it
err := w.DestroyRover(r.Name)
if err != nil {
log.Println(err)
// TODO: Report this error somehow
}
// Spawn a new one for this account
_, err = w.SpawnRover(r.Owner)
if err != nil {
log.Println(err)
// TODO: Report this error somehow
}
}
}
// Increment the current tick count
w.CurrentTicks++
// Change the wind every day
if (w.CurrentTicks % w.TicksPerDay) == 0 {
w.Wind = roveapi.Bearing((rand.Int() % 8) + 1) // Random cardinal bearing
}
}
// ExecuteCommand will execute a single command
func (w *World) ExecuteCommand(c *roveapi.Command, rover string) (done bool, err error) {
log.Printf("Executing command: %+v for %s\n", c.Command, rover)
switch c.Command {
case roveapi.CommandType_toggle:
_, err = w.RoverToggle(rover)
case roveapi.CommandType_stash:
_, err = w.RoverStash(rover)
case roveapi.CommandType_repair:
_, err = w.RoverRepair(rover)
case roveapi.CommandType_broadcast:
err = w.RoverBroadcast(rover, c.GetData())
case roveapi.CommandType_turn:
_, err = w.RoverTurn(rover, c.GetBearing())
case roveapi.CommandType_salvage:
_, err = w.RoverSalvage(rover)
case roveapi.CommandType_transfer:
_, err = w.RoverTransfer(rover)
case roveapi.CommandType_upgrade:
_, err = w.RoverUpgrade(rover, c.GetUpgrade())
case roveapi.CommandType_wait:
// Nothing to do
default:
return true, fmt.Errorf("unknown command: %s", c.Command)
}
// Decrement the repeat number
c.Repeat--
return c.Repeat < 0, err
}
// Daytime returns if it's currently daytime
// for simplicity this uses the 1st half of the day as daytime, the 2nd half as nighttime
func (w *World) Daytime() bool {
tickInDay := w.CurrentTicks % w.TicksPerDay
return tickInDay < w.TicksPerDay/2
}
// RLock read locks the world
func (w *World) RLock() {
w.worldMutex.RLock()
w.cmdMutex.RLock()
}
// RUnlock read unlocks the world
func (w *World) RUnlock() {
w.worldMutex.RUnlock()
w.cmdMutex.RUnlock()
}
// Lock locks the world
func (w *World) Lock() {
w.worldMutex.Lock()
w.cmdMutex.Lock()
}
// Unlock unlocks the world
func (w *World) Unlock() {
w.worldMutex.Unlock()
w.cmdMutex.Unlock()
}

359
pkg/rove/world_test.go Normal file
View file

@ -0,0 +1,359 @@
package rove
import (
"testing"
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/stretchr/testify/assert"
)
func TestNewWorld(t *testing.T) {
// Very basic for now, nothing to verify
world := NewWorld(4)
if world == nil {
t.Error("Failed to create world")
}
}
func TestWorld_CreateRover(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover("")
assert.NoError(t, err)
b, err := world.SpawnRover("")
assert.NoError(t, err)
// Basic duplicate check
if a == b {
t.Errorf("Created identical rovers")
} else if len(world.Rovers) != 2 {
t.Errorf("Incorrect number of rovers created")
}
}
func TestWorld_GetRover(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover("")
assert.NoError(t, err)
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover attribs")
assert.NotZero(t, rover.Range, "Rover should not be spawned blind")
assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "created", "Rover logs should contain the creation")
}
func TestWorld_DestroyRover(t *testing.T) {
world := NewWorld(1)
a, err := world.SpawnRover("")
assert.NoError(t, err)
b, err := world.SpawnRover("")
assert.NoError(t, err)
err = world.DestroyRover(a)
assert.NoError(t, err, "Error returned from rover destroy")
// Basic duplicate check
if len(world.Rovers) != 1 {
t.Error("Too many rovers left in world")
} else if _, ok := world.Rovers[b]; !ok {
t.Error("Remaining rover is incorrect")
}
}
func TestWorld_GetSetMovePosition(t *testing.T) {
world := NewWorld(4)
a, err := world.SpawnRover("")
assert.NoError(t, err)
pos := maths.Vector{
X: 0.0,
Y: 0.0,
}
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
newPos, err := world.RoverPosition(a)
assert.NoError(t, err, "Failed to set position for rover")
assert.Equal(t, pos, newPos, "Failed to correctly set position for rover")
b := roveapi.Bearing_North
newPos, err = world.TryMoveRover(a, b)
assert.NoError(t, err, "Failed to set position for rover")
pos.Add(maths.Vector{X: 0, Y: 1})
assert.Equal(t, pos, newPos, "Failed to correctly move position for rover")
rover, err := world.GetRover(a)
assert.NoError(t, err, "Failed to get rover information")
assert.Contains(t, rover.Logs[len(rover.Logs)-1].Text, "moved", "Rover logs should contain the move")
// Place a tile in front of the rover
world.Atlas.SetObject(maths.Vector{X: 0, Y: 2}, Object{Type: roveapi.Object_RockLarge})
newPos, err = world.TryMoveRover(a, b)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, newPos, "Failed to correctly not move position for rover into wall")
}
func TestWorld_RadarFromRover(t *testing.T) {
// Create world that should have visible walls on the radar
world := NewWorld(2)
a, err := world.SpawnRover("")
assert.NoError(t, err)
b, err := world.SpawnRover("")
assert.NoError(t, err)
// Warp the rovers into position
bpos := maths.Vector{X: -3, Y: -3}
world.Atlas.SetObject(bpos, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(b, bpos), "Failed to warp rover")
world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(a, maths.Vector{X: 0, Y: 0}), "Failed to warp rover")
r, err := world.GetRover(a)
assert.NoError(t, err)
radar, objs, err := world.RadarFromRover(a)
assert.NoError(t, err, "Failed to get radar from rover")
fullRange := r.Range + r.Range + 1
assert.Equal(t, fullRange*fullRange, len(radar), "Radar returned wrong length")
assert.Equal(t, fullRange*fullRange, len(objs), "Radar returned wrong length")
// TODO: Verify the other rover is on the radar
// Check the radar results are stable
radar1, objs1, err := world.RadarFromRover(a)
assert.NoError(t, err)
radar2, objs2, err := world.RadarFromRover(a)
assert.NoError(t, err)
assert.Equal(t, radar1, radar2)
assert.Equal(t, objs1, objs2)
}
func TestWorld_RoverDamage(t *testing.T) {
world := NewWorld(2)
acc, err := world.Accountant.RegisterAccount("tmp")
assert.NoError(t, err)
a, err := world.SpawnRover(acc.Name)
assert.NoError(t, err)
pos := maths.Vector{
X: 0.0,
Y: 0.0,
}
world.Atlas.SetObject(pos, Object{Type: roveapi.Object_ObjectUnknown})
err = world.WarpRover(a, pos)
assert.NoError(t, err, "Failed to set position for rover")
info, err := world.GetRover(a)
assert.NoError(t, err, "couldn't get rover info")
world.Atlas.SetObject(maths.Vector{X: 0.0, Y: 1.0}, Object{Type: roveapi.Object_RockLarge})
vec, err := world.TryMoveRover(a, roveapi.Bearing_North)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, vec, "Rover managed to move into large rock")
newinfo, err := world.GetRover(a)
assert.NoError(t, err, "couldn't get rover info")
assert.Equal(t, info.Integrity-1, newinfo.Integrity, "rover should have lost integrity")
assert.Contains(t, newinfo.Logs[len(newinfo.Logs)-1].Text, "collision", "Rover logs should contain the collision")
// Keep moving to damage the rover
for i := 0; i < info.Integrity-1; i++ {
vec, err := world.TryMoveRover(a, roveapi.Bearing_North)
assert.NoError(t, err, "Failed to move rover")
assert.Equal(t, pos, vec, "Rover managed to move into large rock")
}
// Tick the world to check for rover deaths
world.Tick()
// Rover should have been destroyed now
_, err = world.GetRover(a)
assert.Error(t, err)
_, obj := world.Atlas.QueryPosition(info.Pos)
assert.Equal(t, roveapi.Object_RoverDormant, obj.Type)
}
func TestWorld_Daytime(t *testing.T) {
world := NewWorld(1)
a, err := world.SpawnRover("")
assert.NoError(t, err)
// Remove rover charge
rover := world.Rovers[a]
rover.Charge = 0
world.Rovers[a] = rover
// Try and recharge, should work
_, err = world.RoverRecharge(a)
assert.NoError(t, err)
assert.Equal(t, 1, world.Rovers[a].Charge)
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.True(t, world.Daytime())
world.Tick()
}
// Remove rover charge again
rover = world.Rovers[a]
rover.Charge = 0
world.Rovers[a] = rover
// Try and recharge, should fail
_, err = world.RoverRecharge(a)
assert.NoError(t, err)
assert.Equal(t, 0, world.Rovers[a].Charge)
// Loop for half the day
for i := 0; i < world.TicksPerDay/2; i++ {
assert.False(t, world.Daytime())
world.Tick()
}
}
func TestWorld_Broadcast(t *testing.T) {
world := NewWorld(8)
a, err := world.SpawnRover("")
assert.NoError(t, err)
b, err := world.SpawnRover("")
assert.NoError(t, err)
// Warp rovers near to eachother
world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
world.Atlas.SetObject(maths.Vector{X: 1, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(a, maths.Vector{X: 0, Y: 0}))
assert.NoError(t, world.WarpRover(b, maths.Vector{X: 1, Y: 0}))
// Broadcast from a
assert.NoError(t, world.RoverBroadcast(a, []byte{'A', 'B', 'C'}))
// Check if b heard it
ra, err := world.GetRover(a)
assert.NoError(t, err)
assert.Equal(t, ra.MaximumCharge-1, ra.Charge, "A should have used a charge to broadcast")
assert.Contains(t, ra.Logs[len(ra.Logs)-1].Text, "ABC", "Rover B should have heard the broadcast")
// Check if a logged it
rb, err := world.GetRover(b)
assert.NoError(t, err)
assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "ABC", "Rover A should have logged it's broadcast")
// Warp B outside of the range of A
world.Atlas.SetObject(maths.Vector{X: ra.Range, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range, Y: 0}))
// Broadcast from a again
assert.NoError(t, world.RoverBroadcast(a, []byte{'X', 'Y', 'Z'}))
// Check if b heard it
ra, err = world.GetRover(b)
assert.NoError(t, err)
assert.NotContains(t, ra.Logs[len(ra.Logs)-1].Text, "XYZ", "Rover B should not have heard the broadcast")
// Check if a logged it
rb, err = world.GetRover(a)
assert.NoError(t, err)
assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "XYZ", "Rover A should have logged it's broadcast")
// Warp B outside of the range of A
world.Atlas.SetObject(maths.Vector{X: ra.Range + 1, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(b, maths.Vector{X: ra.Range + 1, Y: 0}))
// Broadcast from a again
assert.NoError(t, world.RoverBroadcast(a, []byte{'H', 'J', 'K'}))
// Check if b heard it
ra, err = world.GetRover(b)
assert.NoError(t, err)
assert.NotContains(t, ra.Logs[len(ra.Logs)-1].Text, "HJK", "Rover B should have heard the broadcast")
// Check if a logged it
rb, err = world.GetRover(a)
assert.NoError(t, err)
assert.Contains(t, rb.Logs[len(rb.Logs)-1].Text, "HJK", "Rover A should have logged it's broadcast")
}
func TestWorld_Sailing(t *testing.T) {
world := NewWorld(8)
world.Tick() // One initial tick to set the wind direction the first time
world.Wind = roveapi.Bearing_North // Set the wind direction to north
name, err := world.SpawnRover("")
assert.NoError(t, err)
// Warp the rover to 0,0 after clearing it
world.Atlas.SetObject(maths.Vector{X: 0, Y: 0}, Object{Type: roveapi.Object_ObjectUnknown})
assert.NoError(t, world.WarpRover(name, maths.Vector{X: 0, Y: 0}))
s, err := world.RoverToggle(name)
assert.NoError(t, err)
assert.Equal(t, roveapi.SailPosition_CatchingWind, s)
b, err := world.RoverTurn(name, roveapi.Bearing_North)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_North, b)
// Clear the space to the north
world.Atlas.SetObject(maths.Vector{X: 0, Y: 1}, Object{Type: roveapi.Object_ObjectUnknown})
// Tick the world and check we've moved not moved
world.Tick()
info, err := world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 0}, info.Pos)
// Loop a few more times
for i := 0; i < ticksPerNormalMove-2; i++ {
world.Tick()
info, err := world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 0}, info.Pos)
}
// Now check we've moved (after the TicksPerNormalMove number of ticks)
world.Tick()
info, err = world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 1}, info.Pos)
// Reset the world ticks back to stop any wind changes etc.
world.CurrentTicks = 1
// Face the rover south, into the wind
b, err = world.RoverTurn(name, roveapi.Bearing_South)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_South, b)
// Tick a bunch, we should never move
for i := 0; i < ticksPerNormalMove*2; i++ {
world.Tick()
info, err := world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{Y: 1}, info.Pos)
}
// Reset the world ticks back to stop any wind changes etc.
world.CurrentTicks = 1
world.Wind = roveapi.Bearing_SouthEast // Set up a south easternly wind
// Turn the rover perpendicular
b, err = world.RoverTurn(name, roveapi.Bearing_NorthEast)
assert.NoError(t, err)
assert.Equal(t, roveapi.Bearing_NorthEast, b)
// Clear a space
world.Atlas.SetObject(maths.Vector{X: 1, Y: 2}, Object{Type: roveapi.Object_ObjectUnknown})
// Now check we've moved immediately
world.Tick()
info, err = world.GetRover(name)
assert.NoError(t, err)
assert.Equal(t, maths.Vector{X: 1, Y: 2}, info.Pos)
}

70
pkg/rove/worldgen.go Normal file
View file

@ -0,0 +1,70 @@
package rove
import (
"github.com/mdiluz/rove/pkg/maths"
"github.com/mdiluz/rove/proto/roveapi"
"github.com/ojrac/opensimplex-go"
)
// WorldGen describes a world gen algorythm
type WorldGen interface {
// GetTile generates a tile for a location
GetTile(v maths.Vector) roveapi.Tile
// GetObject generates an object for a location
GetObject(v maths.Vector) Object
}
// NoiseWorldGen returns a noise based world generator
type NoiseWorldGen struct {
// noise describes the noise function
noise opensimplex.Noise
}
// NewNoiseWorldGen creates a new noise based world generator
func NewNoiseWorldGen(seed int64) WorldGen {
return &NoiseWorldGen{
noise: opensimplex.New(seed),
}
}
const (
terrainNoiseScale = 15
rockNoiseScale = 3
partsNoiseScale = 2
)
// GetTile returns the chosen tile at a location
func (g *NoiseWorldGen) GetTile(v maths.Vector) roveapi.Tile {
t := g.noise.Eval2(float64(v.X)/terrainNoiseScale, float64(v.Y)/terrainNoiseScale)
switch {
case t > 0.5:
return roveapi.Tile_Gravel
case t > 0.05:
return roveapi.Tile_Sand
default:
return roveapi.Tile_Rock
}
}
// GetObject returns the chosen object at a location
func (g *NoiseWorldGen) GetObject(v maths.Vector) (obj Object) {
r := g.noise.Eval2(float64(v.X)/rockNoiseScale, float64(v.Y)/rockNoiseScale)
switch {
// Prioritise rocks
case r > 0.6:
obj.Type = roveapi.Object_RockLarge
case r > 0.5:
obj.Type = roveapi.Object_RockSmall
default:
// Otherwise, try some rover parts
p := g.noise.Eval2(float64(v.X)/partsNoiseScale, float64(v.Y)/partsNoiseScale)
switch {
case p > 0.7:
obj.Type = roveapi.Object_RoverParts
}
}
return obj
}

View file

@ -1,56 +0,0 @@
package vector
import (
"math"
"github.com/mdiluz/rove/pkg/maths"
)
// Vector desribes a 3D vector
type Vector struct {
X int `json:"x"`
Y int `json:"y"`
}
// Add adds one vector to another
func (v *Vector) Add(v2 Vector) {
v.X += v2.X
v.Y += v2.Y
}
// Added calculates a new vector
func (v Vector) Added(v2 Vector) Vector {
v.Add(v2)
return v
}
// Negated returns a negated vector
func (v Vector) Negated() Vector {
return Vector{-v.X, -v.Y}
}
// Length returns the length of the vector
func (v Vector) Length() float64 {
return math.Sqrt(float64(v.X*v.X + v.Y*v.Y))
}
// Distance returns the distance between two vectors
func (v Vector) Distance(v2 Vector) float64 {
// Negate the two vectors and calciate the length
return v.Added(v2.Negated()).Length()
}
// Multiplied returns the vector multiplied by an int
func (v Vector) Multiplied(val int) Vector {
return Vector{v.X * val, v.Y * val}
}
// Divided returns the vector divided by an int
func (v Vector) Divided(val int) Vector {
return Vector{v.X / val, v.Y / val}
}
// Abs returns an absolute version of the vector
func (v Vector) Abs() Vector {
return Vector{maths.Abs(v.X), maths.Abs(v.Y)}
}

View file

@ -1,3 +1,4 @@
package version
// Version represents a version to be overrided with -ldflags
var Version = "undefined"

View file

@ -1,54 +0,0 @@
syntax = "proto3";
option go_package = "github.com/mdiluz/rove/pkg/accounts";
package accounts;
service Accountant {
// Register should create a new account in the database
// It will return an error if the account already exists
rpc Register(RegisterInfo) returns (RegisterResponse) {}
// AssignValue assigns a key-value pair to an account, or overwrites an existing key
rpc AssignValue(DataKeyValue) returns (DataKeyResponse) {}
// GetValue will get the value for a key for an account
rpc GetValue(DataKey) returns (DataResponse) {}
}
// RegisterInfo contains the information needed to register an account
message RegisterInfo {
// The name for the account, must be unique
string name = 1;
}
// RegisterResponse is the response information from registering an account
message RegisterResponse {}
// DataKeyValue represents a simple key value pair to assign to an account
message DataKeyValue {
// The account to assign the new key value pair to
string account = 1;
// The key value pair to assign
string key = 2;
string value = 3;
}
// DataKeyResponse is a simple response
message DataKeyResponse {}
// DataKey describes a simple key value with an account, for fetching
message DataKey {
// The account to fetch data for
string account = 1;
// The key to fetch
string key = 2;
}
// DataResponse describes a data fetch response
message DataResponse {
// The value of the key
string value = 3;
}

View file

@ -1,23 +0,0 @@
Google APIs
============
Project: Google APIs
URL: https://github.com/google/googleapis
Revision: 3544ab16c3342d790b00764251e348705991ea4b
License: Apache License 2.0
Imported Files
---------------
- google/api/annotations.proto
- google/api/http.proto
- google/api/httpbody.proto
Generated Files
----------------
They are generated from the .proto files by protoc-gen-go.
- google/api/annotations.pb.go
- google/api/http.pb.go

View file

@ -1,31 +0,0 @@
// Copyright (c) 2015, Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
import "google/api/http.proto";
import "google/protobuf/descriptor.proto";
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
extend google.protobuf.MethodOptions {
// See `HttpRule`.
HttpRule http = 72295728;
}

View file

@ -1,318 +0,0 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
// Defines the HTTP configuration for an API service. It contains a list of
// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
// to one or more HTTP REST API methods.
message Http {
// A list of HTTP configuration rules that apply to individual API methods.
//
// **NOTE:** All service configuration rules follow "last one wins" order.
repeated HttpRule rules = 1;
// When set to true, URL path parmeters will be fully URI-decoded except in
// cases of single segment matches in reserved expansion, where "%2F" will be
// left encoded.
//
// The default behavior is to not decode RFC 6570 reserved characters in multi
// segment matches.
bool fully_decode_reserved_expansion = 2;
}
// `HttpRule` defines the mapping of an RPC method to one or more HTTP
// REST API methods. The mapping specifies how different portions of the RPC
// request message are mapped to URL path, URL query parameters, and
// HTTP request body. The mapping is typically specified as an
// `google.api.http` annotation on the RPC method,
// see "google/api/annotations.proto" for details.
//
// The mapping consists of a field specifying the path template and
// method kind. The path template can refer to fields in the request
// message, as in the example below which describes a REST GET
// operation on a resource collection of messages:
//
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}";
// }
// }
// message GetMessageRequest {
// message SubMessage {
// string subfield = 1;
// }
// string message_id = 1; // mapped to the URL
// SubMessage sub = 2; // `sub.subfield` is url-mapped
// }
// message Message {
// string text = 1; // content of the resource
// }
//
// The same http annotation can alternatively be expressed inside the
// `GRPC API Configuration` YAML file.
//
// http:
// rules:
// - selector: <proto_package_name>.Messaging.GetMessage
// get: /v1/messages/{message_id}/{sub.subfield}
//
// This definition enables an automatic, bidrectional mapping of HTTP
// JSON to RPC. Example:
//
// HTTP | RPC
// -----|-----
// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))`
//
// In general, not only fields but also field paths can be referenced
// from a path pattern. Fields mapped to the path pattern cannot be
// repeated and must have a primitive (non-message) type.
//
// Any fields in the request message which are not bound by the path
// pattern automatically become (optional) HTTP query
// parameters. Assume the following definition of the request message:
//
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http).get = "/v1/messages/{message_id}";
// }
// }
// message GetMessageRequest {
// message SubMessage {
// string subfield = 1;
// }
// string message_id = 1; // mapped to the URL
// int64 revision = 2; // becomes a parameter
// SubMessage sub = 3; // `sub.subfield` becomes a parameter
// }
//
//
// This enables a HTTP JSON to RPC mapping as below:
//
// HTTP | RPC
// -----|-----
// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))`
//
// Note that fields which are mapped to HTTP parameters must have a
// primitive type or a repeated primitive type. Message types are not
// allowed. In the case of a repeated type, the parameter can be
// repeated in the URL, as in `...?param=A&param=B`.
//
// For HTTP method kinds which allow a request body, the `body` field
// specifies the mapping. Consider a REST update method on the
// message resource collection:
//
//
// service Messaging {
// rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
// option (google.api.http) = {
// put: "/v1/messages/{message_id}"
// body: "message"
// };
// }
// }
// message UpdateMessageRequest {
// string message_id = 1; // mapped to the URL
// Message message = 2; // mapped to the body
// }
//
//
// The following HTTP JSON to RPC mapping is enabled, where the
// representation of the JSON in the request body is determined by
// protos JSON encoding:
//
// HTTP | RPC
// -----|-----
// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })`
//
// The special name `*` can be used in the body mapping to define that
// every field not bound by the path template should be mapped to the
// request body. This enables the following alternative definition of
// the update method:
//
// service Messaging {
// rpc UpdateMessage(Message) returns (Message) {
// option (google.api.http) = {
// put: "/v1/messages/{message_id}"
// body: "*"
// };
// }
// }
// message Message {
// string message_id = 1;
// string text = 2;
// }
//
//
// The following HTTP JSON to RPC mapping is enabled:
//
// HTTP | RPC
// -----|-----
// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")`
//
// Note that when using `*` in the body mapping, it is not possible to
// have HTTP parameters, as all fields not bound by the path end in
// the body. This makes this option more rarely used in practice of
// defining REST APIs. The common usage of `*` is in custom methods
// which don't use the URL at all for transferring data.
//
// It is possible to define multiple HTTP methods for one RPC by using
// the `additional_bindings` option. Example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get: "/v1/messages/{message_id}"
// additional_bindings {
// get: "/v1/users/{user_id}/messages/{message_id}"
// }
// };
// }
// }
// message GetMessageRequest {
// string message_id = 1;
// string user_id = 2;
// }
//
//
// This enables the following two alternative HTTP JSON to RPC
// mappings:
//
// HTTP | RPC
// -----|-----
// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")`
//
// # Rules for HTTP mapping
//
// The rules for mapping HTTP path, query parameters, and body fields
// to the request message are as follows:
//
// 1. The `body` field specifies either `*` or a field path, or is
// omitted. If omitted, it indicates there is no HTTP request body.
// 2. Leaf fields (recursive expansion of nested messages in the
// request) can be classified into three types:
// (a) Matched in the URL template.
// (b) Covered by body (if body is `*`, everything except (a) fields;
// else everything under the body field)
// (c) All other fields.
// 3. URL query parameters found in the HTTP request are mapped to (c) fields.
// 4. Any body sent with an HTTP request can contain only (b) fields.
//
// The syntax of the path template is as follows:
//
// Template = "/" Segments [ Verb ] ;
// Segments = Segment { "/" Segment } ;
// Segment = "*" | "**" | LITERAL | Variable ;
// Variable = "{" FieldPath [ "=" Segments ] "}" ;
// FieldPath = IDENT { "." IDENT } ;
// Verb = ":" LITERAL ;
//
// The syntax `*` matches a single path segment. The syntax `**` matches zero
// or more path segments, which must be the last part of the path except the
// `Verb`. The syntax `LITERAL` matches literal text in the path.
//
// The syntax `Variable` matches part of the URL path as specified by its
// template. A variable template must not contain other variables. If a variable
// matches a single path segment, its template may be omitted, e.g. `{var}`
// is equivalent to `{var=*}`.
//
// If a variable contains exactly one path segment, such as `"{var}"` or
// `"{var=*}"`, when such a variable is expanded into a URL path, all characters
// except `[-_.~0-9a-zA-Z]` are percent-encoded. Such variables show up in the
// Discovery Document as `{var}`.
//
// If a variable contains one or more path segments, such as `"{var=foo/*}"`
// or `"{var=**}"`, when such a variable is expanded into a URL path, all
// characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. Such variables
// show up in the Discovery Document as `{+var}`.
//
// NOTE: While the single segment variable matches the semantics of
// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2
// Simple String Expansion, the multi segment variable **does not** match
// RFC 6570 Reserved Expansion. The reason is that the Reserved Expansion
// does not expand special characters like `?` and `#`, which would lead
// to invalid URLs.
//
// NOTE: the field paths in variables and in the `body` must not refer to
// repeated fields or map fields.
message HttpRule {
// Selects methods to which this rule applies.
//
// Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
string selector = 1;
// Determines the URL pattern is matched by this rules. This pattern can be
// used with any of the {get|put|post|delete|patch} methods. A custom method
// can be defined using the 'custom' field.
oneof pattern {
// Used for listing and getting information about resources.
string get = 2;
// Used for updating a resource.
string put = 3;
// Used for creating a resource.
string post = 4;
// Used for deleting a resource.
string delete = 5;
// Used for updating a resource.
string patch = 6;
// The custom pattern is used for specifying an HTTP method that is not
// included in the `pattern` field, such as HEAD, or "*" to leave the
// HTTP method unspecified for this rule. The wild-card rule is useful
// for services that provide content to Web (HTML) clients.
CustomHttpPattern custom = 8;
}
// The name of the request field whose value is mapped to the HTTP body, or
// `*` for mapping all fields not captured by the path pattern to the HTTP
// body. NOTE: the referred field must not be a repeated field and must be
// present at the top-level of request message type.
string body = 7;
// Optional. The name of the response field whose value is mapped to the HTTP
// body of response. Other response fields are ignored. When
// not set, the response message will be used as HTTP body of response.
string response_body = 12;
// Additional HTTP bindings for the selector. Nested bindings must
// not contain an `additional_bindings` field themselves (that is,
// the nesting may only be one level deep).
repeated HttpRule additional_bindings = 11;
}
// A custom pattern is used for defining custom HTTP verb.
message CustomHttpPattern {
// The name of this custom HTTP verb.
string kind = 1;
// The path matched by this custom verb.
string path = 2;
}

View file

@ -1,78 +0,0 @@
// Copyright 2018 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
syntax = "proto3";
package google.api;
import "google/protobuf/any.proto";
option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody";
option java_multiple_files = true;
option java_outer_classname = "HttpBodyProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
// Message that represents an arbitrary HTTP body. It should only be used for
// payload formats that can't be represented as JSON, such as raw binary or
// an HTML page.
//
//
// This message can be used both in streaming and non-streaming API methods in
// the request as well as the response.
//
// It can be used as a top-level request field, which is convenient if one
// wants to extract parameters from either the URL or HTTP template into the
// request fields and also want access to the raw HTTP body.
//
// Example:
//
// message GetResourceRequest {
// // A unique request id.
// string request_id = 1;
//
// // The raw HTTP body is bound to this field.
// google.api.HttpBody http_body = 2;
// }
//
// service ResourceService {
// rpc GetResource(GetResourceRequest) returns (google.api.HttpBody);
// rpc UpdateResource(google.api.HttpBody) returns
// (google.protobuf.Empty);
// }
//
// Example with streaming methods:
//
// service CaldavService {
// rpc GetCalendar(stream google.api.HttpBody)
// returns (stream google.api.HttpBody);
// rpc UpdateCalendar(stream google.api.HttpBody)
// returns (stream google.api.HttpBody);
// }
//
// Use of this type only changes how the request and response bodies are
// handled, all other features will continue to work unchanged.
message HttpBody {
// The HTTP Content-Type header value specifying the content type of the body.
string content_type = 1;
// The HTTP request/response body as raw binary.
bytes data = 2;
// Application specific response metadata. Must be set in the first response
// for streaming APIs.
repeated google.protobuf.Any extensions = 3;
}

View file

@ -1,186 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.rpc;
option go_package = "google.golang.org/genproto/googleapis/rpc/code;code";
option java_multiple_files = true;
option java_outer_classname = "CodeProto";
option java_package = "com.google.rpc";
option objc_class_prefix = "RPC";
// The canonical error codes for Google APIs.
//
//
// Sometimes multiple error codes may apply. Services should return
// the most specific error code that applies. For example, prefer
// `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply.
// Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`.
enum Code {
// Not an error; returned on success
//
// HTTP Mapping: 200 OK
OK = 0;
// The operation was cancelled, typically by the caller.
//
// HTTP Mapping: 499 Client Closed Request
CANCELLED = 1;
// Unknown error. For example, this error may be returned when
// a `Status` value received from another address space belongs to
// an error space that is not known in this address space. Also
// errors raised by APIs that do not return enough error information
// may be converted to this error.
//
// HTTP Mapping: 500 Internal Server Error
UNKNOWN = 2;
// The client specified an invalid argument. Note that this differs
// from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments
// that are problematic regardless of the state of the system
// (e.g., a malformed file name).
//
// HTTP Mapping: 400 Bad Request
INVALID_ARGUMENT = 3;
// The deadline expired before the operation could complete. For operations
// that change the state of the system, this error may be returned
// even if the operation has completed successfully. For example, a
// successful response from a server could have been delayed long
// enough for the deadline to expire.
//
// HTTP Mapping: 504 Gateway Timeout
DEADLINE_EXCEEDED = 4;
// Some requested entity (e.g., file or directory) was not found.
//
// Note to server developers: if a request is denied for an entire class
// of users, such as gradual feature rollout or undocumented whitelist,
// `NOT_FOUND` may be used. If a request is denied for some users within
// a class of users, such as user-based access control, `PERMISSION_DENIED`
// must be used.
//
// HTTP Mapping: 404 Not Found
NOT_FOUND = 5;
// The entity that a client attempted to create (e.g., file or directory)
// already exists.
//
// HTTP Mapping: 409 Conflict
ALREADY_EXISTS = 6;
// The caller does not have permission to execute the specified
// operation. `PERMISSION_DENIED` must not be used for rejections
// caused by exhausting some resource (use `RESOURCE_EXHAUSTED`
// instead for those errors). `PERMISSION_DENIED` must not be
// used if the caller can not be identified (use `UNAUTHENTICATED`
// instead for those errors). This error code does not imply the
// request is valid or the requested entity exists or satisfies
// other pre-conditions.
//
// HTTP Mapping: 403 Forbidden
PERMISSION_DENIED = 7;
// The request does not have valid authentication credentials for the
// operation.
//
// HTTP Mapping: 401 Unauthorized
UNAUTHENTICATED = 16;
// Some resource has been exhausted, perhaps a per-user quota, or
// perhaps the entire file system is out of space.
//
// HTTP Mapping: 429 Too Many Requests
RESOURCE_EXHAUSTED = 8;
// The operation was rejected because the system is not in a state
// required for the operation's execution. For example, the directory
// to be deleted is non-empty, an rmdir operation is applied to
// a non-directory, etc.
//
// Service implementors can use the following guidelines to decide
// between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`:
// (a) Use `UNAVAILABLE` if the client can retry just the failing call.
// (b) Use `ABORTED` if the client should retry at a higher level
// (e.g., when a client-specified test-and-set fails, indicating the
// client should restart a read-modify-write sequence).
// (c) Use `FAILED_PRECONDITION` if the client should not retry until
// the system state has been explicitly fixed. E.g., if an "rmdir"
// fails because the directory is non-empty, `FAILED_PRECONDITION`
// should be returned since the client should not retry unless
// the files are deleted from the directory.
//
// HTTP Mapping: 400 Bad Request
FAILED_PRECONDITION = 9;
// The operation was aborted, typically due to a concurrency issue such as
// a sequencer check failure or transaction abort.
//
// See the guidelines above for deciding between `FAILED_PRECONDITION`,
// `ABORTED`, and `UNAVAILABLE`.
//
// HTTP Mapping: 409 Conflict
ABORTED = 10;
// The operation was attempted past the valid range. E.g., seeking or
// reading past end-of-file.
//
// Unlike `INVALID_ARGUMENT`, this error indicates a problem that may
// be fixed if the system state changes. For example, a 32-bit file
// system will generate `INVALID_ARGUMENT` if asked to read at an
// offset that is not in the range [0,2^32-1], but it will generate
// `OUT_OF_RANGE` if asked to read from an offset past the current
// file size.
//
// There is a fair bit of overlap between `FAILED_PRECONDITION` and
// `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific
// error) when it applies so that callers who are iterating through
// a space can easily look for an `OUT_OF_RANGE` error to detect when
// they are done.
//
// HTTP Mapping: 400 Bad Request
OUT_OF_RANGE = 11;
// The operation is not implemented or is not supported/enabled in this
// service.
//
// HTTP Mapping: 501 Not Implemented
UNIMPLEMENTED = 12;
// Internal errors. This means that some invariants expected by the
// underlying system have been broken. This error code is reserved
// for serious errors.
//
// HTTP Mapping: 500 Internal Server Error
INTERNAL = 13;
// The service is currently unavailable. This is most likely a
// transient condition, which can be corrected by retrying with
// a backoff.
//
// See the guidelines above for deciding between `FAILED_PRECONDITION`,
// `ABORTED`, and `UNAVAILABLE`.
//
// HTTP Mapping: 503 Service Unavailable
UNAVAILABLE = 14;
// Unrecoverable data loss or corruption.
//
// HTTP Mapping: 500 Internal Server Error
DATA_LOSS = 15;
}

View file

@ -1,200 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.rpc;
import "google/protobuf/duration.proto";
option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails";
option java_multiple_files = true;
option java_outer_classname = "ErrorDetailsProto";
option java_package = "com.google.rpc";
option objc_class_prefix = "RPC";
// Describes when the clients can retry a failed request. Clients could ignore
// the recommendation here or retry when this information is missing from error
// responses.
//
// It's always recommended that clients should use exponential backoff when
// retrying.
//
// Clients should wait until `retry_delay` amount of time has passed since
// receiving the error response before retrying. If retrying requests also
// fail, clients should use an exponential backoff scheme to gradually increase
// the delay between retries based on `retry_delay`, until either a maximum
// number of retires have been reached or a maximum retry delay cap has been
// reached.
message RetryInfo {
// Clients should wait at least this long between retrying the same request.
google.protobuf.Duration retry_delay = 1;
}
// Describes additional debugging info.
message DebugInfo {
// The stack trace entries indicating where the error occurred.
repeated string stack_entries = 1;
// Additional debugging information provided by the server.
string detail = 2;
}
// Describes how a quota check failed.
//
// For example if a daily limit was exceeded for the calling project,
// a service could respond with a QuotaFailure detail containing the project
// id and the description of the quota limit that was exceeded. If the
// calling project hasn't enabled the service in the developer console, then
// a service could respond with the project id and set `service_disabled`
// to true.
//
// Also see RetryDetail and Help types for other details about handling a
// quota failure.
message QuotaFailure {
// A message type used to describe a single quota violation. For example, a
// daily quota or a custom quota that was exceeded.
message Violation {
// The subject on which the quota check failed.
// For example, "clientip:<ip address of client>" or "project:<Google
// developer project id>".
string subject = 1;
// A description of how the quota check failed. Clients can use this
// description to find more about the quota configuration in the service's
// public documentation, or find the relevant quota limit to adjust through
// developer console.
//
// For example: "Service disabled" or "Daily Limit for read operations
// exceeded".
string description = 2;
}
// Describes all quota violations.
repeated Violation violations = 1;
}
// Describes what preconditions have failed.
//
// For example, if an RPC failed because it required the Terms of Service to be
// acknowledged, it could list the terms of service violation in the
// PreconditionFailure message.
message PreconditionFailure {
// A message type used to describe a single precondition failure.
message Violation {
// The type of PreconditionFailure. We recommend using a service-specific
// enum type to define the supported precondition violation types. For
// example, "TOS" for "Terms of Service violation".
string type = 1;
// The subject, relative to the type, that failed.
// For example, "google.com/cloud" relative to the "TOS" type would
// indicate which terms of service is being referenced.
string subject = 2;
// A description of how the precondition failed. Developers can use this
// description to understand how to fix the failure.
//
// For example: "Terms of service not accepted".
string description = 3;
}
// Describes all precondition violations.
repeated Violation violations = 1;
}
// Describes violations in a client request. This error type focuses on the
// syntactic aspects of the request.
message BadRequest {
// A message type used to describe a single bad request field.
message FieldViolation {
// A path leading to a field in the request body. The value will be a
// sequence of dot-separated identifiers that identify a protocol buffer
// field. E.g., "field_violations.field" would identify this field.
string field = 1;
// A description of why the request element is bad.
string description = 2;
}
// Describes all violations in a client request.
repeated FieldViolation field_violations = 1;
}
// Contains metadata about the request that clients can attach when filing a bug
// or providing other forms of feedback.
message RequestInfo {
// An opaque string that should only be interpreted by the service generating
// it. For example, it can be used to identify requests in the service's logs.
string request_id = 1;
// Any data that was used to serve this request. For example, an encrypted
// stack trace that can be sent back to the service provider for debugging.
string serving_data = 2;
}
// Describes the resource that is being accessed.
message ResourceInfo {
// A name for the type of resource being accessed, e.g. "sql table",
// "cloud storage bucket", "file", "Google calendar"; or the type URL
// of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic".
string resource_type = 1;
// The name of the resource being accessed. For example, a shared calendar
// name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current
// error is [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED].
string resource_name = 2;
// The owner of the resource (optional).
// For example, "user:<owner email>" or "project:<Google developer project
// id>".
string owner = 3;
// Describes what error is encountered when accessing this resource.
// For example, updating a cloud project may require the `writer` permission
// on the developer console project.
string description = 4;
}
// Provides links to documentation or for performing an out of band action.
//
// For example, if a quota check failed with an error indicating the calling
// project hasn't enabled the accessed service, this can contain a URL pointing
// directly to the right place in the developer console to flip the bit.
message Help {
// Describes a URL link.
message Link {
// Describes what the link offers.
string description = 1;
// The URL of the link.
string url = 2;
}
// URL(s) pointing to additional information on handling the current error.
repeated Link links = 1;
}
// Provides a localized error message that is safe to return to the user
// which can be attached to an RPC error.
message LocalizedMessage {
// The locale used following the specification defined at
// http://www.rfc-editor.org/rfc/bcp/bcp47.txt.
// Examples are: "en-US", "fr-CH", "es-MX"
string locale = 1;
// The localized error message in the above locale.
string message = 2;
}

View file

@ -1,92 +0,0 @@
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.rpc;
import "google/protobuf/any.proto";
option go_package = "google.golang.org/genproto/googleapis/rpc/status;status";
option java_multiple_files = true;
option java_outer_classname = "StatusProto";
option java_package = "com.google.rpc";
option objc_class_prefix = "RPC";
// The `Status` type defines a logical error model that is suitable for different
// programming environments, including REST APIs and RPC APIs. It is used by
// [gRPC](https://github.com/grpc). The error model is designed to be:
//
// - Simple to use and understand for most users
// - Flexible enough to meet unexpected needs
//
// # Overview
//
// The `Status` message contains three pieces of data: error code, error message,
// and error details. The error code should be an enum value of
// [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The
// error message should be a developer-facing English message that helps
// developers *understand* and *resolve* the error. If a localized user-facing
// error message is needed, put the localized message in the error details or
// localize it in the client. The optional error details may contain arbitrary
// information about the error. There is a predefined set of error detail types
// in the package `google.rpc` that can be used for common error conditions.
//
// # Language mapping
//
// The `Status` message is the logical representation of the error model, but it
// is not necessarily the actual wire format. When the `Status` message is
// exposed in different client libraries and different wire protocols, it can be
// mapped differently. For example, it will likely be mapped to some exceptions
// in Java, but more likely mapped to some error codes in C.
//
// # Other uses
//
// The error model and the `Status` message can be used in a variety of
// environments, either with or without APIs, to provide a
// consistent developer experience across different environments.
//
// Example uses of this error model include:
//
// - Partial errors. If a service needs to return partial errors to the client,
// it may embed the `Status` in the normal response to indicate the partial
// errors.
//
// - Workflow errors. A typical workflow has multiple steps. Each step may
// have a `Status` message for error reporting.
//
// - Batch operations. If a client uses batch request and batch response, the
// `Status` message should be used directly inside batch response, one for
// each error sub-response.
//
// - Asynchronous operations. If an API call embeds asynchronous operation
// results in its response, the status of those operations should be
// represented directly using the `Status` message.
//
// - Logging. If some API errors are stored in logs, the message `Status` could
// be used directly after any stripping needed for security/privacy reasons.
message Status {
// The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].
int32 code = 1;
// A developer-facing error message, which should be in English. Any
// user-facing error message should be localized and sent in the
// [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.
string message = 2;
// A list of messages that carry the error details. There is a common set of
// message types for APIs to use.
repeated google.protobuf.Any details = 3;
}

View file

@ -1,150 +0,0 @@
syntax = "proto3";
// Rove
//
// Rove is an asychronous nomadic game about exploring a planet as part of a loose community
package rove;
option go_package = "github.com/mdiluz/rove/pkg/rove";
import "google/api/annotations.proto";
service Rove {
// Server status
//
// Responds with various details about the current server status
rpc Status(StatusRequest) returns (StatusResponse) {
option (google.api.http) = {
get: "/status"
};
}
// Register an account
//
// Tries to register an account with the given name
rpc Register(RegisterRequest) returns (RegisterResponse) {
option (google.api.http) = {
post: "/register"
body: "*"
};
}
// Send commands to rover
//
// Sending commands to this endpoint will queue them to be executed during the following ticks, in the order sent
rpc Commands(CommandsRequest) returns (CommandsResponse) {
option (google.api.http) = {
post: "/commands"
body: "*"
};
}
// Get radar information
//
// Gets the radar output for the given rover
rpc Radar(RadarRequest) returns (RadarResponse) {
option (google.api.http) = {
post: "/radar"
body: "*"
};
}
// Get rover information
//
// Gets information for the account's rover
rpc Rover(RoverRequest) returns (RoverResponse) {
option (google.api.http) = {
post: "/rover"
body: "*"
};
}
}
message Command {
// The command to execute, currently only accepts move, which requires a bearing and a duration.
string command = 1;
// The bearing, example: NE
string bearing = 2;
// The duration in ticks, example: 5
int32 duration = 3;
}
message CommandsRequest {
// The account to execture these commands
string account = 1;
// The set of desired commands
repeated Command commands = 2;
}
// Empty placeholder
message CommandsResponse {}
message Error {
// An explanation for the HTTP error returned
string error = 1;
}
message RadarRequest {
// The account for this request
string account = 1;
}
message RadarResponse {
// The range in tiles from the rover of the radar data
int32 range = 1;
// A 1D array representing range*2 + 1 squared set of tiles, origin bottom left and in row->column order
bytes tiles = 2;
}
// Empty placeholder
message RegisterResponse{}
message RegisterRequest {
// The desired account name
string name = 1;
}
message RoverRequest {
// The account for this request
string account = 1;
}
message RoverResponse {
// The name of the rover
string name = 1;
// Position of the rover in world coordinates
Vector position = 2;
// The range of this rover's radar
int32 range = 3;
// The speed the rover can move per tick
int32 speed = 4;
}
// Empty placeholder
message StatusRequest {}
message StatusResponse {
// The time the next tick will occur
string next_tick = 1;
// Whether the server is ready to accept requests
bool ready = 2;
// The tick rate of the server in minutes (how many minutes per tick)
int32 tick = 3;
// The version of the server in v{major}.{minor}-{delta}-{sha} form
string version = 4;
}
message Vector {
int32 x = 1;
int32 y = 2;
}

2202
proto/roveapi/roveapi.pb.go Normal file

File diff suppressed because it is too large Load diff

322
proto/roveapi/roveapi.proto Normal file
View file

@ -0,0 +1,322 @@
syntax = "proto3";
// Rove
//
// Rove is an asychronous nomadic game about exploring a planet as part of a
// loose community
package roveapi;
option go_package = "github.com/mdiluz/rove/proto/roveapi";
// The Rove server hosts a single game session and world with multiple players
service Rove {
// Server status
// Responds with various details about the current server status
rpc ServerStatus(ServerStatusRequest) returns (ServerStatusResponse) {}
// Register an account
// Tries to register an account with the given name
rpc Register(RegisterRequest) returns (RegisterResponse) {}
// Send commands to rover
// Sending commands to this endpoint will queue them to be executed during the
// following ticks, in the order sent. Commands sent within the same tick will
// overwrite until the tick has finished and the commands are queued
rpc Command(CommandRequest) returns (CommandResponse) {}
// Get radar information
// Gets the radar output for the given rover
rpc Radar(RadarRequest) returns (RadarResponse) {}
// Get rover information
// Gets information for the account's rover
rpc Status(StatusRequest) returns (StatusResponse) {}
}
//
// ServerStatus
//
// ServerStatusRequest is an empty placeholder
message ServerStatusRequest {}
// ServerStatusResponse is a response with useful server information
message ServerStatusResponse {
// The version of the server in v{major}.{minor}-{delta}-{sha} form
string version = 1;
// Whether the server is ready to accept requests
bool ready = 2;
// The tick rate of the server in minutes (how many minutes per tick)
int32 tickRate = 3;
// The current tick of the server
int32 currentTick = 4;
// The time the next tick will occur
string next_tick = 5;
}
//
// Register
//
// RegisterRequest contains data to register an account
message RegisterRequest {
// The desired account name
string name = 1;
}
// Account describes a registered account
message Account {
// The account name
string name = 1;
// The account secret value, given when creating the account
string secret = 2;
}
// RegisterResponse is the response given to registering an account
message RegisterResponse {
// The registered account information
Account account = 1;
}
//
// Command
//
// CommandType defines the type of a command to give to the rover
enum CommandType {
none = 0;
// Waits before performing the next command
wait = 1;
// Toggles the sails, either catching the wind, or charging from the sun
toggle = 2;
// Turns the rover in the specified bearing (requires bearing)
turn = 3;
// Stashes item at current location in rover inventory
stash = 4;
// Repairs the rover using an inventory object
repair = 5;
// Broadcasts a message to nearby rovers (requires data)
broadcast = 6;
// Salvages a neighboring dormant rover for parts
salvage = 7;
// Transfers remote control into dormant rover
transfer = 8;
// Upgrades a chosen rover specification using 5 rover parts
upgrade = 9;
}
// Bearing represents a compass direction
enum Bearing {
// BearingUnknown an unknown invalid bearing
BearingUnknown = 0;
North = 1;
NorthEast = 2;
East = 3;
SouthEast = 4;
South = 5;
SouthWest = 6;
West = 7;
NorthWest = 8;
}
// Describes the type of upgrade
enum RoverUpgrade {
RoverUpgradeUnknown = 0;
Range = 1;
Capacity = 2;
MaximumIntegrity = 3;
MaximumCharge = 4;
}
// Command is a single command for a rover
message Command {
// The command type
CommandType command = 1;
// The number of times to repeat the command after the first
int32 repeat = 2;
// broadcast - a simple message, must be composed of up to 3 printable ASCII
// glyphs (32-126)
bytes data = 3;
// move - the bearing for the rover to turn to
Bearing bearing = 4;
// upgrade - the upgrade to apply to the rover
RoverUpgrade upgrade = 5;
}
// CommandRequest describes a set of commands to be requested for the rover
message CommandRequest {
// The account to execute these commands
Account account = 1;
// The set of desired commands
repeated Command commands = 2;
}
// CommandResponse is an empty placeholder
message CommandResponse {}
//
// Radar
//
// Types of objects
enum Object {
// ObjectUnknown represents no object at all
ObjectUnknown = 0;
// RoverLive represents a live rover
RoverLive = 1;
// RoverDormant describes a dormant rover
RoverDormant = 2;
// RockSmall is a small stashable rock
RockSmall = 3;
// RockLarge is a large blocking rock
RockLarge = 4;
// RoverParts is one unit of rover parts, used for repairing and fixing the
// rover
RoverParts = 5;
}
enum Tile {
// TileUnknown is a keyword for nothing
TileUnknown = 0;
// Rock is solid rock ground
Rock = 1;
// Gravel is loose rocks
Gravel = 2;
// Sand is sand
Sand = 3;
}
// RadarRequest is the data needed to request the radar for a rover
message RadarRequest {
// The account for this request
Account account = 1;
}
// RadarResponse describes radar information
message RadarResponse {
// The range in tiles from the rover of the radar data
int32 range = 1;
// A 1D array representing range*2 + 1 squared set of tiles, origin bottom
// left and in row->column order
repeated Tile tiles = 2;
// A similar array to the tile array, but containing objects
repeated Object objects = 3;
}
//
// Status
//
// StatusRequest is information needed to request rover status
message StatusRequest {
// The account for this request
Account account = 1;
}
// Log is a single log item
message Log {
// The unix timestamp of the log
string time = 1;
// The text of the log
string text = 2;
}
// Vector describes a point or vector in 2D space
message Vector {
int32 x = 1;
int32 y = 2;
}
// SailPosition represents the position of the sola sail
enum SailPosition {
UnknownSailPosition = 0;
// CatchingWind means the sail is catching the wind and moving the rover
CatchingWind = 1;
// SolarCharging means the sail is facing the sun and charging
SolarCharging = 2;
}
message RoverSpecifications {
// The name of the rover
string name = 1;
// The range of this rover's radar and broadcasting
int32 range = 2;
// The capacity of the inventory
int32 capacity = 3;
// The maximum health of the rover
int32 maximumIntegrity = 4;
// The max energy the rover can store
int32 maximumCharge = 5;
}
message RoverStatus {
// The current direction of the rover
Bearing bearing = 1;
// The current position of the sails
SailPosition sailPosition = 2;
// The items in the rover inventory
bytes inventory = 3;
// The current health of the rover
int32 integrity = 4;
// The energy stored in the rover
int32 charge = 5;
// The set of currently queued commands
repeated Command queuedCommands = 6;
}
message RoverReadings {
// Position of the rover in world coordinates
Vector position = 1;
// The current wind direction
Bearing wind = 2;
// The most recent logs
repeated Log logs = 3;
}
// StatusResponse is the response given to a status request
message StatusResponse {
// The static rover information
RoverSpecifications spec = 1;
// Current rover status
RoverStatus status = 2;
// Current rover readings
RoverReadings readings = 3;
}

View file

@ -17,7 +17,8 @@ apps:
plugs:
- network
environment:
USER_DATA: $SNAP_USER_DATA
ROVE_USER_DATA: $SNAP_USER_DATA
rove-server:
command: bin/rove-server
plugs:
@ -26,21 +27,7 @@ apps:
environment:
WORDS_FILE : "$SNAP/data/words_alpha.txt"
DATA_PATH : $SNAP_USER_DATA
rove-accountant:
command: bin/rove-accountant
plugs:
- network
- network-bind
environment:
DATA_PATH : $SNAP_USER_DATA
rove-rest-server:
command: bin/rove-reverse-proxy
plugs:
- network
- network-bind
environment:
DATA_PATH : $SNAP_USER_DATA
parts:
go-rove:
plugin: go