mirror of
https://github.com/googleforgames/open-match.git
synced 2025-04-04 12:09:58 +00:00
Compare commits
214 Commits
calebatwd/
...
v0.7.0
Author | SHA1 | Date | |
---|---|---|---|
4f521b41db | |||
3c6183241e | |||
61449fe2cf | |||
21cf0697fe | |||
12e5a37816 | |||
e658cc0d84 | |||
9e89735d79 | |||
5cbbfef1cc | |||
1c0e4ff94e | |||
79862c9950 | |||
8ac27d7975 | |||
86b8cb5aa8 | |||
fdea3c8f1e | |||
61a28df3e5 | |||
13fe3fe5a9 | |||
a674fb1c02 | |||
75ffc83b98 | |||
7dc4de6a14 | |||
f02283e2a6 | |||
d1fe7f1ac4 | |||
84eb9b27ef | |||
707de22912 | |||
780e3abf10 | |||
524b7d333f | |||
c544b9a239 | |||
04b6f1a5ad | |||
13952ea54e | |||
a61f4a643e | |||
949fa28505 | |||
85cc481f5d | |||
c3cbcd7625 | |||
e01fc12549 | |||
e1682100fa | |||
603aef207f | |||
baf403ac44 | |||
b1da77eaba | |||
bb82a397d2 | |||
abd2c1434c | |||
bc9dc27210 | |||
084461d387 | |||
bc7d014db6 | |||
230ae76bb4 | |||
ebbe5aa6ce | |||
9b350c690c | |||
80b817f488 | |||
df7021de1b | |||
5c8f218000 | |||
3f538df971 | |||
1e856658c9 | |||
eb6697052d | |||
31d3464a31 | |||
c96b65d52b | |||
9d601351cc | |||
7272ca8b93 | |||
b463d2e0fd | |||
07da543f8e | |||
0d54c39828 | |||
5469c8bc69 | |||
c837211cd1 | |||
5729e72214 | |||
66910632da | |||
c832074112 | |||
a6d526b36b | |||
13e017ba65 | |||
3784300d22 | |||
31fd18e39b | |||
a54d1fcf21 | |||
72a435758e | |||
6848fa71c2 | |||
987d90cc44 | |||
baf943fdd3 | |||
c7ce1b047b | |||
36a194e761 | |||
605511d177 | |||
2a08732508 | |||
e1c2b96cb5 | |||
1bd84355b7 | |||
a9014fbf78 | |||
8050c61618 | |||
8b765871c4 | |||
88786ecbd1 | |||
f41c175f29 | |||
3607809371 | |||
d21ae712a7 | |||
f3f1908318 | |||
9cb4a9ce6e | |||
8dad7fd7d0 | |||
f70cfee14a | |||
36f92b4336 | |||
164dfdde67 | |||
c0d6531f3f | |||
52610974de | |||
041572eef6 | |||
e28fe42f3b | |||
880e340859 | |||
88a659544e | |||
9381918163 | |||
1c41052bd6 | |||
819ae18478 | |||
ad96f42b94 | |||
1d778c079c | |||
4bbfafd761 | |||
28e5d0a1d1 | |||
a394c8b22e | |||
93276f4d02 | |||
310d98a078 | |||
a84eda4dab | |||
ce038bc6dd | |||
74fb195f41 | |||
1dc3fc8b6b | |||
de469cb349 | |||
7462f32125 | |||
3268461a21 | |||
3897cd295e | |||
5a9212a46e | |||
04cfddefd0 | |||
b7872489ae | |||
6d65841b77 | |||
b4fb725008 | |||
50d9a0c234 | |||
a68fd5ed1e | |||
be58fae864 | |||
f22ad9afc5 | |||
6b1b84c54e | |||
043ffd69e3 | |||
e5f7d3bafe | |||
8f88ba151e | |||
9c83062a41 | |||
7b31bdcedf | |||
269e6cd0ad | |||
864f13f2e8 | |||
e3a9f59ad9 | |||
8a3f6e43b8 | |||
2b8597c72e | |||
c403f28c04 | |||
317f914daa | |||
16fbc015b2 | |||
d1d9114ddb | |||
e6622ff585 | |||
99fb4a8fcf | |||
e0ebb139bf | |||
31dcbe39f7 | |||
76a1cd8427 | |||
ac6c00c89d | |||
a7d97fdf0d | |||
f08121cf25 | |||
cd6dd410ee | |||
5f0a2409e8 | |||
d445a0b2d5 | |||
1526827e3c | |||
82e60e861f | |||
5900a1c542 | |||
a02aa99c7a | |||
2f3f8b7f56 | |||
a7eb1719cc | |||
ea24b702c8 | |||
e7ab30dc63 | |||
8b88f26e4e | |||
d5f60ae202 | |||
113ee00a6c | |||
c083f1735a | |||
52ad8de602 | |||
3daebfc39d | |||
3e5da9f7d5 | |||
951e82b6a2 | |||
d201242610 | |||
1328a109e5 | |||
2415194e68 | |||
b2214f7b9b | |||
98220fdc0b | |||
b2bf00631a | |||
49ac68c32a | |||
7b3d6d38d3 | |||
a1271ff820 | |||
2932144d80 | |||
3a14bf3641 | |||
7d0ec363e5 | |||
dcff6326b1 | |||
ffd77212b0 | |||
ea3e529b0d | |||
db2c298a48 | |||
85d5f9fdbb | |||
401329030a | |||
d9e20f9c29 | |||
f95164148f | |||
ab39bcc93d | |||
d1ae3e9620 | |||
de83c9f06a | |||
9fb445fda6 | |||
050367eb88 | |||
40d288964b | |||
e4c87c2c3a | |||
bd2927bcc5 | |||
271e745a61 | |||
98c15e78ad | |||
fbbe3cd2b4 | |||
878ef89c40 | |||
a9a5a29e58 | |||
8cd7cd0035 | |||
92495071d2 | |||
a766b38d62 | |||
6c941909e8 | |||
77dc8f8c47 | |||
a804e1009b | |||
3b2efc39c7 | |||
b8054633bf | |||
336fad9079 | |||
ce59eedd29 | |||
83c0913c34 | |||
6b50cdd804 | |||
f427303505 | |||
269dd9bc2f | |||
d501dbcde6 | |||
04c4e376b5 |
@ -12,10 +12,13 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
.git
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.nupkg
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
@ -101,14 +104,28 @@ install/yaml/
|
||||
# Temp Directories
|
||||
tmp/
|
||||
|
||||
# Terraform context
|
||||
.terraform
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
|
||||
# Credential Files
|
||||
creds.json
|
||||
|
||||
# Open Match Binaries
|
||||
cmd/backend/backend
|
||||
cmd/frontend/frontend
|
||||
cmd/mmlogic/mmlogic
|
||||
cmd/evaluator/evaluator
|
||||
cmd/synchronizer/synchronizer
|
||||
cmd/minimatch/minimatch
|
||||
cmd/swaggerui/swaggerui
|
||||
tools/certgen/certgen
|
||||
examples/functions/golang/simple/simple
|
||||
examples/demo/demo
|
||||
examples/functions/golang/soloduel/soloduel
|
||||
examples/functions/golang/rosterbased/rosterbased
|
||||
examples/functions/golang/pool/pool
|
||||
examples/evaluator/golang/simple/simple
|
||||
tools/reaper/reaper
|
||||
|
||||
# Open Match Build Directory
|
||||
build/
|
||||
|
25
.github/ISSUE_TEMPLATE/apichange.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/apichange.md
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Breaking API change
|
||||
about: Details of a breaking API change proposal.
|
||||
title: 'API change: <>'
|
||||
labels: breaking api change
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<High level description of this change>
|
||||
|
||||
## Motivation
|
||||
|
||||
<What is the primary motivation for this API change>
|
||||
|
||||
## Impact
|
||||
|
||||
<What usage does this impact? Add details here such that a consumer of Open
|
||||
Match API can clearly tell if this will impact them>
|
||||
|
||||
## Change Proto
|
||||
|
||||
<Add snippet of the proposed change proto>
|
||||
|
83
.github/ISSUE_TEMPLATE/release.md
vendored
83
.github/ISSUE_TEMPLATE/release.md
vendored
@ -8,88 +8,98 @@ assignees: ''
|
||||
|
||||
# Open Match Release Process
|
||||
|
||||
Follow these instructions to create an Open Match release. The output of the
|
||||
Follow these instructions to create an Open Match release. The output of the
|
||||
release process is new images and new configuration.
|
||||
|
||||
## Getting setup
|
||||
|
||||
*note: the commands below are pasted from the 0.5 release. make the necessary
|
||||
changes to match your naming & environment.*
|
||||
**NOTE: The instructions below are NOT strictly copy-pastable and assume 0.5**
|
||||
**release. Please update the version number for your commands.**
|
||||
|
||||
The Git flow for pushing a new release is similar to the development process
|
||||
but there are some small differences.
|
||||
|
||||
**1. Clone your fork of the Open Match repository.**
|
||||
### 1. Clone Repository
|
||||
|
||||
```shell
|
||||
# Clone your fork of the Open Match repository.
|
||||
git clone git@github.com:afeddersen/open-match.git
|
||||
```
|
||||
**2. Move into the new open-match directory.**
|
||||
|
||||
```shell
|
||||
# Change directory to the git repository.
|
||||
cd open-match
|
||||
```
|
||||
|
||||
**3. Configure a remote that points to the upstream repository. This is required to sync changes you make in a fork with the original repository. Note: Upstream is the gatekeeper of the project or the source of truth to which you wish to contribute.**
|
||||
|
||||
```shell
|
||||
# Add a remote, you'll be pushing to this.
|
||||
git remote add upstream https://github.com/googleforgames/open-match.git
|
||||
```
|
||||
|
||||
**3. Fetch the branches and their respective commits from the upstream repo.**
|
||||
### 2. Release Branch
|
||||
|
||||
If you're creating the first release of the version, that would be `0.5.0-rc.1`
|
||||
then you'll need to create the release branch.
|
||||
|
||||
```shell
|
||||
git fetch upstream
|
||||
# Create a local release branch.
|
||||
git checkout -b release-0.5 upstream/master
|
||||
# Push the branch upstream.
|
||||
git push upstream release-0.5
|
||||
```
|
||||
|
||||
**4. Create a local release branch that tracks upstream and check it out.**
|
||||
otherwise there should already be a `release-0.5` branch so run,
|
||||
|
||||
```shell
|
||||
# Checkout the release branch.
|
||||
git checkout -b release-0.5 upstream/release-0.5
|
||||
```
|
||||
|
||||
**NOTE: The branch name must be in the format, `release-X.Y` otherwise**
|
||||
**some artifacts will not be pushed.**
|
||||
|
||||
## Releases & Versions
|
||||
|
||||
|
||||
Open Match uses Semantic Versioning 2.0.0. If you're not familiar please
|
||||
Open Match uses Semantic Versioning 2.0.0. If you're not familiar please
|
||||
see the documentation - [https://semver.org/](https://semver.org/).
|
||||
|
||||
Full Release / Stable Release:
|
||||
|
||||
* The final software product. Stable, reliable, etc...
|
||||
* Naming example: 1.0.0
|
||||
* The final software product. Stable, reliable, etc...
|
||||
* Example: 1.0.0, 1.1.0
|
||||
|
||||
Release Candidate (RC):
|
||||
|
||||
* A release candidate (RC) is a version with the potential to be the final
|
||||
product but it hasn't validated by automated and/or manual tests.
|
||||
* Naming example: 1.0.0-rc.1
|
||||
* Example: 1.0.0-rc.1
|
||||
|
||||
Hot Fixes:
|
||||
|
||||
* Code developed to correct a major software bug or fault
|
||||
that's been discovered after the full release.
|
||||
* Naming example: 1.0.1
|
||||
* Example: 1.0.1
|
||||
|
||||
Preview:
|
||||
|
||||
* Rare, a one off release cut from the master branch to provide early access
|
||||
to APIs or some other major change.
|
||||
* **NOTE: There's no branch for this release.**
|
||||
* Example: 0.5-preview.1
|
||||
|
||||
**NOTE: Semantic versioning is enforced by `go mod`. A non-compliant version**
|
||||
**tag will cause `go get` to break for users.**
|
||||
|
||||
# Detailed Instructions
|
||||
|
||||
|
||||
## Find and replace
|
||||
|
||||
|
||||
Below this point you will see {version} used as a placeholder for future
|
||||
releases. Find {version} and replace with the current release (e.g. 0.5.0)
|
||||
releases. Find {version} and replace with the current release (e.g. 0.5.0)
|
||||
|
||||
## Create a release branch in the upstream repository
|
||||
|
||||
|
||||
**Note: This step is performed by the person who starts the release. It is
|
||||
only required once.**
|
||||
|
||||
- [ ] Create the branch in the **upstream** repository. It should be named
|
||||
release-X.Y. Example: release-0.5. At this point there's effectively a code
|
||||
freeze for this version and all work on master will be included in a future
|
||||
version. If you're on the branch that you created in the *getting setup*
|
||||
version. If you're on the branch that you created in the *getting setup*
|
||||
section above you should be able to push upstream.
|
||||
|
||||
```shell
|
||||
@ -98,19 +108,18 @@ git push origin release-0.5
|
||||
|
||||
- [ ] Announce a PR freeze on release-X.Y branch on [open-match-discuss@](mailing-list-post).
|
||||
- [ ] Open the [`Makefile`](makefile-version) and change BASE_VERSION entry.
|
||||
- [ ] Open the [`install/helm/open-match/Chart.yaml`](om-chart-yaml-version) and [`install/helm/open-match-example/Chart.yaml`](om-example-chart-yaml-version) and change the `appVersion` and `version` entries.
|
||||
- [ ] Open the [`install/helm/open-match/values.yaml`](om-values-yaml-version) and [`install/helm/open-match-example/values.yaml`](om-example-values-yaml-version) and change the `tag` entries.
|
||||
- [ ] Open the [`site/config.toml`] and change the `release_branch` and `release_version` entries.
|
||||
- [ ] Open the [`install/helm/open-match/Chart.yaml`](om-chart-yaml-version) and change the `appVersion` and `version` entries.
|
||||
- [ ] Open the [`install/helm/open-match/values.yaml`](om-values-yaml-version) and change the `tag` entries.
|
||||
- [ ] Open the [`cloudbuild.yaml`] and change the `_OM_VERSION` entry.
|
||||
- [ ] Run `make clean release`
|
||||
- [ ] There might be additional references to the old version but be careful not to change it for places that have it for historical purposes.
|
||||
- [ ] Run `make release`
|
||||
- [ ] Create a PR with the changes and include the release candidate name.
|
||||
- [ ] Go to [open-match-build](https://pantheon.corp.google.com/cloud-build/triggers?project=open-match-build) and update all the triggers' `_GCB_LATEST_VERSION` value to the `X.Y` of the release. This value should only increase as it's used to determine the latest stable version.
|
||||
- [ ] Merge your changes once the PR is approved.
|
||||
|
||||
## Complete Milestone
|
||||
|
||||
|
||||
**Note: This step is performed by the person who starts the release. It is
|
||||
**Note: This step is performed by the person who starts the release. It is
|
||||
only required once.**
|
||||
- [ ] Create the next [version milestone](https://github.com/googleforgames/open-match/milestones) and use [semantic versioning](https://semver.org/) when naming it to be consistent with the [Go community](https://blog.golang.org/versioning-proposal).
|
||||
- [ ] Create a *draft* [release](https://github.com/googleforgames/open-match/releases).
|
||||
@ -138,8 +147,6 @@ TODO: Add guidelines for labeling issues.
|
||||
- [ ] Run `./docs/governance/templates/release.sh {source version tag} {version}` to copy the images to open-match-public-images.
|
||||
- [ ] If this is a new minor version in the newest major version then run `./docs/governance/templates/release.sh {source version tag} latest`.
|
||||
- [ ] Copy the files from `build/release/` generated from `make release` to the release draft you created. You can drag and drop the files using the Github UI.
|
||||
- [ ] Run `make REGISTRY=gcr.io/open-match-public-images TAG={version} delete-gke-cluster create-gke-cluster push-helm sleep-10 install-chart install-demo-chart` and verify that the demo runs correctly.
|
||||
- [ ] Run `make delete-gke-cluster create-gke-cluster` and run through the instructions under the [README](readme-deploy), verify the pods are healthy. You'll need to adjust the path to the `build/release/install.yaml` and `build/release/install-demo.yaml` in your local clone since you haven't published them yet.
|
||||
- [ ] Open the [`README.md`](readme-deploy) update the version references and submit. (Release candidates can ignore this step.)
|
||||
- [ ] Publish the [Release](om-release) in Github.
|
||||
|
||||
@ -152,9 +159,7 @@ TODO: Add guidelines for labeling issues.
|
||||
[mailing-list-post]: https://groups.google.com/forum/#!newtopic/open-match-discuss
|
||||
[release-template]: https://github.com/googleforgames/open-match/blob/master/docs/governance/templates/release.md
|
||||
[makefile-version]: https://github.com/googleforgames/open-match/blob/master/Makefile#L53
|
||||
[om-example-chart-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match/Chart.yaml#L16
|
||||
[om-example-values-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match/values.yaml#L16
|
||||
[om-example-chart-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match-example/Chart.yaml#L16
|
||||
[om-example-values-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match-example/values.yaml#L16
|
||||
[om-chart-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match/Chart.yaml#L16
|
||||
[om-values-yaml-version]: https://github.com/googleforgames/open-match/blob/master/install/helm/open-match/values.yaml#L16
|
||||
[om-release]: https://github.com/googleforgames/open-match/releases/new
|
||||
[readme-deploy]: https://github.com/googleforgames/open-match/blob/master/README.md#deploy-to-kubernetes
|
||||
|
18
.gitignore
vendored
18
.gitignore
vendored
@ -16,6 +16,7 @@
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.nupkg
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
@ -101,14 +102,27 @@ install/yaml/
|
||||
# Temp Directories
|
||||
tmp/
|
||||
|
||||
# Terraform context
|
||||
.terraform
|
||||
*.tfstate.backup
|
||||
|
||||
# Credential Files
|
||||
creds.json
|
||||
|
||||
# Open Match Binaries
|
||||
cmd/backend/backend
|
||||
cmd/frontend/frontend
|
||||
cmd/mmlogic/mmlogic
|
||||
cmd/evaluator/evaluator
|
||||
cmd/synchronizer/synchronizer
|
||||
cmd/minimatch/minimatch
|
||||
cmd/swaggerui/swaggerui
|
||||
tools/certgen/certgen
|
||||
examples/functions/golang/simple/simple
|
||||
examples/demo/demo
|
||||
examples/functions/golang/soloduel/soloduel
|
||||
examples/functions/golang/rosterbased/rosterbased
|
||||
examples/functions/golang/pool/pool
|
||||
examples/evaluator/golang/simple/simple
|
||||
tools/reaper/reaper
|
||||
|
||||
# Secrets Directories
|
||||
install/helm/open-match/secrets/
|
||||
|
@ -45,7 +45,7 @@ run:
|
||||
# won't be reported. Default value is empty list, but there is
|
||||
# no need to include all autogenerated files, we confidently recognize
|
||||
# autogenerated files. If it's not please let us know.
|
||||
skip-files:
|
||||
skip-files: '.*\.gw\.go'
|
||||
|
||||
# output configuration options
|
||||
output:
|
||||
@ -71,7 +71,7 @@ linters-settings:
|
||||
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: false
|
||||
check-shadowing: true
|
||||
|
||||
# settings per analyzer
|
||||
settings:
|
||||
@ -177,6 +177,7 @@ linters:
|
||||
- prealloc
|
||||
- gofmt
|
||||
- interfacer # deprecated - "A tool that suggests interfaces is prone to bad suggestions"
|
||||
- gochecknoglobals
|
||||
|
||||
#linters:
|
||||
# enable-all: true
|
||||
@ -191,32 +192,11 @@ issues:
|
||||
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||
exclude-rules:
|
||||
- path: internal[/\\]config[/\\]
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
# Exclue some linters from running on test files
|
||||
# Exclude some linters from running on test files
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
# The following are allowed global variable patterns.
|
||||
# Generally it's ok to have constants or variables that effectively act as constants such as a static logger or flag values.
|
||||
# The filters below specify the source code pattern that's allowed when declaring a global
|
||||
# 'source: "flag."' will match 'var destFlag = flag.String("dest", "", "")'
|
||||
- source: "flag."
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- source: "View."
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- source: "tag."
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- source: "logrus."
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- source: "stats."
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- bodyclose
|
||||
|
||||
# Exclude known linters from partially hard-vendored code,
|
||||
# which is impossible to exclude via "nolint" comments.
|
||||
|
@ -1,22 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logic", "Logic\Logic.csproj", "{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{0687300A-A514-43FE-924C-29E9203EBD50}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1EF89BE7-709C-420A-9FDE-6AED2D0FBF2E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0687300A-A514-43FE-924C-29E9203EBD50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0687300A-A514-43FE-924C-29E9203EBD50}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0687300A-A514-43FE-924C-29E9203EBD50}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0687300A-A514-43FE-924C-29E9203EBD50}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
@ -1,11 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,32 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Data
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic range-based filter for querying data in ITicketData
|
||||
/// </summary>
|
||||
public class Filter
|
||||
{
|
||||
public Filter(string key, double min, double max)
|
||||
{
|
||||
Key = key;
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The attribute to query
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The minimum value [inclusive] of the range
|
||||
/// </summary>
|
||||
public double Min { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum value [exclusive] of the range
|
||||
/// </summary>
|
||||
public double Max { get; set; }
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Data
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for creating, deleting, querying, and updating ticket information
|
||||
/// </summary>
|
||||
public interface ITicketData
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks the provided ticket ids as ignorable in queries for a period of time
|
||||
/// </summary>
|
||||
/// <param name="ticketIds">The ticket ids to be ignored by queries</param>
|
||||
/// <param name="durationMs"></param>
|
||||
Task AwaitingAssignmentAsync(IEnumerable<Guid> ticketIds, long durationMs);
|
||||
|
||||
/// <summary>
|
||||
/// Populates the assignment field of the target tickets
|
||||
/// </summary>
|
||||
/// <param name="ticketIds">A list of ticket ids to be assigned</param>
|
||||
/// <param name="assignment">A string containing the assignment of the tickets</param>
|
||||
/// <returns></returns>
|
||||
Task AssignTicketsAsync(IEnumerable<Guid> ticketIds, string assignment);
|
||||
|
||||
/// <summary>
|
||||
/// Executes a collection of queries on the ticket data and returns the union of the results
|
||||
/// </summary>
|
||||
/// <param name="query">The query to execute</param>
|
||||
/// <returns>A collection of tickets union of the results</returns>
|
||||
Task<IEnumerable<Ticket>> QueryTicketsAsync(Query query);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ticket and adds it to the ticket datastore
|
||||
/// </summary>
|
||||
/// <param name="ticket">The ticket to create</param>
|
||||
/// <remarks>This may create indexes for the data in the ticket to make it queryable</remarks>
|
||||
Task CreateTicketAsync(Ticket ticket);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a ticket by id
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the ticket</param>
|
||||
/// <returns>The requested ticket if it exists</returns>
|
||||
Task<Ticket> GetTicketAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a ticket by id
|
||||
/// </summary>
|
||||
/// <param name="id">The id of the ticket</param>
|
||||
/// <remarks>This may delete indexes for the data in the ticket</remarks>
|
||||
Task DeleteTicketAsync(Guid id);
|
||||
}
|
||||
}
|
@ -1,202 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Data
|
||||
{
|
||||
public class MemoryData : ITicketData
|
||||
{
|
||||
private const string awaitingIndex = "awaitingAssignment";
|
||||
|
||||
private const string createdIndex = "created";
|
||||
|
||||
protected ConcurrentDictionary<Guid, Ticket> m_Tickets = new ConcurrentDictionary<Guid, Ticket>();
|
||||
|
||||
protected ConcurrentDictionary<string, SortedDictionary<Guid, double>> m_Indexes = new ConcurrentDictionary<string, SortedDictionary<Guid, double>>();
|
||||
|
||||
public Task AssignTicketsAsync(IEnumerable<Guid> ticketIds, string assignment)
|
||||
{
|
||||
foreach (var ticketId in ticketIds)
|
||||
{
|
||||
Ticket ticket = m_Tickets[ticketId];
|
||||
RemoveTicketIndexes(ticket);
|
||||
ticket.Assignment = assignment;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task AwaitingAssignmentAsync(IEnumerable<Guid> ticketIds, long durationMs)
|
||||
{
|
||||
long unixNowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
AddOrCreateIndex(ticketIds, new KeyValuePair<string, double>(awaitingIndex, unixNowMs + durationMs));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Ticket>> QueryTicketsAsync(Query query)
|
||||
{
|
||||
// Validate the query
|
||||
if (query.Filters == null || query.Filters.Count < 1) throw new ArgumentException("Must specify at least 1 filter");
|
||||
|
||||
// Get a copy of the indexes and tickets
|
||||
Dictionary<string, SortedDictionary<Guid, double>> indexes = new Dictionary<string, SortedDictionary<Guid, double>>();
|
||||
foreach (var keyValueIndex in m_Indexes)
|
||||
{
|
||||
lock (keyValueIndex.Value)
|
||||
{
|
||||
indexes.Add(keyValueIndex.Key, new SortedDictionary<Guid, double>(keyValueIndex.Value));
|
||||
}
|
||||
}
|
||||
|
||||
List<List<Guid>> hits = new List<List<Guid>>();
|
||||
foreach (var filter in query.Filters)
|
||||
{
|
||||
if (indexes.ContainsKey(filter.Key))
|
||||
{
|
||||
IEnumerable<Guid> hit = indexes[filter.Key]
|
||||
.Where(i => i.Value >= filter.Min && i.Value <= filter.Max)
|
||||
.Select(k => k.Key);
|
||||
hits.Add(hit.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
List<Guid> pool = hits.FirstOrDefault();
|
||||
if (pool == null)
|
||||
return Task.FromResult<IEnumerable<Ticket>>(new List<Ticket>());
|
||||
|
||||
for (int i = 1; i < hits.Count; i++)
|
||||
{
|
||||
pool = pool.Intersect(hits[i]).ToList();
|
||||
}
|
||||
|
||||
// Built-in ignore index
|
||||
if (indexes.ContainsKey(awaitingIndex))
|
||||
{
|
||||
long unixNowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
IEnumerable<Guid> ignore = indexes[awaitingIndex].Where(i => i.Value >= unixNowMs).Select(k => k.Key);
|
||||
pool = pool.Except(ignore).ToList(); // TODO: the list is ordered and except doesn't take advantage of that
|
||||
}
|
||||
|
||||
List<Ticket> ticketList = new List<Ticket>();
|
||||
foreach (var guid in pool)
|
||||
{
|
||||
ticketList.Add(m_Tickets[guid]);
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<Ticket>>(ticketList);
|
||||
}
|
||||
|
||||
public Task CreateTicketAsync(Ticket ticket)
|
||||
{
|
||||
// Validate the ticket
|
||||
if (ticket == null) throw new ArgumentNullException(nameof(ticket));
|
||||
if (ticket.Attributes == null) throw new ArgumentNullException(paramName: "ticketAttributes");
|
||||
if (ticket.Attributes.Count == 0) throw new ArgumentException("There must be at least 1 attribute to index", paramName: "ticketAttributes");
|
||||
|
||||
Ticket newTicket = new Ticket()
|
||||
{
|
||||
Id = ticket.Id,
|
||||
Attributes = new Dictionary<string, double>(ticket.Attributes),
|
||||
Properties = ticket.Properties,
|
||||
Created = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
Assignment = string.IsNullOrEmpty(ticket.Assignment) ? string.Empty : ticket.Assignment
|
||||
};
|
||||
|
||||
AddTicketIndexes(newTicket);
|
||||
m_Tickets.TryAdd(newTicket.Id, newTicket);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<Ticket> GetTicketAsync(Guid id)
|
||||
{
|
||||
if (m_Tickets.TryGetValue(id, out Ticket ticket))
|
||||
{
|
||||
return Task.FromResult(ticket);
|
||||
}
|
||||
|
||||
throw new Exception("Not Found");
|
||||
}
|
||||
|
||||
public Task DeleteTicketAsync(Guid id)
|
||||
{
|
||||
Ticket ticket = m_Tickets[id];
|
||||
RemoveTicketIndexes(ticket);
|
||||
m_Tickets.TryRemove(id, out ticket);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void AddTicketIndexes(Ticket ticket)
|
||||
{
|
||||
foreach (var attribute in ticket.Attributes)
|
||||
{
|
||||
AddOrCreateIndex(ticket.Id, attribute);
|
||||
}
|
||||
|
||||
// Built-in indexes
|
||||
AddOrCreateIndex(ticket.Id, new KeyValuePair<string, double>(createdIndex, ticket.Created));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The bulk version for reducing locks when updating lots of records at once with the same attribute
|
||||
/// </summary>
|
||||
private void AddOrCreateIndex(IEnumerable<Guid> ids, KeyValuePair<string, double> attribute)
|
||||
{
|
||||
if (!m_Indexes.ContainsKey(attribute.Key))
|
||||
{
|
||||
m_Indexes.TryAdd(attribute.Key, new SortedDictionary<Guid, double>());
|
||||
}
|
||||
|
||||
lock (m_Indexes[attribute.Key])
|
||||
{
|
||||
foreach (var id in ids)
|
||||
{
|
||||
m_Indexes[attribute.Key].Add(id, attribute.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddOrCreateIndex(Guid id, KeyValuePair<string, double> attribute)
|
||||
{
|
||||
if (m_Indexes.ContainsKey(attribute.Key))
|
||||
{
|
||||
lock (m_Indexes[attribute.Key])
|
||||
{
|
||||
m_Indexes[attribute.Key].Add(id, attribute.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Indexes.TryAdd(attribute.Key, new SortedDictionary<Guid, double>() { { id, attribute.Value } });
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveTicketIndexes(Ticket ticket)
|
||||
{
|
||||
foreach (var attribute in ticket.Attributes)
|
||||
{
|
||||
DeleteIndex(ticket.Id, attribute.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteIndex(Guid id, string attributeKey)
|
||||
{
|
||||
if (m_Indexes.ContainsKey(attributeKey))
|
||||
{
|
||||
lock (m_Indexes[attributeKey])
|
||||
{
|
||||
m_Indexes[attributeKey].Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateTicketsAsync(IEnumerable<Ticket> tickets)
|
||||
{
|
||||
foreach (var ticket in tickets)
|
||||
{
|
||||
await CreateTicketAsync(ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Captures a searching behavior for ITicketData
|
||||
/// </summary>
|
||||
public class Query
|
||||
{
|
||||
public Query()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Query(List<Filter> filters)
|
||||
{
|
||||
Filters = filters;
|
||||
}
|
||||
/// <summary>
|
||||
/// A list of hard filters to be applied to the provided searchable attributes
|
||||
/// </summary>
|
||||
public List<Filter> Filters { get; set; }
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Intended to be used as a data model abstraction for handling groups of players organized into indexable tickets
|
||||
/// </summary>
|
||||
public class Ticket
|
||||
{
|
||||
/// <summary>
|
||||
/// The identifier of the ticket tracked by clients
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A contract for allowing a backend to provide assignment information
|
||||
/// </summary>
|
||||
public string Assignment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Range indexes
|
||||
/// </summary>
|
||||
public IDictionary<string, double> Attributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The milliseconds in unix utc representing when this ticket was created
|
||||
/// </summary>
|
||||
public long Created {get;set;}
|
||||
|
||||
/// <summary>
|
||||
/// Custom data provided by the ticket creator
|
||||
/// </summary>
|
||||
public JObject Properties { get; set; }
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using Data;
|
||||
using Logic.InternalContracts;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
public class FunctionClientResolver
|
||||
{
|
||||
readonly HttpClient m_HttpClient;
|
||||
|
||||
ILoggerFactory m_LoggerFactory;
|
||||
|
||||
public FunctionClientResolver(IServiceProvider serviceProvider, HttpClient httpClient, ITicketData ticketData, ILoggerFactory loggerFactory)
|
||||
{
|
||||
m_HttpClient = httpClient;
|
||||
m_LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public IFunctionClient GetFunctionClientByTarget(TargetFunction target)
|
||||
{
|
||||
switch (target.Kind)
|
||||
{
|
||||
case FunctionKind.Rest:
|
||||
return new FunctionRestClient(m_HttpClient, target, m_LoggerFactory.CreateLogger<FunctionRestClient>());
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Logic.InternalContracts;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
public class FunctionRestClient : IFunctionClient
|
||||
{
|
||||
HttpClient m_Client;
|
||||
|
||||
string m_Address;
|
||||
|
||||
ILogger<FunctionRestClient> m_Log;
|
||||
|
||||
public FunctionRestClient(HttpClient client, TargetFunction targetFunction, ILogger<FunctionRestClient> log)
|
||||
{
|
||||
m_Log = log;
|
||||
m_Client = client;
|
||||
IPHostEntry dns = Dns.GetHostEntry(targetFunction.Name);
|
||||
m_Address = dns.AddressList[0].ToString();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Match>> RunAsync(MatchSpec spec, CancellationToken cancellationToken)
|
||||
{
|
||||
FunctionRestParams context = new FunctionRestParams()
|
||||
{
|
||||
Pools = new List<Pool>(),
|
||||
Config = spec.Config
|
||||
};
|
||||
|
||||
foreach (var specPool in spec.Pools)
|
||||
{
|
||||
context.Pools.Add(new Pool() { Name = specPool.Key, Filters = specPool.Value });
|
||||
}
|
||||
|
||||
string json = JsonConvert.SerializeObject(context);
|
||||
string url = "http://" + m_Address + ":8080" + "/api/function";
|
||||
m_Log.LogDebug("Calling {Url} with body {Body}", url, json);
|
||||
|
||||
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
request.Content = new StringContent(json);
|
||||
request.Content.Headers.Clear();
|
||||
request.Content.Headers.Add("Content-Type", "application/json");
|
||||
|
||||
HttpResponseMessage message = await m_Client.SendAsync(request, cancellationToken);
|
||||
if (!message.IsSuccessStatusCode)
|
||||
{
|
||||
m_Log.LogWarning("{StatusCode} received from function {Url}. {Reason}", message.StatusCode, m_Address, message.ReasonPhrase);
|
||||
}
|
||||
|
||||
string body = await message.Content.ReadAsStringAsync();
|
||||
|
||||
return JsonConvert.DeserializeObject<List<Match>>(body);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Logic.InternalContracts;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
public interface IEvaluator
|
||||
{
|
||||
Task<List<Match>> Evaluate(List<Match> Matches);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Logic.InternalContracts;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
public interface IFunctionClient
|
||||
{
|
||||
Task<IEnumerable<Match>> RunAsync(MatchSpec config, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Logic.InternalContracts;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
public interface IMatchmakingBackend
|
||||
{
|
||||
Task<List<Match>> GetMatchesAsync(List<MatchSpec> matchSpecs, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Logic.InternalContracts
|
||||
{
|
||||
public class FunctionRestParams
|
||||
{
|
||||
public JObject Config { get; set; }
|
||||
|
||||
public List<Pool> Pools { get; set; }
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Data;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Logic.InternalContracts
|
||||
{
|
||||
public class Match
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public List<Ticket> Tickets { get; set; }
|
||||
|
||||
public JObject Properties { get; set; }
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Data;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Logic.InternalContracts
|
||||
{
|
||||
public class MatchSpec
|
||||
{
|
||||
public TargetFunction Target { get; set; }
|
||||
|
||||
public JObject Config { get; set; }
|
||||
|
||||
public IDictionary<string, List<Filter>> Pools { get; set; }
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Data;
|
||||
|
||||
namespace Logic.InternalContracts
|
||||
{
|
||||
/// <summary>
|
||||
/// A generalized matchmaking "hard" filtering description. Consists of sets of filters
|
||||
/// </summary>
|
||||
public class Pool
|
||||
{
|
||||
/// <summary>
|
||||
/// A friendly name identifier for the pool
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The collection of generic filters for performing query logic
|
||||
/// </summary>
|
||||
public List<Filter> Filters { get; set; }
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Logic.InternalContracts
|
||||
{
|
||||
public class TargetFunction
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Version { get; set; }
|
||||
|
||||
public FunctionKind Kind { get; set; }
|
||||
}
|
||||
|
||||
public enum FunctionKind
|
||||
{
|
||||
None,
|
||||
Rest,
|
||||
Grpc,
|
||||
Memory
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Data\Data.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="2.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data;
|
||||
using Logic.InternalContracts;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
public class MatchmakingBackend : IMatchmakingBackend
|
||||
{
|
||||
ITicketData m_TicketData;
|
||||
|
||||
SynchronizationContext m_SyncContext;
|
||||
|
||||
ILogger<MatchmakingBackend> m_Logger;
|
||||
|
||||
FunctionClientResolver m_FunctionClientResolver;
|
||||
|
||||
public MatchmakingBackend(ITicketData ticketData, ILogger<MatchmakingBackend> logger, FunctionClientResolver resolver, SynchronizationContext syncContext)
|
||||
{
|
||||
m_TicketData = ticketData;
|
||||
m_Logger = logger;
|
||||
m_FunctionClientResolver = resolver;
|
||||
m_SyncContext = syncContext;
|
||||
}
|
||||
|
||||
public async Task<List<Match>> GetMatchesAsync(List<MatchSpec> matchSpecs, CancellationToken cancellationToken)
|
||||
{
|
||||
// Generate a cancellation time for all the functions. TODO: Make the global timeout configurable
|
||||
CancellationToken token = AddTimeCancellationToken(cancellationToken, 60000);
|
||||
Guid contextRegistrationId = await m_SyncContext.AcquireContext();
|
||||
|
||||
// Execute functions in parallel
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
List<Task<IEnumerable<Match>>> tasks = new List<Task<IEnumerable<Match>>>();
|
||||
foreach (var matchSpec in matchSpecs)
|
||||
{
|
||||
IFunctionClient client = m_FunctionClientResolver.GetFunctionClientByTarget(matchSpec.Target);
|
||||
m_Logger.LogInformation("Running target {Target} as {Kind}", matchSpec.Target.Name, matchSpec.Target.Kind);
|
||||
tasks.Add(Task.Run(() => client.RunAsync(matchSpec, token)));
|
||||
}
|
||||
|
||||
// Wait for all the Matches to come back in
|
||||
List<Match> Matches = (await Task.WhenAll(tasks)).SelectMany(r => r.AsEnumerable()).ToList();
|
||||
m_Logger.LogInformation("Function run time {ElapsedMs}ms. Submitting {MatchCount} Matches for evaluation.", watch.ElapsedMilliseconds, Matches.Count);
|
||||
watch.Restart();
|
||||
|
||||
// Send the Matches to the evaluator, which will automatically synchronize the Matches
|
||||
List<Match> goodMatches = await m_SyncContext.EvaluateAsync(contextRegistrationId, Matches);
|
||||
m_Logger.LogDebug("Evaluator waiting time {ElapsedMs}ms", watch.ElapsedMilliseconds);
|
||||
|
||||
// Tell the data api so it can start de-indexing those players
|
||||
List<Match> matches = new List<Match>();
|
||||
foreach (var Match in goodMatches)
|
||||
{
|
||||
matches.Add(new Match()
|
||||
{
|
||||
Properties = JObject.FromObject(Match.Properties),
|
||||
Tickets = Match.Tickets,
|
||||
// MatchSpec = null TODO: Maybe re-associate the original matchSpec
|
||||
});
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
static CancellationToken AddTimeCancellationToken(CancellationToken token, int ms)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(ms);
|
||||
return CancellationTokenSource.CreateLinkedTokenSource(token, cts.Token).Token;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Logic.InternalContracts;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// A Match de-collider takes non-colliding Matches in descending score order
|
||||
/// </summary>
|
||||
public class ScoreEvaluator : IEvaluator
|
||||
{
|
||||
ILogger<ScoreEvaluator> m_Log { get; }
|
||||
|
||||
public ScoreEvaluator(ILogger<ScoreEvaluator> log)
|
||||
{
|
||||
m_Log = log;
|
||||
}
|
||||
|
||||
public Task<List<Match>> Evaluate(List<Match> Matches)
|
||||
{
|
||||
m_Log.LogDebug("{MatchCount} Matches to be evaluated", Matches.Count);
|
||||
|
||||
// Sort the Matches by score
|
||||
Matches = Matches.OrderByDescending(p => p.Properties["score"]).ToList();
|
||||
|
||||
List<Match> goodMatches = new List<Match>();
|
||||
HashSet<Guid> ticketsPresent = new HashSet<Guid>();
|
||||
foreach (var nextMatch in Matches)
|
||||
{
|
||||
// Optimize by converting the prop tickets to a hashset
|
||||
var propTickets = nextMatch.Tickets.Select(t => t.Id).ToHashSet();
|
||||
|
||||
// Check if any of the tickets in the Match are already spoken for
|
||||
if (ticketsPresent.Overlaps(propTickets))
|
||||
continue;
|
||||
|
||||
// If not, the Match is a good match and mark the tickets as spoken for
|
||||
goodMatches.Add(nextMatch);
|
||||
foreach (var ticketId in propTickets)
|
||||
{
|
||||
ticketsPresent.Add(ticketId);
|
||||
}
|
||||
}
|
||||
|
||||
m_Log.LogDebug("{MatchesApproved} Matches approved in evaluation. {TicketsApproved} tickets approved", goodMatches.Count, ticketsPresent.Count);
|
||||
|
||||
return Task.FromResult(goodMatches);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,252 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data;
|
||||
using Logic.InternalContracts;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// A threadsafe way to run an evaluator. Intended to be used as a singleton, or shared context behind a service
|
||||
/// </summary>
|
||||
public class SynchronizationContext
|
||||
{
|
||||
int m_MinRunMs;
|
||||
|
||||
int m_MaxRunMs;
|
||||
|
||||
ITicketData m_TicketData;
|
||||
|
||||
IEvaluator m_Evaluator;
|
||||
|
||||
ILogger<SynchronizationContext> m_Logger;
|
||||
|
||||
ConcurrentDictionary<Guid, List<Match>> m_ContextMatches = new ConcurrentDictionary<Guid, List<Match>>();
|
||||
|
||||
ConcurrentDictionary<Guid, List<Guid>> m_ContextResults = new ConcurrentDictionary<Guid, List<Guid>>();
|
||||
|
||||
ConcurrentDictionary<Guid, bool> m_ExistingContexts = new ConcurrentDictionary<Guid, bool>();
|
||||
|
||||
ManualResetEvent m_NewContextsAvailable = new ManualResetEvent(false);
|
||||
|
||||
ManualResetEvent m_ResultsAvailable = new ManualResetEvent(false);
|
||||
|
||||
bool m_AcceptingMatches = false;
|
||||
|
||||
Timer m_Timer;
|
||||
|
||||
Stopwatch m_Watch = new Stopwatch();
|
||||
|
||||
Task m_EvalTask = null;
|
||||
|
||||
SyncState m_State = SyncState.NotRunning;
|
||||
|
||||
object startLock = new object();
|
||||
|
||||
enum SyncState
|
||||
{
|
||||
NotRunning,
|
||||
AcceptingContexts,
|
||||
AcceptingMatches,
|
||||
Evaluating
|
||||
}
|
||||
|
||||
public SynchronizationContext(ILogger<SynchronizationContext> logger, ITicketData ticketData, IEvaluator evaluator, IOptions<SynchronizationOptions> options)
|
||||
{
|
||||
m_Logger = logger;
|
||||
m_MinRunMs = options.Value.MinWindowSizeMs;
|
||||
m_MaxRunMs = options.Value.MaxWindowSizeMs;
|
||||
m_Evaluator = evaluator;
|
||||
m_TicketData = ticketData;
|
||||
|
||||
// TODO: Make the loop event schedulable instead of loop driven
|
||||
m_Timer = new Timer(
|
||||
UpdateState,
|
||||
new AutoResetEvent(true),
|
||||
options.Value.StateMachineUpdateMs,
|
||||
options.Value.StateMachineUpdateMs
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread safe way to acquire a contextId and register for the evaluator
|
||||
/// </summary>
|
||||
/// <returns>A contextId</returns>
|
||||
public async Task<Guid> AcquireContext()
|
||||
{
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
Guid contextId = await WaitRegisterContextAsync();
|
||||
m_Logger.LogDebug("{ElapsedMs}ms to acquire context", watch.ElapsedMilliseconds);
|
||||
return contextId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread safe way to let the evaluator de-collide the passed in Matches with other contexts
|
||||
/// </summary>
|
||||
/// <param name="contextId">The id of this context</param>
|
||||
/// <param name="Matches">Matches to de-collide</param>
|
||||
/// <returns>A list of de-collided Matches</returns>
|
||||
/// <exception cref="Exception"></exception>
|
||||
public async Task<List<Match>> EvaluateAsync(Guid contextId, List<Match> Matches)
|
||||
{
|
||||
Stopwatch watch = Stopwatch.StartNew();
|
||||
|
||||
// Try to register the Matches with the machine
|
||||
if (TryRegisterMatches(contextId, Matches))
|
||||
{
|
||||
m_Logger.LogDebug("{ElapsedMs}ms to register Matches", watch.ElapsedMilliseconds);
|
||||
watch.Restart();
|
||||
|
||||
// Wait for the machine to run and return my results
|
||||
List<Guid> good = await WaitResultsAsync(contextId);
|
||||
m_Logger.LogDebug("{ElapsedMs}ms evaluation results available", watch.ElapsedMilliseconds);
|
||||
List<Match> goodMatches = new List<Match>();
|
||||
foreach (var prop in Matches)
|
||||
{
|
||||
if (good.Contains(prop.Id))
|
||||
{
|
||||
goodMatches.Add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
return goodMatches;
|
||||
}
|
||||
|
||||
throw new Exception("Match registration failed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to wait for context acquisition to become available and register for one. If the evaluator
|
||||
/// is not running, it will set the state machine into a runnable state
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Task<Guid> WaitRegisterContextAsync()
|
||||
{
|
||||
// If the machine isn't started, try to start it
|
||||
if (m_State == SyncState.NotRunning)
|
||||
{
|
||||
lock(startLock)
|
||||
{
|
||||
// Make sure this call got the lock in time, otherwise bail
|
||||
if (m_State == SyncState.NotRunning)
|
||||
{
|
||||
// The machine isn't running so clear the current results, any registrations, and any Matches
|
||||
m_ContextResults.Clear();
|
||||
m_ExistingContexts.Clear();
|
||||
m_ContextMatches.Clear();
|
||||
|
||||
// Allow new contexts to register, allow new Matches, and disallow results reading
|
||||
m_State = SyncState.AcceptingContexts;
|
||||
m_NewContextsAvailable.Set();
|
||||
m_ResultsAvailable.Reset();
|
||||
m_AcceptingMatches = true;
|
||||
m_Watch = Stopwatch.StartNew();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_NewContextsAvailable.WaitOne(5000); // TODO: Make this wait timeout automated
|
||||
Guid newId = Guid.NewGuid();
|
||||
m_ExistingContexts.TryAdd(newId, false);
|
||||
return Task.FromResult(newId);
|
||||
}
|
||||
|
||||
private Task<List<Guid>> WaitResultsAsync(Guid contextId)
|
||||
{
|
||||
m_ResultsAvailable.WaitOne(5000); // TODO: Make this wait timeout automated
|
||||
return Task.FromResult(m_ContextResults[contextId]);
|
||||
}
|
||||
|
||||
private bool TryRegisterMatches(Guid contextId, List<Match> Matches)
|
||||
{
|
||||
if (!m_ExistingContexts.ContainsKey(contextId)) return false;
|
||||
if (!m_AcceptingMatches) return false;
|
||||
|
||||
m_ExistingContexts[contextId] = true;
|
||||
m_ContextResults.TryAdd(contextId, new List<Guid>());
|
||||
|
||||
return m_ContextMatches.TryAdd(contextId, Matches);
|
||||
}
|
||||
|
||||
private void UpdateState(object state)
|
||||
{
|
||||
switch (m_State)
|
||||
{
|
||||
case SyncState.NotRunning:
|
||||
break;
|
||||
case SyncState.AcceptingContexts:
|
||||
if (m_Watch.ElapsedMilliseconds > m_MinRunMs)
|
||||
{
|
||||
m_Logger.LogDebug("Min window passed at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
|
||||
m_State = SyncState.AcceptingMatches;
|
||||
m_NewContextsAvailable.Reset();
|
||||
UpdateState(state); // Just go ahead and check the accepting state
|
||||
}
|
||||
|
||||
break;
|
||||
case SyncState.AcceptingMatches:
|
||||
bool maxWindowExceeded = m_Watch.ElapsedMilliseconds > m_MaxRunMs;
|
||||
bool allIn = m_ExistingContexts.Values.All(b => b);
|
||||
if (m_Watch.ElapsedMilliseconds > m_MaxRunMs || m_ExistingContexts.Values.All(b => b))
|
||||
{
|
||||
if (maxWindowExceeded) m_Logger.LogDebug("Max window exceeded. Moving to eval at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
|
||||
if (allIn) m_Logger.LogDebug("All contexts reported in. Moving to eval at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
|
||||
m_State = SyncState.Evaluating;
|
||||
m_AcceptingMatches = false;
|
||||
m_EvalTask = Task.Run(async () =>
|
||||
{
|
||||
// Run the evaluator in parallel to this state system
|
||||
await RunEvaluation(m_Evaluator);
|
||||
|
||||
// Once done, clear the other threads to read from the results. Set the machine back to doing nothing
|
||||
m_State = SyncState.NotRunning;
|
||||
m_ResultsAvailable.Set();
|
||||
m_Logger.LogDebug("Evaluation completed at {ElapsedMs}ms", m_Watch.ElapsedMilliseconds);
|
||||
m_Watch.Reset();
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
case SyncState.Evaluating:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunEvaluation(IEvaluator evaluator)
|
||||
{
|
||||
// Create a reverse map of context to Match (to rebuild the results at the end)
|
||||
Dictionary<Guid, Guid> MatchIdToContextId = new Dictionary<Guid, Guid>();
|
||||
List<Match> allMatches = new List<Match>();
|
||||
foreach (var contextMatch in m_ContextMatches)
|
||||
{
|
||||
foreach (var Match in contextMatch.Value)
|
||||
{
|
||||
allMatches.Add(Match);
|
||||
MatchIdToContextId.Add(Match.Id, contextMatch.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the evaluator
|
||||
List<Match> matches = await evaluator.Evaluate(allMatches);
|
||||
|
||||
// Flag the selected tickets as un-queryable for a set period of time. // TODO: Make configurable
|
||||
List<Guid> ticketsTaken = matches.SelectMany(m => m.Tickets.Select(t => t.Id)).ToList();
|
||||
await m_TicketData.AwaitingAssignmentAsync(ticketsTaken, 60000);
|
||||
|
||||
// Put the match results into the proper results context. TODO: Failure handled good enough by unqueryable timeout?
|
||||
foreach (var match in matches)
|
||||
{
|
||||
Guid contextId = MatchIdToContextId[match.Id];
|
||||
m_ContextResults[contextId].Add(match.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Logic
|
||||
{
|
||||
public class SynchronizationOptions
|
||||
{
|
||||
public const string SectionName = "SynchronizationOptions";
|
||||
|
||||
public int MinWindowSizeMs { get; set; }
|
||||
|
||||
public int MaxWindowSizeMs { get; set; }
|
||||
|
||||
public int StateMachineUpdateMs { get; set; }
|
||||
}
|
||||
}
|
@ -16,6 +16,12 @@ FROM golang:latest
|
||||
ENV GO111MODULE=on
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match
|
||||
COPY . .
|
||||
|
||||
# First copy only the go.sum and go.mod then download dependencies. Docker
|
||||
# caching is [in]validated by the input files changes. So when the dependencies
|
||||
# for the project don't change, the previous image layer can be re-used. go.sum
|
||||
# is included as its hashing verifies the expected files are downloaded.
|
||||
COPY go.sum go.mod ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
@ -35,11 +35,10 @@ RUN export CLOUD_SDK_REPO="cloud-sdk-stretch" && \
|
||||
|
||||
# Install Golang
|
||||
# https://github.com/docker-library/golang/blob/fd272b2b72db82a0bd516ce3d09bba624651516c/1.12/stretch/Dockerfile
|
||||
|
||||
RUN mkdir -p /toolchain/golang
|
||||
WORKDIR /toolchain/golang
|
||||
RUN sudo rm -rf /usr/local/go/
|
||||
RUN curl -L https://storage.googleapis.com/golang/go1.12.1.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
|
||||
RUN curl -L https://storage.googleapis.com/golang/go1.12.6.linux-amd64.tar.gz | sudo tar -C /usr/local -xz
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
|
||||
@ -49,7 +48,8 @@ RUN sudo mkdir -p "$GOPATH/src" "$GOPATH/bin" \
|
||||
|
||||
# Prepare toolchain and workspace
|
||||
RUN mkdir -p /toolchain
|
||||
RUN mkdir -p /workspace
|
||||
|
||||
WORKDIR /workspace
|
||||
ENV ALLOW_BUILD_WITH_SUDO=1
|
||||
ENV OPEN_MATCH_CI_MODE=1
|
||||
ENV KUBECONFIG=$HOME/.kube/config
|
||||
RUN mkdir -p $HOME/.kube/
|
||||
|
60
Dockerfile.cmd
Normal file
60
Dockerfile.cmd
Normal file
@ -0,0 +1,60 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match
|
||||
|
||||
ARG IMAGE_TITLE
|
||||
|
||||
RUN make "build/cmd/${IMAGE_TITLE}"
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
ARG IMAGE_TITLE
|
||||
WORKDIR /app/
|
||||
|
||||
COPY --from=builder --chown=nonroot "/go/src/open-match.dev/open-match/build/cmd/${IMAGE_TITLE}/" "/app/"
|
||||
|
||||
ENTRYPOINT ["/app/run"]
|
||||
|
||||
# Docker Image Arguments
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG BUILD_VERSION
|
||||
|
||||
# Standardized Docker Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
LABEL \
|
||||
org.opencontainers.image.created="${BUILD_TIME}" \
|
||||
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
|
||||
org.opencontainers.image.url="https://open-match.dev/" \
|
||||
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
|
||||
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.revision="1" \
|
||||
org.opencontainers.image.vendor="Google LLC" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.ref.name="" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.url="http://open-match.dev/" \
|
||||
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
|
||||
org.label-schema.version=$BUILD_VERSION \
|
||||
org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vendor="Google LLC" \
|
||||
org.label-schema.name="${IMAGE_TITLE}" \
|
||||
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.usage="https://open-match.dev/site/docs/"
|
49
README.md
49
README.md
@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
[](https://godoc.org/open-match.dev/open-match)
|
||||
[](https://goreportcard.com/report/open-match.dev/open-match)
|
||||
@ -6,19 +6,27 @@
|
||||
[](https://github.com/googleforgames/open-match/releases)
|
||||
[](https://twitter.com/intent/follow?screen_name=Open_Match)
|
||||
|
||||
Open Match is a flexible, extensible, and scalable game matchmaking framework.
|
||||
Open Match is an open source game matchmaking framework that simplifies building
|
||||
a scalable and extensible Matchmaker. It is designed to give the game developer
|
||||
full control over how to make matches while removing the burden of dealing with
|
||||
the challenges of running a production service at scale.
|
||||
|
||||
Open Match replaces the complex plumbing parts of matchmaking and allows you to
|
||||
focus on the details of making a good match based on your game.
|
||||
Please visit [Open Match website](https://open-match.dev/site/docs/) for user
|
||||
documentation, demo instructions etc.
|
||||
|
||||
## Contributing to Open Match
|
||||
|
||||
Open Match is in active development and we would love your contribution! Please
|
||||
read the [contributing guide](CONTRIBUTING.md) for guidelines on contributing to
|
||||
Open Match.
|
||||
|
||||
The [Open Match Development guide](docs/development.md) has detailed instructions
|
||||
on getting the source code, making changes, testing and submitting a pull request
|
||||
to Open Match.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This software is currently alpha, and subject to change. Although Open Match has
|
||||
already been used to run [production workloads within Google](https://cloud.google.com/blog/topics/inside-google-cloud/no-tricks-just-treats-globally-scaling-the-halloween-multiplayer-doodle-with-open-match-on-google-cloud).
|
||||
|
||||
## Usage
|
||||
|
||||
Documentation can be found on the [Open Match website](https://open-match.dev/site/docs/).
|
||||
This software is currently alpha, and subject to change.
|
||||
|
||||
## Support
|
||||
|
||||
@ -30,27 +38,6 @@ Documentation can be found on the [Open Match website](https://open-match.dev/si
|
||||
|
||||
Participation in this project comes under the [Contributor Covenant Code of Conduct](code-of-conduct.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
Here are some useful links to additional documentation:
|
||||
|
||||
* [Get Started](https://open-match.dev/site/docs/installation/)
|
||||
* [Development Guide](docs/development.md)
|
||||
* [Future Roadmap](docs/roadmap.md)
|
||||
* [Open Match Concepts](docs/concepts.md)
|
||||
* [Open Match Integrations](docs/integrations.md)
|
||||
* [References](docs/references.md)
|
||||
|
||||
For more information on the technical underpinnings of Open Match you can refer to the [docs/](docs/) directory.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read the [contributing](CONTRIBUTING.md) guide for directions on submitting Pull Requests to Open Match.
|
||||
|
||||
See the [Development guide](docs/development.md) for documentation for development and building Open Match from source.
|
||||
|
||||
Open Match is in active development - we would love your help in shaping its future!
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
|
@ -13,8 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
package api;
|
||||
option go_package = "internal/pb";
|
||||
package openmatch;
|
||||
option go_package = "open-match.dev/open-match/pkg/pb";
|
||||
option csharp_namespace = "OpenMatch";
|
||||
|
||||
import "api/messages.proto";
|
||||
import "google/api/annotations.proto";
|
||||
@ -54,28 +55,15 @@ option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
|
||||
// https://github.com/grpc-ecosystem/grpc-gateway/blob/master/examples/proto/examplepb/a_bit_of_everything.proto
|
||||
};
|
||||
|
||||
// Configuration for a GRPC Match Function
|
||||
message GrpcFunctionConfig {
|
||||
string host = 1;
|
||||
int32 port = 2;
|
||||
}
|
||||
|
||||
// Configuration for a REST Match Function.
|
||||
message RestFunctionConfig {
|
||||
string host = 1;
|
||||
int32 port = 2;
|
||||
}
|
||||
|
||||
// Configuration for the Match Function to be triggered by Open Match to
|
||||
// generate proposals.
|
||||
message FunctionConfig {
|
||||
// A developer-chosen human-readable name for this Match Function.
|
||||
string name = 1;
|
||||
|
||||
// Properties for the type of this function.
|
||||
oneof type {
|
||||
GrpcFunctionConfig grpc = 10001;
|
||||
RestFunctionConfig rest = 10002;
|
||||
string host = 1;
|
||||
int32 port = 2;
|
||||
Type type = 3;
|
||||
enum Type {
|
||||
GRPC = 0;
|
||||
REST = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,17 +72,18 @@ message FetchMatchesRequest {
|
||||
FunctionConfig config = 1;
|
||||
|
||||
// MatchProfiles for which this MatchFunction should be executed.
|
||||
repeated MatchProfile profile = 2;
|
||||
repeated MatchProfile profiles = 2;
|
||||
}
|
||||
|
||||
message FetchMatchesResponse {
|
||||
// Result Match for the requested MatchProfile.
|
||||
// Note that OpenMatch will validate the proposals, a valid match should contain at least one ticket.
|
||||
Match match = 1;
|
||||
}
|
||||
|
||||
message AssignTicketsRequest {
|
||||
// List of Ticket IDs for which the Assignment is to be made.
|
||||
repeated string ticket_id = 1;
|
||||
repeated string ticket_ids = 1;
|
||||
|
||||
// Assignment to be associated with the Ticket IDs.
|
||||
Assignment assignment = 2;
|
||||
|
@ -32,7 +32,7 @@
|
||||
"200": {
|
||||
"description": "A successful response.(streaming responses)",
|
||||
"schema": {
|
||||
"$ref": "#/x-stream-definitions/apiFetchMatchesResponse"
|
||||
"$ref": "#/x-stream-definitions/openmatchFetchMatchesResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -48,7 +48,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiFetchMatchesRequest"
|
||||
"$ref": "#/definitions/openmatchFetchMatchesRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -65,7 +65,7 @@
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiAssignTicketsResponse"
|
||||
"$ref": "#/definitions/openmatchAssignTicketsResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -81,7 +81,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiAssignTicketsRequest"
|
||||
"$ref": "#/definitions/openmatchAssignTicketsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -92,10 +92,10 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"apiAssignTicketsRequest": {
|
||||
"openmatchAssignTicketsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticket_id": {
|
||||
"ticket_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@ -103,15 +103,15 @@
|
||||
"description": "List of Ticket IDs for which the Assignment is to be made."
|
||||
},
|
||||
"assignment": {
|
||||
"$ref": "#/definitions/apiAssignment",
|
||||
"$ref": "#/definitions/openmatchAssignment",
|
||||
"description": "Assignment to be associated with the Ticket IDs."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiAssignTicketsResponse": {
|
||||
"openmatchAssignTicketsResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"apiAssignment": {
|
||||
"openmatchAssignment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connection": {
|
||||
@ -119,42 +119,55 @@
|
||||
"description": "Connection information for this Assignment."
|
||||
},
|
||||
"properties": {
|
||||
"type": "string",
|
||||
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
|
||||
"type": "object",
|
||||
"description": "Other details to be sent to the players."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"$ref": "#/definitions/rpcStatus",
|
||||
"description": "Error when finding an Assignment for this Ticket."
|
||||
}
|
||||
},
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket."
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket. Open\nmatch does not require or inspect any fields on assignment."
|
||||
},
|
||||
"apiFetchMatchesRequest": {
|
||||
"openmatchBoolEqualsFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "Filters boolean values.\n attribute: \"foo\"\n value: false\nmatches:\n {\"foo\": false}\ndoes not match:\n {\"foo\": true}\n {\"foo\": \"bar\"}\n {\"foo\": 1}\n {\"foo\": \"false\"}\n {\"foo\": [false]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"openmatchFetchMatchesRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"$ref": "#/definitions/apiFunctionConfig",
|
||||
"$ref": "#/definitions/openmatchFunctionConfig",
|
||||
"title": "Configuration of the MatchFunction to be executed for the given list of MatchProfiles"
|
||||
},
|
||||
"profile": {
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiMatchProfile"
|
||||
"$ref": "#/definitions/openmatchMatchProfile"
|
||||
},
|
||||
"description": "MatchProfiles for which this MatchFunction should be executed."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiFetchMatchesResponse": {
|
||||
"openmatchFetchMatchesResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"match": {
|
||||
"$ref": "#/definitions/apiMatch",
|
||||
"description": "Result Match for the requested MatchProfile."
|
||||
"$ref": "#/definitions/openmatchMatch",
|
||||
"description": "Result Match for the requested MatchProfile.\nNote that OpenMatch will validate the proposals, a valid match should contain at least one ticket."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiFilter": {
|
||||
"openmatchFloatRangeFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
@ -172,25 +185,9 @@
|
||||
"description": "Minimum value. Defaults to 0."
|
||||
}
|
||||
},
|
||||
"description": "A hard filter used to query a subset of Tickets meeting the filtering\ncriteria."
|
||||
"title": "Filters numerical values to only those within a range.\n attribute: \"foo\"\n max: 10\n min: 5\nmatches:\n {\"foo\": 5}\n {\"foo\": 7.5}\n {\"foo\": 10}\ndoes not match:\n {\"foo\": 4}\n {\"foo\": 10.01}\n {\"foo\": \"7.5\"}\n {\"foo\": true}\n {\"foo\": [7.5]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"apiFunctionConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A developer-chosen human-readable name for this Match Function."
|
||||
},
|
||||
"grpc": {
|
||||
"$ref": "#/definitions/apiGrpcFunctionConfig"
|
||||
},
|
||||
"rest": {
|
||||
"$ref": "#/definitions/apiRestFunctionConfig"
|
||||
}
|
||||
},
|
||||
"description": "Configuration for the Match Function to be triggered by Open Match to\ngenerate proposals."
|
||||
},
|
||||
"apiGrpcFunctionConfig": {
|
||||
"openmatchFunctionConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {
|
||||
@ -199,11 +196,22 @@
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/openmatchFunctionConfigType"
|
||||
}
|
||||
},
|
||||
"title": "Configuration for a GRPC Match Function"
|
||||
"description": "Configuration for the Match Function to be triggered by Open Match to\ngenerate proposals."
|
||||
},
|
||||
"apiMatch": {
|
||||
"openmatchFunctionConfigType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"GRPC",
|
||||
"REST"
|
||||
],
|
||||
"default": "GRPC"
|
||||
},
|
||||
"openmatchMatch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"match_id": {
|
||||
@ -218,28 +226,28 @@
|
||||
"type": "string",
|
||||
"description": "Name of the match function that generated this Match."
|
||||
},
|
||||
"ticket": {
|
||||
"tickets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiTicket"
|
||||
"$ref": "#/definitions/openmatchTicket"
|
||||
},
|
||||
"description": "Tickets belonging to this match."
|
||||
},
|
||||
"roster": {
|
||||
"rosters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiRoster"
|
||||
"$ref": "#/definitions/openmatchRoster"
|
||||
},
|
||||
"title": "Set of Rosters that comprise this Match"
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Match properties for this Match. Open Match does not interpret this field."
|
||||
}
|
||||
},
|
||||
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call."
|
||||
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call.\nWhen a match is returned by the FetchMatches call, it should contain at least \none ticket to be considered as valid."
|
||||
},
|
||||
"apiMatchProfile": {
|
||||
"openmatchMatchProfile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
@ -247,63 +255,62 @@
|
||||
"description": "Name of this match profile."
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Set of properties associated with this MatchProfile. (Optional)\nOpen Match does not interpret these properties but passes them through to\nthe MatchFunction."
|
||||
},
|
||||
"pool": {
|
||||
"pools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiPool"
|
||||
"$ref": "#/definitions/openmatchPool"
|
||||
},
|
||||
"description": "Set of pools to be queried when generating a match for this MatchProfile.\nThe pool names can be used in empty Rosters to specify composition of a\nmatch."
|
||||
},
|
||||
"roster": {
|
||||
"rosters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiRoster"
|
||||
"$ref": "#/definitions/openmatchRoster"
|
||||
},
|
||||
"description": "Set of Rosters for this match request. Could be empty Rosters used to\nindicate the composition of the generated Match or they could be partially\npre-populated Ticket list to be used in scenarios such as backfill / join\nin progress."
|
||||
}
|
||||
},
|
||||
"description": "A MatchProfile is Open Match's representation of a Match specification. It is\nused to indicate the criteria for selecting players for a match. A\nMatchProfile is the input to the API to get matches and is passed to the\nMatchFunction. It contains all the information required by the MatchFunction\nto generate match proposals."
|
||||
},
|
||||
"apiPool": {
|
||||
"openmatchPool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A developer-chosen human-readable name for this Pool."
|
||||
},
|
||||
"filter": {
|
||||
"float_range_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiFilter"
|
||||
"$ref": "#/definitions/openmatchFloatRangeFilter"
|
||||
},
|
||||
"description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
|
||||
},
|
||||
"bool_equals_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/openmatchBoolEqualsFilter"
|
||||
}
|
||||
},
|
||||
"string_equals_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/openmatchStringEqualsFilter"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiRestFunctionConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
},
|
||||
"description": "Configuration for a REST Match Function."
|
||||
},
|
||||
"apiRoster": {
|
||||
"openmatchRoster": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A developer-chosen human-readable name for this Roster."
|
||||
},
|
||||
"ticket_id": {
|
||||
"ticket_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@ -313,7 +320,19 @@
|
||||
},
|
||||
"description": "A Roster is a named collection of Ticket IDs. It exists so that a Tickets\nassociated with a Match can be labelled to belong to a team, sub-team etc. It\ncan also be used to represent the current state of a Match in scenarios such\nas backfill, join-in-progress etc."
|
||||
},
|
||||
"apiTicket": {
|
||||
"openmatchStringEqualsFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "Filters strings exactly equaling a value.\n attribute: \"foo\"\n value: \"bar\"\nmatches:\n {\"foo\": \"bar\"}\ndoes not match:\n {\"foo\": \"baz\"}\n {\"foo\": true}\n {\"foo\": 5}\n {\"foo\": [\"bar\"]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"openmatchTicket": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@ -321,11 +340,11 @@
|
||||
"description": "The Ticket ID generated by Open Match."
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
|
||||
},
|
||||
"assignment": {
|
||||
"$ref": "#/definitions/apiAssignment",
|
||||
"$ref": "#/definitions/openmatchAssignment",
|
||||
"description": "Assignment associated with the Ticket."
|
||||
}
|
||||
},
|
||||
@ -346,19 +365,6 @@
|
||||
},
|
||||
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
|
||||
},
|
||||
"protobufListValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Repeated field of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
|
||||
},
|
||||
"protobufNullValue": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@ -367,50 +373,28 @@
|
||||
"default": "NULL_VALUE",
|
||||
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
|
||||
},
|
||||
"protobufStruct": {
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Unordered map of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
|
||||
},
|
||||
"protobufValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"null_value": {
|
||||
"$ref": "#/definitions/protobufNullValue",
|
||||
"description": "Represents a null value."
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The status code, which should be an enum value of\n[google.rpc.Code][google.rpc.Code]."
|
||||
},
|
||||
"number_value": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Represents a double value."
|
||||
},
|
||||
"string_value": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Represents a string value."
|
||||
"description": "A developer-facing error message, which should be in English. Any\nuser-facing error message should be localized and sent in the\n[google.rpc.Status.details][google.rpc.Status.details] field, or localized\nby the client."
|
||||
},
|
||||
"bool_value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"description": "Represents a boolean value."
|
||||
},
|
||||
"struct_value": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"description": "Represents a structured value."
|
||||
},
|
||||
"list_value": {
|
||||
"$ref": "#/definitions/protobufListValue",
|
||||
"description": "Represents a repeated `Value`."
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
},
|
||||
"description": "A list of messages that carry the error details. There is a common set of\nmessage types for APIs to use."
|
||||
}
|
||||
},
|
||||
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
|
||||
"description": "- Simple to use and understand for most users\n- Flexible enough to meet unexpected needs\n\n# Overview\n\nThe `Status` message contains three pieces of data: error code, error\nmessage, and error details. The error code should be an enum value of\n[google.rpc.Code][google.rpc.Code], but it may accept additional error codes\nif needed. The error message should be a developer-facing English message\nthat helps developers *understand* and *resolve* the error. If a localized\nuser-facing error message is needed, put the localized message in the error\ndetails or localize it in the client. The optional error details may contain\narbitrary information about the error. There is a predefined set of error\ndetail types in the package `google.rpc` that can be used for common error\nconditions.\n\n# Language mapping\n\nThe `Status` message is the logical representation of the error model, but it\nis not necessarily the actual wire format. When the `Status` message is\nexposed in different client libraries and different wire protocols, it can be\nmapped differently. For example, it will likely be mapped to some exceptions\nin Java, but more likely mapped to some error codes in C.\n\n# Other uses\n\nThe error model and the `Status` message can be used in a variety of\nenvironments, either with or without APIs, to provide a\nconsistent developer experience across different environments.\n\nExample uses of this error model include:\n\n- Partial errors. If a service needs to return partial errors to the client,\n it may embed the `Status` in the normal response to indicate the partial\n errors.\n\n- Workflow errors. A typical workflow has multiple steps. Each step may\n have a `Status` message for error reporting.\n\n- Batch operations. If a client uses batch request and batch response, the\n `Status` message should be used directly inside batch response, one for\n each error sub-response.\n\n- Asynchronous operations. If an API call embeds asynchronous operation\n results in its response, the status of those operations should be\n represented directly using the `Status` message.\n\n- Logging. If some API errors are stored in logs, the message `Status` could\n be used directly after any stripping needed for security/privacy reasons.",
|
||||
"title": "The `Status` type defines a logical error model that is suitable for\ndifferent programming environments, including REST APIs and RPC APIs. It is\nused by [gRPC](https://github.com/grpc). The error model is designed to be:"
|
||||
},
|
||||
"runtimeStreamError": {
|
||||
"type": "object",
|
||||
@ -439,17 +423,17 @@
|
||||
}
|
||||
},
|
||||
"x-stream-definitions": {
|
||||
"apiFetchMatchesResponse": {
|
||||
"openmatchFetchMatchesResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/apiFetchMatchesResponse"
|
||||
"$ref": "#/definitions/openmatchFetchMatchesResponse"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/definitions/runtimeStreamError"
|
||||
}
|
||||
},
|
||||
"title": "Stream result of apiFetchMatchesResponse"
|
||||
"title": "Stream result of openmatchFetchMatchesResponse"
|
||||
}
|
||||
},
|
||||
"externalDocs": {
|
||||
|
@ -13,8 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
package api;
|
||||
option go_package = "internal/pb";
|
||||
package openmatch;
|
||||
option go_package = "open-match.dev/open-match/pkg/pb";
|
||||
option csharp_namespace = "OpenMatch";
|
||||
|
||||
import "api/messages.proto";
|
||||
import "google/api/annotations.proto";
|
||||
@ -56,21 +57,20 @@ option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
|
||||
|
||||
message EvaluateRequest {
|
||||
// List of Matches to evaluate.
|
||||
repeated Match match = 1;
|
||||
Match match = 1;
|
||||
}
|
||||
|
||||
message EvaluateResponse {
|
||||
// Accepted list of Matches.
|
||||
repeated Match match = 1;
|
||||
Match match = 1;
|
||||
}
|
||||
|
||||
// The service implementing the Evaluator API that is called to evaluate
|
||||
// matches generated by MMFs and shortlist them to accepted results.
|
||||
service Evaluator {
|
||||
// Evaluate accepts a list of matches, triggers the user configured evaluation
|
||||
// function with these and other matches in the evaluation window and returns
|
||||
// matches that are accepted by the Evaluator as valid results.
|
||||
rpc Evaluate(EvaluateRequest) returns (EvaluateResponse) {
|
||||
// Evaluate accepts a list of proposed matches, evaluates them for quality,
|
||||
// collisions etc. and returns matches that should be accepted as results.
|
||||
rpc Evaluate(stream EvaluateRequest) returns (stream EvaluateResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v1/evaluator/matches:evaluate"
|
||||
body: "*"
|
||||
|
@ -26,13 +26,13 @@
|
||||
"paths": {
|
||||
"/v1/evaluator/matches:evaluate": {
|
||||
"post": {
|
||||
"summary": "Evaluate accepts a list of matches, triggers the user configured evaluation\nfunction with these and other matches in the evaluation window and returns\nmatches that are accepted by the Evaluator as valid results.",
|
||||
"summary": "Evaluate accepts a list of proposed matches, evaluates them for quality,\ncollisions etc. and returns matches that should be accepted as results.",
|
||||
"operationId": "Evaluate",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"description": "A successful response.(streaming responses)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiEvaluateResponse"
|
||||
"$ref": "#/x-stream-definitions/openmatchEvaluateResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -45,10 +45,11 @@
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": " (streaming inputs)",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiEvaluateRequest"
|
||||
"$ref": "#/definitions/openmatchEvaluateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -59,7 +60,7 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"apiAssignment": {
|
||||
"openmatchAssignment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connection": {
|
||||
@ -67,41 +68,35 @@
|
||||
"description": "Connection information for this Assignment."
|
||||
},
|
||||
"properties": {
|
||||
"type": "string",
|
||||
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
|
||||
"type": "object",
|
||||
"description": "Other details to be sent to the players."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"$ref": "#/definitions/rpcStatus",
|
||||
"description": "Error when finding an Assignment for this Ticket."
|
||||
}
|
||||
},
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket."
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket. Open\nmatch does not require or inspect any fields on assignment."
|
||||
},
|
||||
"apiEvaluateRequest": {
|
||||
"openmatchEvaluateRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"match": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiMatch"
|
||||
},
|
||||
"$ref": "#/definitions/openmatchMatch",
|
||||
"description": "List of Matches to evaluate."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiEvaluateResponse": {
|
||||
"openmatchEvaluateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"match": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiMatch"
|
||||
},
|
||||
"$ref": "#/definitions/openmatchMatch",
|
||||
"description": "Accepted list of Matches."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiMatch": {
|
||||
"openmatchMatch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"match_id": {
|
||||
@ -116,35 +111,35 @@
|
||||
"type": "string",
|
||||
"description": "Name of the match function that generated this Match."
|
||||
},
|
||||
"ticket": {
|
||||
"tickets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiTicket"
|
||||
"$ref": "#/definitions/openmatchTicket"
|
||||
},
|
||||
"description": "Tickets belonging to this match."
|
||||
},
|
||||
"roster": {
|
||||
"rosters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiRoster"
|
||||
"$ref": "#/definitions/openmatchRoster"
|
||||
},
|
||||
"title": "Set of Rosters that comprise this Match"
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Match properties for this Match. Open Match does not interpret this field."
|
||||
}
|
||||
},
|
||||
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call."
|
||||
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call.\nWhen a match is returned by the FetchMatches call, it should contain at least \none ticket to be considered as valid."
|
||||
},
|
||||
"apiRoster": {
|
||||
"openmatchRoster": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A developer-chosen human-readable name for this Roster."
|
||||
},
|
||||
"ticket_id": {
|
||||
"ticket_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@ -154,7 +149,7 @@
|
||||
},
|
||||
"description": "A Roster is a named collection of Ticket IDs. It exists so that a Tickets\nassociated with a Match can be labelled to belong to a team, sub-team etc. It\ncan also be used to represent the current state of a Match in scenarios such\nas backfill, join-in-progress etc."
|
||||
},
|
||||
"apiTicket": {
|
||||
"openmatchTicket": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@ -162,28 +157,30 @@
|
||||
"description": "The Ticket ID generated by Open Match."
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
|
||||
},
|
||||
"assignment": {
|
||||
"$ref": "#/definitions/apiAssignment",
|
||||
"$ref": "#/definitions/openmatchAssignment",
|
||||
"description": "Assignment associated with the Ticket."
|
||||
}
|
||||
},
|
||||
"description": "A Ticket is a basic matchmaking entity in Open Match. In order to enter\nmatchmaking using Open Match, the client should generate a Ticket, passing in\nthe properties to be associated with this Ticket. Open Match will generate an\nID for a Ticket during creation. A Ticket could be used to represent an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof properties. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
|
||||
},
|
||||
"protobufListValue": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Repeated field of dynamically typed values."
|
||||
"type_url": {
|
||||
"type": "string",
|
||||
"description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Must be a valid serialized protocol buffer of the above specified type."
|
||||
}
|
||||
},
|
||||
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
|
||||
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
|
||||
},
|
||||
"protobufNullValue": {
|
||||
"type": "string",
|
||||
@ -193,50 +190,67 @@
|
||||
"default": "NULL_VALUE",
|
||||
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
|
||||
},
|
||||
"protobufStruct": {
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Unordered map of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
|
||||
},
|
||||
"protobufValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"null_value": {
|
||||
"$ref": "#/definitions/protobufNullValue",
|
||||
"description": "Represents a null value."
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The status code, which should be an enum value of\n[google.rpc.Code][google.rpc.Code]."
|
||||
},
|
||||
"number_value": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Represents a double value."
|
||||
},
|
||||
"string_value": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Represents a string value."
|
||||
"description": "A developer-facing error message, which should be in English. Any\nuser-facing error message should be localized and sent in the\n[google.rpc.Status.details][google.rpc.Status.details] field, or localized\nby the client."
|
||||
},
|
||||
"bool_value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"description": "Represents a boolean value."
|
||||
},
|
||||
"struct_value": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"description": "Represents a structured value."
|
||||
},
|
||||
"list_value": {
|
||||
"$ref": "#/definitions/protobufListValue",
|
||||
"description": "Represents a repeated `Value`."
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
},
|
||||
"description": "A list of messages that carry the error details. There is a common set of\nmessage types for APIs to use."
|
||||
}
|
||||
},
|
||||
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
|
||||
"description": "- Simple to use and understand for most users\n- Flexible enough to meet unexpected needs\n\n# Overview\n\nThe `Status` message contains three pieces of data: error code, error\nmessage, and error details. The error code should be an enum value of\n[google.rpc.Code][google.rpc.Code], but it may accept additional error codes\nif needed. The error message should be a developer-facing English message\nthat helps developers *understand* and *resolve* the error. If a localized\nuser-facing error message is needed, put the localized message in the error\ndetails or localize it in the client. The optional error details may contain\narbitrary information about the error. There is a predefined set of error\ndetail types in the package `google.rpc` that can be used for common error\nconditions.\n\n# Language mapping\n\nThe `Status` message is the logical representation of the error model, but it\nis not necessarily the actual wire format. When the `Status` message is\nexposed in different client libraries and different wire protocols, it can be\nmapped differently. For example, it will likely be mapped to some exceptions\nin Java, but more likely mapped to some error codes in C.\n\n# Other uses\n\nThe error model and the `Status` message can be used in a variety of\nenvironments, either with or without APIs, to provide a\nconsistent developer experience across different environments.\n\nExample uses of this error model include:\n\n- Partial errors. If a service needs to return partial errors to the client,\n it may embed the `Status` in the normal response to indicate the partial\n errors.\n\n- Workflow errors. A typical workflow has multiple steps. Each step may\n have a `Status` message for error reporting.\n\n- Batch operations. If a client uses batch request and batch response, the\n `Status` message should be used directly inside batch response, one for\n each error sub-response.\n\n- Asynchronous operations. If an API call embeds asynchronous operation\n results in its response, the status of those operations should be\n represented directly using the `Status` message.\n\n- Logging. If some API errors are stored in logs, the message `Status` could\n be used directly after any stripping needed for security/privacy reasons.",
|
||||
"title": "The `Status` type defines a logical error model that is suitable for\ndifferent programming environments, including REST APIs and RPC APIs. It is\nused by [gRPC](https://github.com/grpc). The error model is designed to be:"
|
||||
},
|
||||
"runtimeStreamError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"grpc_code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"http_code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"http_status": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-stream-definitions": {
|
||||
"openmatchEvaluateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/openmatchEvaluateResponse"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/definitions/runtimeStreamError"
|
||||
}
|
||||
},
|
||||
"title": "Stream result of openmatchEvaluateResponse"
|
||||
}
|
||||
},
|
||||
"externalDocs": {
|
||||
|
@ -13,8 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
package api;
|
||||
option go_package = "internal/pb";
|
||||
package openmatch;
|
||||
option go_package = "open-match.dev/open-match/pkg/pb";
|
||||
option csharp_namespace = "OpenMatch";
|
||||
|
||||
import "api/messages.proto";
|
||||
import "google/api/annotations.proto";
|
||||
@ -102,8 +103,10 @@ service Frontend {
|
||||
}
|
||||
|
||||
// DeleteTicket removes the Ticket from state storage and from corresponding
|
||||
// configured indices. Deleting the ticket stops the ticket from being
|
||||
// considered for future matchmaking requests.
|
||||
// configured indices and lazily removes the ticket from state storage.
|
||||
// Deleting a ticket immediately stops the ticket from being
|
||||
// considered for future matchmaking requests, yet when the ticket itself will be deleted
|
||||
// is undeterministic. Users may still be able to assign/get a ticket after calling DeleteTicket on it.
|
||||
rpc DeleteTicket(DeleteTicketRequest) returns (DeleteTicketResponse) {
|
||||
option (google.api.http) = {
|
||||
delete: "/v1/frontend/tickets/{ticket_id}"
|
||||
|
@ -32,7 +32,7 @@
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiCreateTicketResponse"
|
||||
"$ref": "#/definitions/openmatchCreateTicketResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -48,7 +48,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiCreateTicketRequest"
|
||||
"$ref": "#/definitions/openmatchCreateTicketRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -65,7 +65,7 @@
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiTicket"
|
||||
"$ref": "#/definitions/openmatchTicket"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -89,13 +89,13 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"summary": "DeleteTicket removes the Ticket from state storage and from corresponding\nconfigured indices. Deleting the ticket stops the ticket from being\nconsidered for future matchmaking requests.",
|
||||
"summary": "DeleteTicket removes the Ticket from state storage and from corresponding\nconfigured indices and lazily removes the ticket from state storage.\nDeleting a ticket immediately stops the ticket from being\nconsidered for future matchmaking requests, yet when the ticket itself will be deleted\nis undeterministic. Users may still be able to assign/get a ticket after calling DeleteTicket on it.",
|
||||
"operationId": "DeleteTicket",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiDeleteTicketResponse"
|
||||
"$ref": "#/definitions/openmatchDeleteTicketResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -127,7 +127,7 @@
|
||||
"200": {
|
||||
"description": "A successful response.(streaming responses)",
|
||||
"schema": {
|
||||
"$ref": "#/x-stream-definitions/apiGetAssignmentsResponse"
|
||||
"$ref": "#/x-stream-definitions/openmatchGetAssignmentsResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -153,7 +153,7 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"apiAssignment": {
|
||||
"openmatchAssignment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connection": {
|
||||
@ -161,47 +161,47 @@
|
||||
"description": "Connection information for this Assignment."
|
||||
},
|
||||
"properties": {
|
||||
"type": "string",
|
||||
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
|
||||
"type": "object",
|
||||
"description": "Other details to be sent to the players."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"$ref": "#/definitions/rpcStatus",
|
||||
"description": "Error when finding an Assignment for this Ticket."
|
||||
}
|
||||
},
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket."
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket. Open\nmatch does not require or inspect any fields on assignment."
|
||||
},
|
||||
"apiCreateTicketRequest": {
|
||||
"openmatchCreateTicketRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticket": {
|
||||
"$ref": "#/definitions/apiTicket",
|
||||
"$ref": "#/definitions/openmatchTicket",
|
||||
"description": "Ticket object with the properties of the Ticket to be created."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiCreateTicketResponse": {
|
||||
"openmatchCreateTicketResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticket": {
|
||||
"$ref": "#/definitions/apiTicket",
|
||||
"$ref": "#/definitions/openmatchTicket",
|
||||
"description": "Ticket object for the created Ticket - with the ticket ID populated."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiDeleteTicketResponse": {
|
||||
"openmatchDeleteTicketResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"apiGetAssignmentsResponse": {
|
||||
"openmatchGetAssignmentsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assignment": {
|
||||
"$ref": "#/definitions/apiAssignment",
|
||||
"$ref": "#/definitions/openmatchAssignment",
|
||||
"description": "The updated Ticket object."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiTicket": {
|
||||
"openmatchTicket": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@ -209,11 +209,11 @@
|
||||
"description": "The Ticket ID generated by Open Match."
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
|
||||
},
|
||||
"assignment": {
|
||||
"$ref": "#/definitions/apiAssignment",
|
||||
"$ref": "#/definitions/openmatchAssignment",
|
||||
"description": "Assignment associated with the Ticket."
|
||||
}
|
||||
},
|
||||
@ -234,19 +234,6 @@
|
||||
},
|
||||
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
|
||||
},
|
||||
"protobufListValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Repeated field of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
|
||||
},
|
||||
"protobufNullValue": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@ -255,50 +242,28 @@
|
||||
"default": "NULL_VALUE",
|
||||
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
|
||||
},
|
||||
"protobufStruct": {
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Unordered map of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
|
||||
},
|
||||
"protobufValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"null_value": {
|
||||
"$ref": "#/definitions/protobufNullValue",
|
||||
"description": "Represents a null value."
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The status code, which should be an enum value of\n[google.rpc.Code][google.rpc.Code]."
|
||||
},
|
||||
"number_value": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Represents a double value."
|
||||
},
|
||||
"string_value": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Represents a string value."
|
||||
"description": "A developer-facing error message, which should be in English. Any\nuser-facing error message should be localized and sent in the\n[google.rpc.Status.details][google.rpc.Status.details] field, or localized\nby the client."
|
||||
},
|
||||
"bool_value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"description": "Represents a boolean value."
|
||||
},
|
||||
"struct_value": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"description": "Represents a structured value."
|
||||
},
|
||||
"list_value": {
|
||||
"$ref": "#/definitions/protobufListValue",
|
||||
"description": "Represents a repeated `Value`."
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
},
|
||||
"description": "A list of messages that carry the error details. There is a common set of\nmessage types for APIs to use."
|
||||
}
|
||||
},
|
||||
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
|
||||
"description": "- Simple to use and understand for most users\n- Flexible enough to meet unexpected needs\n\n# Overview\n\nThe `Status` message contains three pieces of data: error code, error\nmessage, and error details. The error code should be an enum value of\n[google.rpc.Code][google.rpc.Code], but it may accept additional error codes\nif needed. The error message should be a developer-facing English message\nthat helps developers *understand* and *resolve* the error. If a localized\nuser-facing error message is needed, put the localized message in the error\ndetails or localize it in the client. The optional error details may contain\narbitrary information about the error. There is a predefined set of error\ndetail types in the package `google.rpc` that can be used for common error\nconditions.\n\n# Language mapping\n\nThe `Status` message is the logical representation of the error model, but it\nis not necessarily the actual wire format. When the `Status` message is\nexposed in different client libraries and different wire protocols, it can be\nmapped differently. For example, it will likely be mapped to some exceptions\nin Java, but more likely mapped to some error codes in C.\n\n# Other uses\n\nThe error model and the `Status` message can be used in a variety of\nenvironments, either with or without APIs, to provide a\nconsistent developer experience across different environments.\n\nExample uses of this error model include:\n\n- Partial errors. If a service needs to return partial errors to the client,\n it may embed the `Status` in the normal response to indicate the partial\n errors.\n\n- Workflow errors. A typical workflow has multiple steps. Each step may\n have a `Status` message for error reporting.\n\n- Batch operations. If a client uses batch request and batch response, the\n `Status` message should be used directly inside batch response, one for\n each error sub-response.\n\n- Asynchronous operations. If an API call embeds asynchronous operation\n results in its response, the status of those operations should be\n represented directly using the `Status` message.\n\n- Logging. If some API errors are stored in logs, the message `Status` could\n be used directly after any stripping needed for security/privacy reasons.",
|
||||
"title": "The `Status` type defines a logical error model that is suitable for\ndifferent programming environments, including REST APIs and RPC APIs. It is\nused by [gRPC](https://github.com/grpc). The error model is designed to be:"
|
||||
},
|
||||
"runtimeStreamError": {
|
||||
"type": "object",
|
||||
@ -327,17 +292,17 @@
|
||||
}
|
||||
},
|
||||
"x-stream-definitions": {
|
||||
"apiGetAssignmentsResponse": {
|
||||
"openmatchGetAssignmentsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/apiGetAssignmentsResponse"
|
||||
"$ref": "#/definitions/openmatchGetAssignmentsResponse"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/definitions/runtimeStreamError"
|
||||
}
|
||||
},
|
||||
"title": "Stream result of apiGetAssignmentsResponse"
|
||||
"title": "Stream result of openmatchGetAssignmentsResponse"
|
||||
}
|
||||
},
|
||||
"externalDocs": {
|
||||
|
@ -13,8 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
package api;
|
||||
option go_package = "internal/pb";
|
||||
package openmatch;
|
||||
option go_package = "open-match.dev/open-match/pkg/pb";
|
||||
option csharp_namespace = "OpenMatch";
|
||||
|
||||
import "api/messages.proto";
|
||||
import "google/api/annotations.proto";
|
||||
@ -62,7 +63,8 @@ message RunRequest {
|
||||
|
||||
message RunResponse {
|
||||
// The proposal generated by this MatchFunction Run.
|
||||
repeated Match proposal = 1;
|
||||
// Note that OpenMatch will validate the proposals, a valid match should contain at least one ticket.
|
||||
Match proposal = 1;
|
||||
}
|
||||
|
||||
// This proto defines the API for running Match Functions as long-lived,
|
||||
@ -70,7 +72,7 @@ message RunResponse {
|
||||
service MatchFunction {
|
||||
// This is the function that is executed when by the Open Match backend to
|
||||
// generate Match proposals.
|
||||
rpc Run(RunRequest) returns (RunResponse) {
|
||||
rpc Run(RunRequest) returns (stream RunResponse) {
|
||||
option (google.api.http) = {
|
||||
post: "/v1/matchfunction:run"
|
||||
body: "*"
|
||||
|
@ -30,9 +30,9 @@
|
||||
"operationId": "Run",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"description": "A successful response.(streaming responses)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiRunResponse"
|
||||
"$ref": "#/x-stream-definitions/openmatchRunResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -48,7 +48,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiRunRequest"
|
||||
"$ref": "#/definitions/openmatchRunRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -59,7 +59,7 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"apiAssignment": {
|
||||
"openmatchAssignment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connection": {
|
||||
@ -67,17 +67,30 @@
|
||||
"description": "Connection information for this Assignment."
|
||||
},
|
||||
"properties": {
|
||||
"type": "string",
|
||||
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
|
||||
"type": "object",
|
||||
"description": "Other details to be sent to the players."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"$ref": "#/definitions/rpcStatus",
|
||||
"description": "Error when finding an Assignment for this Ticket."
|
||||
}
|
||||
},
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket."
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket. Open\nmatch does not require or inspect any fields on assignment."
|
||||
},
|
||||
"apiFilter": {
|
||||
"openmatchBoolEqualsFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "Filters boolean values.\n attribute: \"foo\"\n value: false\nmatches:\n {\"foo\": false}\ndoes not match:\n {\"foo\": true}\n {\"foo\": \"bar\"}\n {\"foo\": 1}\n {\"foo\": \"false\"}\n {\"foo\": [false]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"openmatchFloatRangeFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
@ -95,9 +108,9 @@
|
||||
"description": "Minimum value. Defaults to 0."
|
||||
}
|
||||
},
|
||||
"description": "A hard filter used to query a subset of Tickets meeting the filtering\ncriteria."
|
||||
"title": "Filters numerical values to only those within a range.\n attribute: \"foo\"\n max: 10\n min: 5\nmatches:\n {\"foo\": 5}\n {\"foo\": 7.5}\n {\"foo\": 10}\ndoes not match:\n {\"foo\": 4}\n {\"foo\": 10.01}\n {\"foo\": \"7.5\"}\n {\"foo\": true}\n {\"foo\": [7.5]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"apiMatch": {
|
||||
"openmatchMatch": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"match_id": {
|
||||
@ -112,28 +125,28 @@
|
||||
"type": "string",
|
||||
"description": "Name of the match function that generated this Match."
|
||||
},
|
||||
"ticket": {
|
||||
"tickets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiTicket"
|
||||
"$ref": "#/definitions/openmatchTicket"
|
||||
},
|
||||
"description": "Tickets belonging to this match."
|
||||
},
|
||||
"roster": {
|
||||
"rosters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiRoster"
|
||||
"$ref": "#/definitions/openmatchRoster"
|
||||
},
|
||||
"title": "Set of Rosters that comprise this Match"
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Match properties for this Match. Open Match does not interpret this field."
|
||||
}
|
||||
},
|
||||
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call."
|
||||
"description": "A Match is used to represent a completed match object. It can be generated by\na MatchFunction as a proposal or can be returned by OpenMatch as a result in\nresponse to the FetchMatches call.\nWhen a match is returned by the FetchMatches call, it should contain at least \none ticket to be considered as valid."
|
||||
},
|
||||
"apiMatchProfile": {
|
||||
"openmatchMatchProfile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
@ -141,50 +154,62 @@
|
||||
"description": "Name of this match profile."
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Set of properties associated with this MatchProfile. (Optional)\nOpen Match does not interpret these properties but passes them through to\nthe MatchFunction."
|
||||
},
|
||||
"pool": {
|
||||
"pools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiPool"
|
||||
"$ref": "#/definitions/openmatchPool"
|
||||
},
|
||||
"description": "Set of pools to be queried when generating a match for this MatchProfile.\nThe pool names can be used in empty Rosters to specify composition of a\nmatch."
|
||||
},
|
||||
"roster": {
|
||||
"rosters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiRoster"
|
||||
"$ref": "#/definitions/openmatchRoster"
|
||||
},
|
||||
"description": "Set of Rosters for this match request. Could be empty Rosters used to\nindicate the composition of the generated Match or they could be partially\npre-populated Ticket list to be used in scenarios such as backfill / join\nin progress."
|
||||
}
|
||||
},
|
||||
"description": "A MatchProfile is Open Match's representation of a Match specification. It is\nused to indicate the criteria for selecting players for a match. A\nMatchProfile is the input to the API to get matches and is passed to the\nMatchFunction. It contains all the information required by the MatchFunction\nto generate match proposals."
|
||||
},
|
||||
"apiPool": {
|
||||
"openmatchPool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A developer-chosen human-readable name for this Pool."
|
||||
},
|
||||
"filter": {
|
||||
"float_range_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiFilter"
|
||||
"$ref": "#/definitions/openmatchFloatRangeFilter"
|
||||
},
|
||||
"description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
|
||||
},
|
||||
"bool_equals_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/openmatchBoolEqualsFilter"
|
||||
}
|
||||
},
|
||||
"string_equals_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/openmatchStringEqualsFilter"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiRoster": {
|
||||
"openmatchRoster": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A developer-chosen human-readable name for this Roster."
|
||||
},
|
||||
"ticket_id": {
|
||||
"ticket_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@ -194,28 +219,37 @@
|
||||
},
|
||||
"description": "A Roster is a named collection of Ticket IDs. It exists so that a Tickets\nassociated with a Match can be labelled to belong to a team, sub-team etc. It\ncan also be used to represent the current state of a Match in scenarios such\nas backfill, join-in-progress etc."
|
||||
},
|
||||
"apiRunRequest": {
|
||||
"openmatchRunRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile": {
|
||||
"$ref": "#/definitions/apiMatchProfile",
|
||||
"$ref": "#/definitions/openmatchMatchProfile",
|
||||
"description": "The MatchProfile that describes the Match that this MatchFunction needs to\ngenerate proposals for."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiRunResponse": {
|
||||
"openmatchRunResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"proposal": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiMatch"
|
||||
},
|
||||
"description": "The proposal generated by this MatchFunction Run."
|
||||
"$ref": "#/definitions/openmatchMatch",
|
||||
"description": "The proposal generated by this MatchFunction Run.\nNote that OpenMatch will validate the proposals, a valid match should contain at least one ticket."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiTicket": {
|
||||
"openmatchStringEqualsFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "Filters strings exactly equaling a value.\n attribute: \"foo\"\n value: \"bar\"\nmatches:\n {\"foo\": \"bar\"}\ndoes not match:\n {\"foo\": \"baz\"}\n {\"foo\": true}\n {\"foo\": 5}\n {\"foo\": [\"bar\"]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"openmatchTicket": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@ -223,28 +257,30 @@
|
||||
"description": "The Ticket ID generated by Open Match."
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
|
||||
},
|
||||
"assignment": {
|
||||
"$ref": "#/definitions/apiAssignment",
|
||||
"$ref": "#/definitions/openmatchAssignment",
|
||||
"description": "Assignment associated with the Ticket."
|
||||
}
|
||||
},
|
||||
"description": "A Ticket is a basic matchmaking entity in Open Match. In order to enter\nmatchmaking using Open Match, the client should generate a Ticket, passing in\nthe properties to be associated with this Ticket. Open Match will generate an\nID for a Ticket during creation. A Ticket could be used to represent an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof properties. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
|
||||
},
|
||||
"protobufListValue": {
|
||||
"protobufAny": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Repeated field of dynamically typed values."
|
||||
"type_url": {
|
||||
"type": "string",
|
||||
"description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"format": "byte",
|
||||
"description": "Must be a valid serialized protocol buffer of the above specified type."
|
||||
}
|
||||
},
|
||||
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
|
||||
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
|
||||
},
|
||||
"protobufNullValue": {
|
||||
"type": "string",
|
||||
@ -254,50 +290,67 @@
|
||||
"default": "NULL_VALUE",
|
||||
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
|
||||
},
|
||||
"protobufStruct": {
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Unordered map of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
|
||||
},
|
||||
"protobufValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"null_value": {
|
||||
"$ref": "#/definitions/protobufNullValue",
|
||||
"description": "Represents a null value."
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The status code, which should be an enum value of\n[google.rpc.Code][google.rpc.Code]."
|
||||
},
|
||||
"number_value": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Represents a double value."
|
||||
},
|
||||
"string_value": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Represents a string value."
|
||||
"description": "A developer-facing error message, which should be in English. Any\nuser-facing error message should be localized and sent in the\n[google.rpc.Status.details][google.rpc.Status.details] field, or localized\nby the client."
|
||||
},
|
||||
"bool_value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"description": "Represents a boolean value."
|
||||
},
|
||||
"struct_value": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"description": "Represents a structured value."
|
||||
},
|
||||
"list_value": {
|
||||
"$ref": "#/definitions/protobufListValue",
|
||||
"description": "Represents a repeated `Value`."
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
},
|
||||
"description": "A list of messages that carry the error details. There is a common set of\nmessage types for APIs to use."
|
||||
}
|
||||
},
|
||||
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
|
||||
"description": "- Simple to use and understand for most users\n- Flexible enough to meet unexpected needs\n\n# Overview\n\nThe `Status` message contains three pieces of data: error code, error\nmessage, and error details. The error code should be an enum value of\n[google.rpc.Code][google.rpc.Code], but it may accept additional error codes\nif needed. The error message should be a developer-facing English message\nthat helps developers *understand* and *resolve* the error. If a localized\nuser-facing error message is needed, put the localized message in the error\ndetails or localize it in the client. The optional error details may contain\narbitrary information about the error. There is a predefined set of error\ndetail types in the package `google.rpc` that can be used for common error\nconditions.\n\n# Language mapping\n\nThe `Status` message is the logical representation of the error model, but it\nis not necessarily the actual wire format. When the `Status` message is\nexposed in different client libraries and different wire protocols, it can be\nmapped differently. For example, it will likely be mapped to some exceptions\nin Java, but more likely mapped to some error codes in C.\n\n# Other uses\n\nThe error model and the `Status` message can be used in a variety of\nenvironments, either with or without APIs, to provide a\nconsistent developer experience across different environments.\n\nExample uses of this error model include:\n\n- Partial errors. If a service needs to return partial errors to the client,\n it may embed the `Status` in the normal response to indicate the partial\n errors.\n\n- Workflow errors. A typical workflow has multiple steps. Each step may\n have a `Status` message for error reporting.\n\n- Batch operations. If a client uses batch request and batch response, the\n `Status` message should be used directly inside batch response, one for\n each error sub-response.\n\n- Asynchronous operations. If an API call embeds asynchronous operation\n results in its response, the status of those operations should be\n represented directly using the `Status` message.\n\n- Logging. If some API errors are stored in logs, the message `Status` could\n be used directly after any stripping needed for security/privacy reasons.",
|
||||
"title": "The `Status` type defines a logical error model that is suitable for\ndifferent programming environments, including REST APIs and RPC APIs. It is\nused by [gRPC](https://github.com/grpc). The error model is designed to be:"
|
||||
},
|
||||
"runtimeStreamError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"grpc_code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"http_code": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"http_status": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-stream-definitions": {
|
||||
"openmatchRunResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/openmatchRunResponse"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/definitions/runtimeStreamError"
|
||||
}
|
||||
},
|
||||
"title": "Stream result of openmatchRunResponse"
|
||||
}
|
||||
},
|
||||
"externalDocs": {
|
||||
|
@ -13,9 +13,11 @@
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
package api;
|
||||
option go_package = "internal/pb";
|
||||
package openmatch;
|
||||
option go_package = "open-match.dev/open-match/pkg/pb";
|
||||
option csharp_namespace = "OpenMatch";
|
||||
|
||||
import "google/rpc/status.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
// A Ticket is a basic matchmaking entity in Open Match. In order to enter
|
||||
@ -38,22 +40,36 @@ message Ticket {
|
||||
Assignment assignment = 3;
|
||||
}
|
||||
|
||||
// An Assignment object represents the assignment associated with a Ticket.
|
||||
// An Assignment object represents the assignment associated with a Ticket. Open
|
||||
// match does not require or inspect any fields on assignment.
|
||||
message Assignment {
|
||||
// Connection information for this Assignment.
|
||||
string connection = 1;
|
||||
|
||||
// Other details to be sent to the players. (Optional)
|
||||
// Open Match does not interpret these properties.
|
||||
string properties = 2;
|
||||
// Other details to be sent to the players.
|
||||
google.protobuf.Struct properties = 2;
|
||||
|
||||
// Error when finding an Assignment for this Ticket.
|
||||
string error = 3;
|
||||
google.rpc.Status error = 3;
|
||||
}
|
||||
|
||||
// A hard filter used to query a subset of Tickets meeting the filtering
|
||||
// criteria.
|
||||
message Filter {
|
||||
// Filters numerical values to only those within a range.
|
||||
// attribute: "foo"
|
||||
// max: 10
|
||||
// min: 5
|
||||
// matches:
|
||||
// {"foo": 5}
|
||||
// {"foo": 7.5}
|
||||
// {"foo": 10}
|
||||
// does not match:
|
||||
// {"foo": 4}
|
||||
// {"foo": 10.01}
|
||||
// {"foo": "7.5"}
|
||||
// {"foo": true}
|
||||
// {"foo": [7.5]}
|
||||
// {"foo": null}
|
||||
// {}
|
||||
message FloatRangeFilter {
|
||||
// Name of the ticket attribute this Filter operates on.
|
||||
string attribute = 1;
|
||||
|
||||
@ -64,13 +80,54 @@ message Filter {
|
||||
double min = 3;
|
||||
}
|
||||
|
||||
// Filters boolean values.
|
||||
// attribute: "foo"
|
||||
// value: false
|
||||
// matches:
|
||||
// {"foo": false}
|
||||
// does not match:
|
||||
// {"foo": true}
|
||||
// {"foo": "bar"}
|
||||
// {"foo": 1}
|
||||
// {"foo": "false"}
|
||||
// {"foo": [false]}
|
||||
// {"foo": null}
|
||||
// {}
|
||||
message BoolEqualsFilter {
|
||||
string attribute = 1;
|
||||
|
||||
bool value = 2;
|
||||
}
|
||||
|
||||
// Filters strings exactly equaling a value.
|
||||
// attribute: "foo"
|
||||
// value: "bar"
|
||||
// matches:
|
||||
// {"foo": "bar"}
|
||||
// does not match:
|
||||
// {"foo": "baz"}
|
||||
// {"foo": true}
|
||||
// {"foo": 5}
|
||||
// {"foo": ["bar"]}
|
||||
// {"foo": null}
|
||||
// {}
|
||||
message StringEqualsFilter {
|
||||
string attribute = 1;
|
||||
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message Pool {
|
||||
// A developer-chosen human-readable name for this Pool.
|
||||
string name = 1;
|
||||
|
||||
// Set of Filters indicating the filtering criteria. Selected players must
|
||||
// match every Filter.
|
||||
repeated Filter filter = 2;
|
||||
repeated FloatRangeFilter float_range_filters = 2;
|
||||
|
||||
repeated BoolEqualsFilter bool_equals_filters = 3;
|
||||
|
||||
repeated StringEqualsFilter string_equals_filters = 4;
|
||||
}
|
||||
|
||||
// A Roster is a named collection of Ticket IDs. It exists so that a Tickets
|
||||
@ -82,7 +139,7 @@ message Roster {
|
||||
string name = 1;
|
||||
|
||||
// Tickets belonging to this Roster.
|
||||
repeated string ticket_id = 2;
|
||||
repeated string ticket_ids = 2;
|
||||
}
|
||||
|
||||
// A MatchProfile is Open Match's representation of a Match specification. It is
|
||||
@ -102,18 +159,20 @@ message MatchProfile {
|
||||
// Set of pools to be queried when generating a match for this MatchProfile.
|
||||
// The pool names can be used in empty Rosters to specify composition of a
|
||||
// match.
|
||||
repeated Pool pool = 3;
|
||||
repeated Pool pools = 3;
|
||||
|
||||
// Set of Rosters for this match request. Could be empty Rosters used to
|
||||
// indicate the composition of the generated Match or they could be partially
|
||||
// pre-populated Ticket list to be used in scenarios such as backfill / join
|
||||
// in progress.
|
||||
repeated Roster roster = 4;
|
||||
repeated Roster rosters = 4;
|
||||
}
|
||||
|
||||
// A Match is used to represent a completed match object. It can be generated by
|
||||
// a MatchFunction as a proposal or can be returned by OpenMatch as a result in
|
||||
// response to the FetchMatches call.
|
||||
// When a match is returned by the FetchMatches call, it should contain at least
|
||||
// one ticket to be considered as valid.
|
||||
message Match {
|
||||
// A Match ID that should be passed through the stack for tracing.
|
||||
string match_id = 1;
|
||||
@ -125,10 +184,10 @@ message Match {
|
||||
string match_function = 3;
|
||||
|
||||
// Tickets belonging to this match.
|
||||
repeated Ticket ticket = 4;
|
||||
repeated Ticket tickets = 4;
|
||||
|
||||
// Set of Rosters that comprise this Match
|
||||
repeated Roster roster = 5;
|
||||
repeated Roster rosters = 5;
|
||||
|
||||
// Match properties for this Match. Open Match does not interpret this field.
|
||||
google.protobuf.Struct properties = 6;
|
||||
|
@ -13,8 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
package api;
|
||||
option go_package = "internal/pb";
|
||||
package openmatch;
|
||||
option go_package = "open-match.dev/open-match/pkg/pb";
|
||||
option csharp_namespace = "OpenMatch";
|
||||
|
||||
import "api/messages.proto";
|
||||
import "google/api/annotations.proto";
|
||||
@ -61,7 +62,7 @@ message QueryTicketsRequest {
|
||||
|
||||
message QueryTicketsResponse {
|
||||
// The Tickets that meet the Filter criteria requested by the Pool.
|
||||
repeated Ticket ticket = 1;
|
||||
repeated Ticket tickets = 1;
|
||||
}
|
||||
|
||||
// The MMLogic API provides utility functions for common MMF functionality such
|
||||
|
@ -32,7 +32,7 @@
|
||||
"200": {
|
||||
"description": "A successful response.(streaming responses)",
|
||||
"schema": {
|
||||
"$ref": "#/x-stream-definitions/apiQueryTicketsResponse"
|
||||
"$ref": "#/x-stream-definitions/openmatchQueryTicketsResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
@ -48,7 +48,7 @@
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/apiQueryTicketsRequest"
|
||||
"$ref": "#/definitions/openmatchQueryTicketsRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -59,7 +59,7 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"apiAssignment": {
|
||||
"openmatchAssignment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connection": {
|
||||
@ -67,17 +67,30 @@
|
||||
"description": "Connection information for this Assignment."
|
||||
},
|
||||
"properties": {
|
||||
"type": "string",
|
||||
"description": "Other details to be sent to the players. (Optional)\nOpen Match does not interpret these properties."
|
||||
"type": "object",
|
||||
"description": "Other details to be sent to the players."
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"$ref": "#/definitions/rpcStatus",
|
||||
"description": "Error when finding an Assignment for this Ticket."
|
||||
}
|
||||
},
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket."
|
||||
"description": "An Assignment object represents the assignment associated with a Ticket. Open\nmatch does not require or inspect any fields on assignment."
|
||||
},
|
||||
"apiFilter": {
|
||||
"openmatchBoolEqualsFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "Filters boolean values.\n attribute: \"foo\"\n value: false\nmatches:\n {\"foo\": false}\ndoes not match:\n {\"foo\": true}\n {\"foo\": \"bar\"}\n {\"foo\": 1}\n {\"foo\": \"false\"}\n {\"foo\": [false]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"openmatchFloatRangeFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
@ -95,46 +108,70 @@
|
||||
"description": "Minimum value. Defaults to 0."
|
||||
}
|
||||
},
|
||||
"description": "A hard filter used to query a subset of Tickets meeting the filtering\ncriteria."
|
||||
"title": "Filters numerical values to only those within a range.\n attribute: \"foo\"\n max: 10\n min: 5\nmatches:\n {\"foo\": 5}\n {\"foo\": 7.5}\n {\"foo\": 10}\ndoes not match:\n {\"foo\": 4}\n {\"foo\": 10.01}\n {\"foo\": \"7.5\"}\n {\"foo\": true}\n {\"foo\": [7.5]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"apiPool": {
|
||||
"openmatchPool": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "A developer-chosen human-readable name for this Pool."
|
||||
},
|
||||
"filter": {
|
||||
"float_range_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiFilter"
|
||||
"$ref": "#/definitions/openmatchFloatRangeFilter"
|
||||
},
|
||||
"description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
|
||||
},
|
||||
"bool_equals_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/openmatchBoolEqualsFilter"
|
||||
}
|
||||
},
|
||||
"string_equals_filters": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/openmatchStringEqualsFilter"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiQueryTicketsRequest": {
|
||||
"openmatchQueryTicketsRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pool": {
|
||||
"$ref": "#/definitions/apiPool",
|
||||
"$ref": "#/definitions/openmatchPool",
|
||||
"description": "The Pool representing the set of Filters to be queried."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiQueryTicketsResponse": {
|
||||
"openmatchQueryTicketsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ticket": {
|
||||
"tickets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apiTicket"
|
||||
"$ref": "#/definitions/openmatchTicket"
|
||||
},
|
||||
"description": "The Tickets that meet the Filter criteria requested by the Pool."
|
||||
}
|
||||
}
|
||||
},
|
||||
"apiTicket": {
|
||||
"openmatchStringEqualsFilter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "Filters strings exactly equaling a value.\n attribute: \"foo\"\n value: \"bar\"\nmatches:\n {\"foo\": \"bar\"}\ndoes not match:\n {\"foo\": \"baz\"}\n {\"foo\": true}\n {\"foo\": 5}\n {\"foo\": [\"bar\"]}\n {\"foo\": null}\n {}"
|
||||
},
|
||||
"openmatchTicket": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
@ -142,11 +179,11 @@
|
||||
"description": "The Ticket ID generated by Open Match."
|
||||
},
|
||||
"properties": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"type": "object",
|
||||
"description": "Properties contains custom info about the ticket. Top level values can be\nused in indexing and filtering to find tickets."
|
||||
},
|
||||
"assignment": {
|
||||
"$ref": "#/definitions/apiAssignment",
|
||||
"$ref": "#/definitions/openmatchAssignment",
|
||||
"description": "Assignment associated with the Ticket."
|
||||
}
|
||||
},
|
||||
@ -167,19 +204,6 @@
|
||||
},
|
||||
"description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := ptypes.MarshalAny(foo)\n ...\n foo := \u0026pb.Foo{}\n if err := ptypes.UnmarshalAny(any, foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }"
|
||||
},
|
||||
"protobufListValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Repeated field of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`ListValue` is a wrapper around a repeated field of values.\n\nThe JSON representation for `ListValue` is JSON array."
|
||||
},
|
||||
"protobufNullValue": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@ -188,50 +212,28 @@
|
||||
"default": "NULL_VALUE",
|
||||
"description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
|
||||
},
|
||||
"protobufStruct": {
|
||||
"rpcStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/protobufValue"
|
||||
},
|
||||
"description": "Unordered map of dynamically typed values."
|
||||
}
|
||||
},
|
||||
"description": "`Struct` represents a structured data value, consisting of fields\nwhich map to dynamically typed values. In some languages, `Struct`\nmight be supported by a native representation. For example, in\nscripting languages like JS a struct is represented as an\nobject. The details of that representation are described together\nwith the proto support for the language.\n\nThe JSON representation for `Struct` is JSON object."
|
||||
},
|
||||
"protobufValue": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"null_value": {
|
||||
"$ref": "#/definitions/protobufNullValue",
|
||||
"description": "Represents a null value."
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The status code, which should be an enum value of\n[google.rpc.Code][google.rpc.Code]."
|
||||
},
|
||||
"number_value": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"description": "Represents a double value."
|
||||
},
|
||||
"string_value": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Represents a string value."
|
||||
"description": "A developer-facing error message, which should be in English. Any\nuser-facing error message should be localized and sent in the\n[google.rpc.Status.details][google.rpc.Status.details] field, or localized\nby the client."
|
||||
},
|
||||
"bool_value": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"description": "Represents a boolean value."
|
||||
},
|
||||
"struct_value": {
|
||||
"$ref": "#/definitions/protobufStruct",
|
||||
"description": "Represents a structured value."
|
||||
},
|
||||
"list_value": {
|
||||
"$ref": "#/definitions/protobufListValue",
|
||||
"description": "Represents a repeated `Value`."
|
||||
"details": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/protobufAny"
|
||||
},
|
||||
"description": "A list of messages that carry the error details. There is a common set of\nmessage types for APIs to use."
|
||||
}
|
||||
},
|
||||
"description": "`Value` represents a dynamically typed value which can be either\nnull, a number, a string, a boolean, a recursive struct value, or a\nlist of values. A producer of value is expected to set one of that\nvariants, absence of any variant indicates an error.\n\nThe JSON representation for `Value` is JSON value."
|
||||
"description": "- Simple to use and understand for most users\n- Flexible enough to meet unexpected needs\n\n# Overview\n\nThe `Status` message contains three pieces of data: error code, error\nmessage, and error details. The error code should be an enum value of\n[google.rpc.Code][google.rpc.Code], but it may accept additional error codes\nif needed. The error message should be a developer-facing English message\nthat helps developers *understand* and *resolve* the error. If a localized\nuser-facing error message is needed, put the localized message in the error\ndetails or localize it in the client. The optional error details may contain\narbitrary information about the error. There is a predefined set of error\ndetail types in the package `google.rpc` that can be used for common error\nconditions.\n\n# Language mapping\n\nThe `Status` message is the logical representation of the error model, but it\nis not necessarily the actual wire format. When the `Status` message is\nexposed in different client libraries and different wire protocols, it can be\nmapped differently. For example, it will likely be mapped to some exceptions\nin Java, but more likely mapped to some error codes in C.\n\n# Other uses\n\nThe error model and the `Status` message can be used in a variety of\nenvironments, either with or without APIs, to provide a\nconsistent developer experience across different environments.\n\nExample uses of this error model include:\n\n- Partial errors. If a service needs to return partial errors to the client,\n it may embed the `Status` in the normal response to indicate the partial\n errors.\n\n- Workflow errors. A typical workflow has multiple steps. Each step may\n have a `Status` message for error reporting.\n\n- Batch operations. If a client uses batch request and batch response, the\n `Status` message should be used directly inside batch response, one for\n each error sub-response.\n\n- Asynchronous operations. If an API call embeds asynchronous operation\n results in its response, the status of those operations should be\n represented directly using the `Status` message.\n\n- Logging. If some API errors are stored in logs, the message `Status` could\n be used directly after any stripping needed for security/privacy reasons.",
|
||||
"title": "The `Status` type defines a logical error model that is suitable for\ndifferent programming environments, including REST APIs and RPC APIs. It is\nused by [gRPC](https://github.com/grpc). The error model is designed to be:"
|
||||
},
|
||||
"runtimeStreamError": {
|
||||
"type": "object",
|
||||
@ -260,17 +262,17 @@
|
||||
}
|
||||
},
|
||||
"x-stream-definitions": {
|
||||
"apiQueryTicketsResponse": {
|
||||
"openmatchQueryTicketsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"result": {
|
||||
"$ref": "#/definitions/apiQueryTicketsResponse"
|
||||
"$ref": "#/definitions/openmatchQueryTicketsResponse"
|
||||
},
|
||||
"error": {
|
||||
"$ref": "#/definitions/runtimeStreamError"
|
||||
}
|
||||
},
|
||||
"title": "Stream result of apiQueryTicketsResponse"
|
||||
"title": "Stream result of openmatchQueryTicketsResponse"
|
||||
}
|
||||
},
|
||||
"externalDocs": {
|
||||
|
138
cloudbuild.yaml
138
cloudbuild.yaml
@ -52,15 +52,15 @@ steps:
|
||||
args: ['--destination=gcr.io/$PROJECT_ID/open-match-build', '--cache=true', '--cache-ttl=48h', '--dockerfile=Dockerfile.ci', '.']
|
||||
waitFor: ['-']
|
||||
|
||||
- id: 'Build: Clean'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'clean-third-party', 'clean-protos', 'clean-swagger-docs']
|
||||
waitFor: ['Docker Image: open-match-build']
|
||||
|
||||
- id: 'Test: Markdown'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'md-test']
|
||||
waitFor: ['Docker Image: open-match-build']
|
||||
|
||||
- id: 'Build: Clean'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'clean']
|
||||
waitFor: ['Docker Image: open-match-build']
|
||||
waitFor: ['Build: Clean']
|
||||
|
||||
- id: 'Setup: Download Dependencies'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
@ -70,45 +70,52 @@ steps:
|
||||
path: '/go'
|
||||
waitFor: ['Build: Clean']
|
||||
|
||||
- id: 'Build: Install Toolchain'
|
||||
- id: 'Build: Initialize Toolchain'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'install-toolchain']
|
||||
args: ['make', 'install-toolchain', 'push-helm-ci']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Setup: Download Dependencies']
|
||||
|
||||
- id: 'Build: Assets'
|
||||
- id: 'Test: Terraform Configuration'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'all-protos', 'tls-certs', '-j8']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Build: Install Toolchain']
|
||||
|
||||
- id: 'Build: Binaries'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'GOPROXY=off', 'all', '-j8']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Build: Assets']
|
||||
- id: 'Test: Core'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'GOPROXY=off', 'ci-test']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Build: Assets']
|
||||
- id: 'Build: Docker Images'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'VERSION_SUFFIX=$SHORT_SHA', 'push-images', '-j8']
|
||||
waitFor: ['Build: Assets']
|
||||
args: ['make', 'terraform-test']
|
||||
waitFor: ['Build: Initialize Toolchain']
|
||||
|
||||
- id: 'Build: Deployment Configs'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'VERSION_SUFFIX=$SHORT_SHA', 'clean-install-yaml', 'install/yaml/']
|
||||
waitFor: ['Build: Install Toolchain']
|
||||
args: ['make', 'SHORT_SHA=${SHORT_SHA}', 'update-chart-deps', 'install/yaml/']
|
||||
waitFor: ['Build: Initialize Toolchain']
|
||||
|
||||
- id: 'Build: Assets'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'assets', '-j12']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Build: Deployment Configs']
|
||||
|
||||
- id: 'Build: Binaries'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'GOPROXY=off', 'build', 'all', '-j12']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Build: Assets']
|
||||
|
||||
- id: 'Test: Services'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'GOPROXY=off', 'GOLANG_TEST_COUNT=10', 'test']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Build: Assets']
|
||||
|
||||
- id: 'Build: Docker Images'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', '_GCB_LATEST_VERSION=${_GCB_LATEST_VERSION}', 'SHORT_SHA=${SHORT_SHA}', 'BRANCH_NAME=${BRANCH_NAME}', 'push-images', '-j8']
|
||||
waitFor: ['Build: Assets']
|
||||
|
||||
- id: 'Lint: Format, Vet, Charts'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
@ -116,64 +123,51 @@ steps:
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
waitFor: ['Build: Assets', 'Build: Deployment Configs']
|
||||
waitFor: ['Build: Assets']
|
||||
|
||||
- id: 'Build: Website'
|
||||
- id: 'Test: Deploy Open Match'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'build/site/']
|
||||
waitFor: ['Build: Install Toolchain']
|
||||
|
||||
- id: 'Test: Website'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'site-test']
|
||||
waitFor: ['Build: Website']
|
||||
|
||||
- id: 'Deploy: Website'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', '_GCB_LATEST_VERSION=${_GCB_LATEST_VERSION}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-site']
|
||||
waitFor: ['Test: Website', 'Build: Binaries']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
args: ['make', 'SHORT_SHA=${SHORT_SHA}', 'OPEN_MATCH_KUBERNETES_NAMESPACE=open-match-${SHORT_SHA}', 'OPEN_MATCH_RELEASE_NAME=open-match-${SHORT_SHA}', 'auth-gke-cluster', 'delete-chart', 'ci-reap-namespaces', 'install-ci-chart']
|
||||
waitFor: ['Build: Docker Images']
|
||||
|
||||
- id: 'Deploy: Deployment Configs'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', '_GCB_LATEST_VERSION=${_GCB_LATEST_VERSION}', VERSION_SUFFIX=$SHORT_SHA', 'BRANCH_NAME=$BRANCH_NAME', 'ci-deploy-artifacts']
|
||||
waitFor: ['Lint: Format, Vet, Charts', 'Build: Binaries']
|
||||
args: ['make', '_GCB_POST_SUBMIT=${_GCB_POST_SUBMIT}', '_GCB_LATEST_VERSION=${_GCB_LATEST_VERSION}', 'SHORT_SHA=${SHORT_SHA}', 'BRANCH_NAME=${BRANCH_NAME}', 'ci-deploy-artifacts']
|
||||
waitFor: ['Lint: Format, Vet, Charts', 'Test: Deploy Open Match']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
|
||||
- id: 'Test: End-to-End Cluster'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'GOPROXY=off', 'SHORT_SHA=${SHORT_SHA}', 'OPEN_MATCH_KUBERNETES_NAMESPACE=open-match-${SHORT_SHA}', 'test-e2e-cluster']
|
||||
waitFor: ['Test: Deploy Open Match', 'Build: Assets']
|
||||
volumes:
|
||||
- name: 'go-vol'
|
||||
path: '/go'
|
||||
|
||||
- id: 'Test: Delete Open Match'
|
||||
name: 'gcr.io/$PROJECT_ID/open-match-build'
|
||||
args: ['make', 'GCLOUD_EXTRA_FLAGS=--async', 'SHORT_SHA=${SHORT_SHA}', 'OPEN_MATCH_KUBERNETES_NAMESPACE=open-match-${SHORT_SHA}', 'GCP_PROJECT_ID=${PROJECT_ID}', 'delete-chart']
|
||||
waitFor: ['Test: End-to-End Cluster']
|
||||
|
||||
artifacts:
|
||||
objects:
|
||||
location: gs://open-match-build-artifacts/output/
|
||||
paths:
|
||||
- cmd/backend/backend
|
||||
- cmd/frontend/frontend
|
||||
- cmd/mmlogic/mmlogic
|
||||
- cmd/evaluator/evaluator
|
||||
- cmd/minimatch/minimatch
|
||||
- install/yaml/install.yaml
|
||||
- install/yaml/install-demo.yaml
|
||||
- install/yaml/01-redis-chart.yaml
|
||||
- install/yaml/02-open-match.yaml
|
||||
- install/yaml/01-open-match-core.yaml
|
||||
- install/yaml/02-open-match-demo.yaml
|
||||
- install/yaml/03-prometheus-chart.yaml
|
||||
- install/yaml/04-grafana-chart.yaml
|
||||
- install/yaml/05-jaeger-chart.yaml
|
||||
- examples/functions/golang/simple/simple
|
||||
images:
|
||||
- 'gcr.io/$PROJECT_ID/openmatch-backend:${_OM_VERSION}-${SHORT_SHA}'
|
||||
- 'gcr.io/$PROJECT_ID/openmatch-frontend:${_OM_VERSION}-${SHORT_SHA}'
|
||||
- 'gcr.io/$PROJECT_ID/openmatch-mmlogic:${_OM_VERSION}-${SHORT_SHA}'
|
||||
- 'gcr.io/$PROJECT_ID/openmatch-evaluator:${_OM_VERSION}-${SHORT_SHA}'
|
||||
- 'gcr.io/$PROJECT_ID/openmatch-minimatch:${_OM_VERSION}-${SHORT_SHA}'
|
||||
- 'gcr.io/$PROJECT_ID/openmatch-mmf-go-simple:${_OM_VERSION}-${SHORT_SHA}'
|
||||
|
||||
substitutions:
|
||||
_OM_VERSION: "0.0.0-dev"
|
||||
_OM_VERSION: "0.7.0"
|
||||
_GCB_POST_SUBMIT: "0"
|
||||
_GCB_LATEST_VERSION: "undefined"
|
||||
logsBucket: 'gs://open-match-build-logs/'
|
||||
options:
|
||||
sourceProvenanceHash: ['SHA256']
|
||||
machineType: 'N1_HIGHCPU_32'
|
||||
timeout: 1200s
|
||||
timeout: 2500s
|
||||
|
@ -1,55 +0,0 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/cmd/backend/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
COPY --from=builder /go/src/open-match.dev/open-match/cmd/backend/backend .
|
||||
|
||||
ENTRYPOINT ["/backend"]
|
||||
|
||||
# Docker Image Arguments
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG BUILD_VERSION
|
||||
ARG IMAGE_TITLE="Open Match Backend API"
|
||||
|
||||
# Standardized Docker Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
LABEL \
|
||||
org.opencontainers.image.created="${BUILD_TIME}" \
|
||||
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
|
||||
org.opencontainers.image.url="https://open-match.dev/" \
|
||||
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
|
||||
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.revision="1" \
|
||||
org.opencontainers.image.vendor="Google LLC" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.ref.name="" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.url="http://open-match.dev/" \
|
||||
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
|
||||
org.label-schema.version=$BUILD_VERSION \
|
||||
org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vendor="Google LLC" \
|
||||
org.label-schema.name="${IMAGE_TITLE}" \
|
||||
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.usage="https://open-match.dev/site/docs/"
|
@ -16,9 +16,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/internal/app"
|
||||
"open-match.dev/open-match/internal/app/backend"
|
||||
)
|
||||
|
||||
func main() {
|
||||
backend.RunApplication()
|
||||
app.RunApplication("backend", backend.BindService)
|
||||
}
|
||||
|
31
cmd/demo-first-match/main.go
Normal file
31
cmd/demo-first-match/main.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/examples/demo"
|
||||
"open-match.dev/open-match/examples/demo/components"
|
||||
"open-match.dev/open-match/examples/demo/components/clients"
|
||||
"open-match.dev/open-match/examples/demo/components/director"
|
||||
"open-match.dev/open-match/examples/demo/components/uptime"
|
||||
)
|
||||
|
||||
func main() {
|
||||
demo.Run(map[string]func(*components.DemoShared){
|
||||
"uptime": uptime.Run,
|
||||
"clients": clients.Run,
|
||||
"director": director.Run,
|
||||
})
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/cmd/evaluator/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
COPY --from=builder /go/src/open-match.dev/open-match/cmd/evaluator/evaluator .
|
||||
|
||||
ENTRYPOINT ["/evaluator"]
|
||||
|
||||
# Docker Image Arguments
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG BUILD_VERSION
|
||||
ARG IMAGE_TITLE="Open Match Evaluator API"
|
||||
|
||||
# Standardized Docker Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
LABEL \
|
||||
org.opencontainers.image.created="${BUILD_TIME}" \
|
||||
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
|
||||
org.opencontainers.image.url="https://open-match.dev/" \
|
||||
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
|
||||
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.revision="1" \
|
||||
org.opencontainers.image.vendor="Google LLC" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.ref.name="" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.url="http://open-match.dev/" \
|
||||
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
|
||||
org.label-schema.version=$BUILD_VERSION \
|
||||
org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vendor="Google LLC" \
|
||||
org.label-schema.name="${IMAGE_TITLE}" \
|
||||
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.usage="https://open-match.dev/site/docs/"
|
@ -1,29 +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.
|
||||
|
||||
// Package main is the evaluator service for Open Match.
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/internal/app/evaluator"
|
||||
"open-match.dev/open-match/internal/pb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
evaluator.RunApplication(&evaluator.FunctionSettings{Func: evaluate})
|
||||
}
|
||||
|
||||
func evaluate(candidates []*pb.Match) []*pb.Match {
|
||||
return candidates
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/cmd/frontend/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
COPY --from=builder /go/src/open-match.dev/open-match/cmd/frontend/frontend .
|
||||
|
||||
ENTRYPOINT ["/frontend"]
|
||||
|
||||
# Docker Image Arguments
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG BUILD_VERSION
|
||||
ARG IMAGE_TITLE="Open Match Frontend API"
|
||||
|
||||
# Standardized Docker Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
LABEL \
|
||||
org.opencontainers.image.created="${BUILD_TIME}" \
|
||||
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
|
||||
org.opencontainers.image.url="https://open-match.dev/" \
|
||||
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
|
||||
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.revision="1" \
|
||||
org.opencontainers.image.vendor="Google LLC" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.ref.name="" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.url="http://open-match.dev/" \
|
||||
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
|
||||
org.label-schema.version=$BUILD_VERSION \
|
||||
org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vendor="Google LLC" \
|
||||
org.label-schema.name="${IMAGE_TITLE}" \
|
||||
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.usage="https://open-match.dev/site/docs/"
|
@ -16,9 +16,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/internal/app"
|
||||
"open-match.dev/open-match/internal/app/frontend"
|
||||
)
|
||||
|
||||
func main() {
|
||||
frontend.RunApplication()
|
||||
app.RunApplication("frontend", frontend.BindService)
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/cmd/minimatch/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
COPY --from=builder /go/src/open-match.dev/open-match/cmd/minimatch/minimatch .
|
||||
|
||||
ENTRYPOINT ["/minimatch"]
|
||||
|
||||
# Docker Image Arguments
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG BUILD_VERSION
|
||||
ARG IMAGE_TITLE="Mini Match"
|
||||
|
||||
# Standardized Docker Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
LABEL \
|
||||
org.opencontainers.image.created="${BUILD_TIME}" \
|
||||
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
|
||||
org.opencontainers.image.url="https://open-match.dev/" \
|
||||
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
|
||||
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.revision="1" \
|
||||
org.opencontainers.image.vendor="Google LLC" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.ref.name="" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.url="http://open-match.dev/" \
|
||||
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
|
||||
org.label-schema.version=$BUILD_VERSION \
|
||||
org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vendor="Google LLC" \
|
||||
org.label-schema.name="${IMAGE_TITLE}" \
|
||||
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.usage="https://open-match.dev/site/docs/"
|
@ -16,9 +16,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/internal/app"
|
||||
"open-match.dev/open-match/internal/app/minimatch"
|
||||
)
|
||||
|
||||
func main() {
|
||||
minimatch.RunApplication()
|
||||
app.RunApplication("minimatch", minimatch.BindService)
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/cmd/mmlogic/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
COPY --from=builder /go/src/open-match.dev/open-match/cmd/mmlogic/mmlogic .
|
||||
|
||||
ENTRYPOINT ["/mmlogic"]
|
||||
|
||||
# Docker Image Arguments
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG BUILD_VERSION
|
||||
ARG IMAGE_TITLE="Open Match Data API"
|
||||
|
||||
# Standardized Docker Image Labels
|
||||
# https://github.com/opencontainers/image-spec/blob/master/annotations.md
|
||||
LABEL \
|
||||
org.opencontainers.image.created="${BUILD_TIME}" \
|
||||
org.opencontainers.image.authors="Google LLC <open-match-discuss@googlegroups.com>" \
|
||||
org.opencontainers.image.url="https://open-match.dev/" \
|
||||
org.opencontainers.image.documentation="https://open-match.dev/site/docs/" \
|
||||
org.opencontainers.image.source="https://github.com/googleforgames/open-match" \
|
||||
org.opencontainers.image.version="${BUILD_VERSION}" \
|
||||
org.opencontainers.image.revision="1" \
|
||||
org.opencontainers.image.vendor="Google LLC" \
|
||||
org.opencontainers.image.licenses="Apache-2.0" \
|
||||
org.opencontainers.image.ref.name="" \
|
||||
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||
org.opencontainers.image.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.schema-version="1.0" \
|
||||
org.label-schema.build-date=$BUILD_DATE \
|
||||
org.label-schema.url="http://open-match.dev/" \
|
||||
org.label-schema.vcs-url="https://github.com/googleforgames/open-match" \
|
||||
org.label-schema.version=$BUILD_VERSION \
|
||||
org.label-schema.vcs-ref=$VCS_REF \
|
||||
org.label-schema.vendor="Google LLC" \
|
||||
org.label-schema.name="${IMAGE_TITLE}" \
|
||||
org.label-schema.description="Flexible, extensible, and scalable video game matchmaking." \
|
||||
org.label-schema.usage="https://open-match.dev/site/docs/"
|
@ -16,9 +16,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/internal/app"
|
||||
"open-match.dev/open-match/internal/app/mmlogic"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mmlogic.RunApplication()
|
||||
app.RunApplication("mmlogic", mmlogic.BindService)
|
||||
}
|
||||
|
23
cmd/scale-backend/main.go
Normal file
23
cmd/scale-backend/main.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/examples/scale/backend"
|
||||
)
|
||||
|
||||
func main() {
|
||||
backend.Run()
|
||||
}
|
23
cmd/scale-frontend/main.go
Normal file
23
cmd/scale-frontend/main.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/examples/scale/frontend"
|
||||
)
|
||||
|
||||
func main() {
|
||||
frontend.Run()
|
||||
}
|
10
cmd/swaggerui/config.json
Normal file
10
cmd/swaggerui/config.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"urls": [
|
||||
{"name": "Frontend", "url": "https://open-match.dev/api/v0.0.0-dev/frontend.swagger.json"},
|
||||
{"name": "Backend", "url": "https://open-match.dev/api/v0.0.0-dev/backend.swagger.json"},
|
||||
{"name": "Mmlogic", "url": "https://open-match.dev/api/v0.0.0-dev/mmlogic.swagger.json"},
|
||||
{"name": "MatchFunction", "url": "https://open-match.dev/api/v0.0.0-dev/matchfunction.swagger.json"},
|
||||
{"name": "Synchronizer", "url": "https://open-match.dev/api/v0.0.0-dev/synchronizer.swagger.json"},
|
||||
{"name": "Evaluator", "url": "https://open-match.dev/api/v0.0.0-dev/evaluator.swagger.json"}
|
||||
]
|
||||
}
|
24
cmd/swaggerui/swaggerui.go
Normal file
24
cmd/swaggerui/swaggerui.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package main is a simple webserver for hosting Open Match Swagger UI.
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/internal/app/swaggerui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
swaggerui.RunApplication()
|
||||
}
|
25
cmd/synchronizer/synchronizer.go
Normal file
25
cmd/synchronizer/synchronizer.go
Normal file
@ -0,0 +1,25 @@
|
||||
// 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.
|
||||
|
||||
// Package main is the synchronizer service for Open Match.
|
||||
package main
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/internal/app"
|
||||
"open-match.dev/open-match/internal/app/synchronizer"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.RunApplication("synchronizer", synchronizer.BindService)
|
||||
}
|
12
csharp/OpenMatch/OpenMatch.csproj
Normal file
12
csharp/OpenMatch/OpenMatch.csproj
Normal file
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<PackageId>OpenMatch</PackageId>
|
||||
<Version>0.0.0-dev</Version>
|
||||
<Authors>Google LLC</Authors>
|
||||
<Company>Google LLC</Company>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
114
docs/concepts.md
114
docs/concepts.md
@ -1,114 +0,0 @@
|
||||
# Core Concepts
|
||||
|
||||
[Watch the introduction of Open Match at Unite Berlin 2018 on YouTube](https://youtu.be/qasAmy_ko2o)
|
||||
|
||||
Open Match is designed to support massively concurrent matchmaking, and to be scalable to player populations of hundreds of millions or more. It attempts to apply stateless web tech microservices patterns to game matchmaking. If you're not sure what that means, that's okay — it is fully open source and designed to be customizable to fit into your online game architecture — so have a look a the code and modify it as you see fit.
|
||||
|
||||
## Glossary
|
||||
|
||||
### General
|
||||
|
||||
* **DGS** — Dedicated game server
|
||||
* **Client** — The game client program the player uses when playing the game
|
||||
* **Session** — In Open Match, players are matched together, then assigned to a server which hosts the game _session_. Depending on context, this may be referred to as a _match_, _map_, or just _game_ elsewhere in the industry.
|
||||
|
||||
### Open Match
|
||||
|
||||
* **Component** — One of the discrete processes in an Open Match deployment. Open Match is composed of multiple scalable microservices called _components_.
|
||||
* **State Storage** — The storage software used by Open Match to hold all the matchmaking state. Open Match ships with [Redis](https://redis.io/) as the default state storage.
|
||||
* **MMF** — Matchmaking function. This is the customizable matchmaking logic.
|
||||
* **Function Harness** — A GRPC serving harness that triggers the Match function.
|
||||
* **Evaluator** — Customizable evaluation logic that analyzes match proposals and approves / rejects matches.
|
||||
* **MMLogic API** — An API that provides MMF SDK functionality.
|
||||
* **Director** — The software you (as a developer) write against the Open Match Backend API. The _Director_ decides which MMFs to run, and is responsible for sending MMF results to a DGS to host the session.
|
||||
|
||||
### Data Model
|
||||
|
||||
* **Player** — An ID and list of attributes with values for a player who wants to participate in matchmaking.
|
||||
* **Roster** — A list of player objects. Used to hold all the players on a single team.
|
||||
* **Filter** — A _filter_ is used to narrow down the players to only those who have an attribute value within a certain integer range. All attributes are integer values in Open Match because [that is how indices are implemented](internal/statestorage/redis/playerindices/playerindices.go). A _filter_ is defined in a _player pool_.
|
||||
* **Player Pool** — A list of all the players who fit all the _filters_ defined in the pool.
|
||||
* **Match Object** — A protobuffer message format that contains the _profile_ and the results of the matchmaking function. Sent to the backend API from your game backend with the _roster_(s) empty and then returned from your MMF with the matchmaking results filled in.
|
||||
* **Profile** — The json blob containing all the parameters used by your MMF to select which players go into a roster together.
|
||||
* **Assignment** — Refers to assigning a player or group of players to a dedicated game server instance. Open Match offers a path to send dedicated game server connection details from your backend to your game clients after a match has been made.
|
||||
* **Ignore List** — Removing players from matchmaking consideration is accomplished using _ignore lists_. They contain lists of player IDs that your MMF should not include when making matches.
|
||||
|
||||
## Requirements
|
||||
|
||||
* [Kubernetes](https://kubernetes.io/) cluster — tested with version 1.11.7.
|
||||
* [Redis 4+](https://redis.io/) — tested with 4.0.11.
|
||||
* Open Match is compiled against the latest release of [Golang](https://golang.org/) — tested with 1.11.5.
|
||||
|
||||
## Components
|
||||
|
||||
Open Match is a set of processes designed to run on Kubernetes. It contains these **core** components:
|
||||
|
||||
* Frontend API
|
||||
* Backend API
|
||||
* Matchmaking Logic (MMLogic) API
|
||||
|
||||
It also depends on these two **customizable** components.
|
||||
|
||||
* Match Function (MMF)
|
||||
* Evaluator
|
||||
|
||||
While **core** components are fully open source and _can_ be modified, they are designed to support the majority of matchmaking scenarios *without need to change the source code*. The Open Match repository ships with simple **customizable** MMF and Evaluator examples, but it is expected that most users will want full control over the logic in these, so they have been designed to be as easy to modify or replace as possible.
|
||||
|
||||
### Frontend API
|
||||
|
||||
The Frontend API accepts the player data and puts it in state storage so your Matchmaking Function (MMF) can access it.
|
||||
|
||||
The Frontend API is a server application that implements the [gRPC](https://grpc.io/) service defined in `api/protobuf-spec/frontend.proto`. At the most basic level, it expects clients to connect and send:
|
||||
* A **unique ID** for the group of players (the group can contain any number of players, including only one).
|
||||
* A **json blob** containing all player-related data you want to use in your matchmaking function.
|
||||
|
||||
The client is expected to maintain a connection, waiting for an update from the API that contains the details required to connect to a dedicated game server instance (an 'assignment'). There are also basic functions for removing an ID from the matchmaking pool or an existing match.
|
||||
|
||||
### Backend API
|
||||
|
||||
The Backend API writes match objects to state storage which the Matchmaking Functions (MMFs) access to decide which players should be matched. It returns the results from those MMFs.
|
||||
|
||||
The Backend API is a server application that implements the [gRPC](https://grpc.io/) service defined in `api/protobuf-spec/backend.proto`. At the most basic level, it expects to be connected to your online infrastructure (probably to your server scaling manager or **director**, or even directly to a dedicated game server), and to receive:
|
||||
* A **unique ID** for a matchmaking profile.
|
||||
* A **json blob** containing all the matching-related data and filters you want to use in your matchmaking function.
|
||||
* An optional list of **roster**s to hold the resulting teams chosen by your matchmaking function.
|
||||
* An optional set of **filters** that define player pools your matchmaking function will choose players from.
|
||||
|
||||
Your game backend is expected to maintain a connection, waiting for 'filled' match objects containing a roster of players. The Backend API also provides a return path for your game backend to return dedicated game server connection details (an 'assignment') to the game client, and to delete these 'assignments'.
|
||||
|
||||
### Matchmaking Logic (MMLogic) API
|
||||
|
||||
The MMLogic API provides a series of gRPC functions that act as a Matchmaking Function SDK. Much of the basic, boilerplate code for an MMF is the same regardless of what players you want to match together. The MMLogic API offers a gRPC interface for many common MMF tasks, such as:
|
||||
|
||||
1. Reading a profile from state storage.
|
||||
1. Running filters on players in state strorage. It automatically removes players on ignore lists as well!
|
||||
1. Removing chosen players from consideration by other MMFs (by adding them to an ignore list). It does it automatically for you when writing your results!
|
||||
1. Writing the matchmaking results to state storage.
|
||||
1. (Optional, NYI) Exporting MMF stats for metrics collection.
|
||||
|
||||
More details about the available gRPC calls can be found in the [API Specification](api/protobuf-spec/messages.proto).
|
||||
|
||||
**Note**: using the MMLogic API is **optional**. It tries to simplify the development of MMFs, but if you want to take care of these tasks on your own, you can make few or no calls to the MMLogic API as long as your MMF still completes all the required tasks. Read the [Matchmaking Functions section](#matchmaking-functions-mmfs) for more details of what work an MMF must do.
|
||||
|
||||
### Evaluator
|
||||
|
||||
The Evaluator resolves conflicts when multiple MMFs select the same player(s). Evaluator is provided by the developer (sample included in Open Match).
|
||||
|
||||
The Evaluator runs forever, looping over a configured interval, checking if MMFs have completed execution or if certain time interval has passed. Upon reaching those conditions, the Evaluator calls the Evaluation Function (to be modified by the user) with the proposals to choose from. The sample Evaluation function looks at all the proposals, and if multiple proposals contain the same player(s), it breaks the tie. In many simple matchmaking setups with only a few game modes and well-tuned matchmaking functions, the Evaluator may functionally be a no-op or first-in-first-out algorithm. In complex matchmaking setups where, for example, a player can queue for multiple types of matches, the Evaluator provides the critical customizability to evaluate all available proposals and approve those that will passed to your game servers.
|
||||
|
||||
Large-scale concurrent matchmaking functions is a complex topic, and users who wish to do this are encouraged to engage with the [Open Match community](https://github.com/googleforgames/open-match#get-involved) about patterns and best practices.
|
||||
|
||||
### Matchmaking Functions (MMFs)
|
||||
|
||||
Matchmaking Functions (MMFs) are implemented by the developer and are hosted as a gRPC service. Open Match provides a harness (currently for golang) that handles the broiler-plate Open Match communitation, gRPC server setup etc., so that the user only has to write a function that accepts a set of player pools and a match profile and returns a proposal based on some core match making logic. An MMF is called each time a request to generate a match is received. At a high level, an MMF needs to generate a proposal using the given players, match profile and its custom match making logic and return the proposal to the calling harness.
|
||||
**Note**: Currently Open Match only has a golang harness. To add an MMF in any other language, a harness needs to be implemented in that language.
|
||||
|
||||
## Example Tooling
|
||||
|
||||
To see Open Match, in action, here are some basic tools that are provided as samples:
|
||||
|
||||
* `test/cmd/clientloadgen/` is a (VERY) basic client load simulation tool. It endlessly writes players into state storage so you can test your backend integration, and run your custom MMFs and Evaluators (which are only triggered when there are players in the pool).
|
||||
|
||||
* `examples/backendclient` is a fake client for the Backend API. It pretends to be a dedicated game server backend connecting to Open Match and sending in a match profile to fill and receives completed matches. It can call Create / List matches.
|
||||
|
||||
* `test/cmd/frontendclient/` is a fake client for the Frontend API. It pretends to be group of real game clients connecting to Open Match. It requests a game, then dumps out the results each player receives to the screen.
|
@ -1,12 +1,8 @@
|
||||
# Development Guide
|
||||
|
||||
Open Match is a collection of [Go](https://golang.org/) applications that run
|
||||
Open Match is a collection of [Go](https://golang.org/) gRPC services that run
|
||||
within [Kubernetes](https://kubernetes.io).
|
||||
|
||||
If you're not familiar with either that's ok, you can checkout the
|
||||
[reference material](references.md). Knowledge of these tools is not
|
||||
required to setup your environment.
|
||||
|
||||
## Install Prerequisites
|
||||
|
||||
To build Open Match you'll need the following applications installed.
|
||||
@ -107,6 +103,22 @@ make proxy
|
||||
make delete-chart
|
||||
```
|
||||
|
||||
## Interaction
|
||||
|
||||
Before integrating with Open Match you can manually interact with it to get a feel for how it works.
|
||||
|
||||
`make proxy-ui` exposes the Swagger UI for Open Match locally on your computer.
|
||||
You can then go to http://localhost:51500 and view the API as well as interactively call Open Match.
|
||||
|
||||
By default you will be talking to the frontend server but you can change the target API url to any of the following:
|
||||
|
||||
* api/frontend.swagger.json
|
||||
* api/backend.swagger.json
|
||||
* api/synchronizer.swagger.json
|
||||
* api/mmlogic.swagger.json
|
||||
|
||||
For a more current list refer to the api/ directory of this repository. Also matchfunction.swagger.json is not supported.
|
||||
|
||||
## IDE Support
|
||||
|
||||
Open Match is a standard Go project so any IDE that understands that should
|
||||
|
@ -25,21 +25,20 @@ Images
|
||||
|
||||
```bash
|
||||
# Servers
|
||||
docker pull gcr.io/open-match-public-images/openmatch-backendapi:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-frontendapi:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-mmforc:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-mmlogicapi:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-backend:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-frontend:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-mmlogic:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-synchronizer:{version}
|
||||
|
||||
# Evaluators
|
||||
docker pull gcr.io/open-match-public-images/openmatch-evaluator-serving:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-evaluator-go-simple:{version}
|
||||
|
||||
# Sample Match Making Functions
|
||||
docker pull gcr.io/open-match-public-images/openmatch-mmf-go-simple:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-mmf-go-soloduel:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-mmf-go-pool:{version}
|
||||
|
||||
# Test Clients
|
||||
docker pull gcr.io/open-match-public-images/openmatch-backendclient:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-clientloadgen:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-frontendclient:{version}
|
||||
docker pull gcr.io/open-match-public-images/openmatch-demo:{version}
|
||||
```
|
||||
|
||||
_This software is currently alpha, and subject to change. Not to be used in production systems._
|
||||
|
@ -12,7 +12,7 @@ SOURCE_VERSION=$1
|
||||
DEST_VERSION=$2
|
||||
SOURCE_PROJECT_ID=open-match-build
|
||||
DEST_PROJECT_ID=open-match-public-images
|
||||
IMAGE_NAMES="openmatch-backendapi openmatch-frontendapi openmatch-mmforc openmatch-mmlogicapi openmatch-evaluator-serving openmatch-mmf-go-simple openmatch-backendclient openmatch-clientloadgen openmatch-frontendclient"
|
||||
IMAGE_NAMES="openmatch-backend openmatch-frontend openmatch-mmlogic openmatch-synchronizer openmatch-minimatch openmatch-demo openmatch-mmf-go-soloduel openmatch-mmf-go-pool openmatch-evaluator-go-simple openmatch-swaggerui openmatch-reaper"
|
||||
|
||||
for name in $IMAGE_NAMES
|
||||
do
|
||||
|
@ -1,17 +0,0 @@
|
||||
## Open Source Software integrations
|
||||
|
||||
### Structured Logging - Logrus
|
||||
|
||||
Logging for Open Match uses the [Golang logrus module](https://github.com/sirupsen/logrus) to provide structured logs. Logs are output to `stdout` in each component, as expected by Docker and Kubernetes. Level and format are configurable via config/matchmaker_config.json. If you have a specific log aggregator as your final destination, we recommend you have a look at the logrus documentation as there is probably a log formatter that plays nicely with your stack.
|
||||
|
||||
### Instrumentation - OpenCensus
|
||||
|
||||
Open Match uses [OpenCensus](https://opencensus.io/) for metrics instrumentation. The [gRPC](https://grpc.io/) integrations are built-in, and Golang redigo module integrations are incoming, but [haven't been merged into the official repo](https://github.com/opencensus-integrations/redigo/pull/1). All of the core components expose HTTP `/metrics` endpoints on the port defined in `config/matchmaker_config.json` (default: 9555) for Prometheus to scrape. If you would like to export to a different metrics aggregation platform, we suggest you have a look at the OpenCensus documentation — there may be one written for you already, and switching to it may be as simple as changing a few lines of code.
|
||||
|
||||
**Note:** A standard for instrumentation of MMFs is planned.
|
||||
|
||||
### State Storage - Redis
|
||||
|
||||
By default, Open Match expects you to run Redis *somewhere*. Connection information can be put in the config file (`matchmaker_config.json`) for any Redis instance reachable from the [Kubernetes namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). By default, Open Match sensibly runs in the Kubernetes `default` namespace. In most instances, we expect users will run a copy of Redis in a pod in Kubernetes, with a service pointing to it.
|
||||
|
||||
* HA configurations for Redis aren't implemented by the provided Kubernetes resource definition files, but Open Match expects the Redis service to be named `redis`, which provides an easier path to multi-instance deployments.
|
@ -1,57 +0,0 @@
|
||||
# Knative instructions
|
||||
This describes the basic installation of knative (and istio) for serverless match functions.
|
||||
It also provides experimental instructions for setting up you function host and endpoint.
|
||||
|
||||
## New cluster deploy instructions
|
||||
Primarily taken from this [https://knative.dev/docs/install/] guide
|
||||
### Install Istio
|
||||
Download and apply the istio manifests
|
||||
```
|
||||
curl -L https://git.io/getLatestIstio | sh -
|
||||
cd istio-1.0.5/
|
||||
kubectl apply -f install/kubernetes/helm/istio/templates/crds.yaml
|
||||
kubectl apply -f install/kubernetes/istio-demo-auth.yaml
|
||||
```
|
||||
|
||||
Wait for `kubectl get pods --namespace istio-system` to complete
|
||||
|
||||
Don't forget, you'll need to apply istio-injection label for any namespace you deploy to
|
||||
`kubectl label namespace default istio-injection=enabled`
|
||||
|
||||
### Install Knative
|
||||
You may need to give your gcp user cluster admin privs to get through this part
|
||||
`kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user="{your user}"`
|
||||
|
||||
To install knative, promethus + grafana (metrics), elk (logging), and zipkin (tracing), run. See the linked custom installation guide for more options
|
||||
```
|
||||
k apply -f https://github.com/knative/serving/releases/download/v0.3.0/serving.yaml /
|
||||
-f https://github.com/knative/serving/releases/download/v0.3.0/monitoring.yaml
|
||||
```
|
||||
|
||||
Wait for `kubectl get pods --namespace knative-serving` to complete
|
||||
|
||||
## Serving
|
||||
To use the knative serverless flow for match functions...
|
||||
1. Create a host-harness for running your function as a hosted service at `/api/function` (roadmap v0.4 and v0.6 for something more official)
|
||||
2. Modify the provided knative serving manifest example at /deployments/k8s/knative_sample.yaml with your image name
|
||||
3. Run `kubectl apply -f knative_sample.yaml` to start up an instance of the function on your knative installation
|
||||
4. Using the configurable Http/1.1 REST pattern in open match (in review calebatwd/knative-rest-mmf), specify a `{..."hostName": "{knative service name}", "port": 8080}` in your match object properties when calling CreateMatch. The default port is 8080 in knative, but you can use whatever you like.
|
||||
5. This should tell open match mmforc to call your function via hostname discovery in kubernetes over the knative ingress. You should see logs from your mmf function.
|
||||
|
||||
### Other Notes
|
||||
- After 5 minutes of inactivity, knative will spin down your function. Subsequent calls will have a warm-up period of a few seconds depending on your function and host.
|
||||
- The default port for knative is 8080, but you can use whatever you like so long as the intended port is Docker EXPOSED, knative will bind on that
|
||||
- Dns discovery over knative has been problematic via the ingress. If you experience issues, considering updating the dns host discovery in mmforc to resolve on the full name `function-name.default.svc.cluster.local`
|
||||
- Http2 is currently not supported in knative, so GRPC is not currently usable.
|
||||
- The REST contract for the function can be found in cmd/mmforc/main.go as
|
||||
|
||||
```
|
||||
type Profile struct {
|
||||
JobName string
|
||||
ProfId string
|
||||
MoId string
|
||||
PropId string
|
||||
ResultsId string
|
||||
Timestamp string
|
||||
}
|
||||
```
|
@ -1,10 +0,0 @@
|
||||
## Additional References
|
||||
|
||||
### Docker
|
||||
- [Docker's official "Getting Started" guide](https://docs.docker.com/get-started/)
|
||||
|
||||
### Kubernetes
|
||||
- [You should totally read this comic, and interactive tutorial](https://cloud.google.com/kubernetes-engine/kubernetes-comic/)
|
||||
|
||||
### Prometheus
|
||||
- [Prometheus Operator spec](https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md)
|
@ -1,68 +0,0 @@
|
||||
# Open Match Roadmap
|
||||
|
||||
Open Match is currently at release 0.4.0. Open Match 0.5.0 currently has a Release Candidate and we are targeting to cut the release on 04/25/2019.
|
||||
|
||||
Releases can be found on the [releases page](https://github.com/googleforgames/open-match/releases).
|
||||
|
||||
Below sections detail the themes and the roadmap for the future releases. The tasks listed for the 0.6.0 release have been finalized and are well understood. As for the 0.7.0 and beyond, the tasks currently identified are listed. These are subject to change as we make our way through 0.6.0 release and get more feedback from the community.
|
||||
|
||||
## 0.5.0 - Usability
|
||||
|
||||
The primary focus of the 0.5 release is usability. The goal for this release is to make Open Match easy to build and deploy and have solid supporting documentation. Users should be able to try Open Match 0.5.0 functionality and experiment with its features, MMFs etc. Here are some planned features for this release:
|
||||
|
||||
- [X] Add support to invoke MMFs as a gRPC function call.
|
||||
- [X] Provide a gRPC serving harness and an example MMF built using this harness. (golang based).
|
||||
- [X] Provide a evaluation harness and a sample evaluator using this harness (golang based)
|
||||
- [X] Deprecate the k8s based job scheduling mechanism for MMFs, Evaluator in favor of hosted MMFs, Evaluator.
|
||||
- [X] Switch all core Open Match services to use gRPC style request / response protos.
|
||||
- [X] Documentation: Add basic user, developer documentation and set up the Open Match website.
|
||||
- [X] Create and document a formal release process.
|
||||
- [X] Improve developer experience (simplify compiling, deploying and validating)
|
||||
|
||||
## 0.6.0 - API changes, Maturity
|
||||
|
||||
In 0.6.0 release, we are revisiting the Data Model and the API surface exposed by Open Match. The goal of this release is to front-load a major API refactoring that will facilitate achieving scale and other productionizing goals in forthcoming releases. Although breaking chagnes can happen any time till 1.0, the goal is to implement any major breaking changes in 0.6.0 so that future chagnes if any are relatively minor. Customer should be able to start building their Match Makers using the 0.6.0 API surface.
|
||||
|
||||
Here are the tasks planned for 0.6.0 release:
|
||||
|
||||
- [ ] Implement the new Data model and the API changes for the Frontend, Backend and MMLogic API [Change Proposal](https://github.com/googleforgames/open-match/issues/279)
|
||||
- [ ] Accept multiple proposals per MMF execution.
|
||||
- [ ] Remove persistance of matches and proposals from Open Match state storage.
|
||||
- [ ] Implement synchronized evaluation to eliminate use of state storage during evaluation.
|
||||
- [ ] Introduce test framework for unit testing, Component testing and E2E testing.
|
||||
- [ ] Add unit tests, component tests and integration tests for Open Match core components and examples.
|
||||
- [ ] Update harness, evaluator, mmf samples etc., to reflect the API changes.
|
||||
- [ ] Update documentation, website to reflect 0.6.0 API changes.
|
||||
|
||||
## 0.7.0 - Scale, Operationalizing
|
||||
|
||||
Features for 0.7.0 are targeted to enable Open Match to be productionizable. Note that as we identify more feature work past 0.6.0, these tasks may get pushed to future releases. However, these are core tasks that need to be addressed before Open Match reaches 1.0.
|
||||
|
||||
- [ ] Introduce Test framework for load, performance testing
|
||||
- [ ] Automated Load / performance / scale tests
|
||||
- [ ] Test results Dashboard
|
||||
- [ ] Add support for Instrumentation, Monitoring, Dashboards
|
||||
- [ ] Add support or Metrics collection, Analytics, Dashboards
|
||||
- [ ] Identify Autoscaling patterns for each component and configure them.
|
||||
|
||||
## Other Features
|
||||
|
||||
Below are additional features that are not tied to a specific release but will be added incrementally across releases:
|
||||
|
||||
- [ ] Harness support for Python, PHP, C#, C++
|
||||
- [ ] User Guide for Open Match, Tutorials
|
||||
- [ ] Developer Guide for Open Match
|
||||
- [ ] APIs & Reference
|
||||
- [ ] Concept Documentation
|
||||
- [ ] Website Improvements
|
||||
|
||||
## 1.1.0
|
||||
|
||||
Below are the features that have been identified but are not considered critical for Open Match (as a match making framework) to itself reach 1.0. Any other features that are related to Open Match ecosystem but not a part of the framework itself can be listed here. These features may not necessarily wait for Open Match 1.0 and can be implemented before that - but any of the currently identified 1.0 tasks are higher in priority than these to make Open Match production ready.
|
||||
|
||||
- [ ] Canonical usable examples out of box.
|
||||
- [ ] KNative support to run MMFs
|
||||
- [ ] OSS Director to integrate with Agones, other DGS backends
|
||||
|
||||
### Special Thanks
|
||||
- Thanks to https://jbt.github.io/markdown-editor/ for help in marking this document down.
|
20
examples/consts.go
Normal file
20
examples/consts.go
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package examples defines the constants that some of the examples may share.
|
||||
package examples
|
||||
|
||||
const (
|
||||
MatchScore = "match_score"
|
||||
)
|
81
examples/demo/bytesub/bytesub.go
Normal file
81
examples/demo/bytesub/bytesub.go
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package bytesub provides the ability for many clients to subscribe to the
|
||||
// latest value of a byte array.
|
||||
package bytesub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var partialWriteError = errors.New("ByteSub subscriber didn't consume the whole message.")
|
||||
|
||||
type ByteSub struct {
|
||||
nextReady chan struct{}
|
||||
b []byte
|
||||
r sync.RWMutex
|
||||
}
|
||||
|
||||
func New() *ByteSub {
|
||||
return &ByteSub{
|
||||
nextReady: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// AnnounceLatest writes b to all of the subscribers, with caviets listed in Subscribe.
|
||||
func (s *ByteSub) AnnounceLatest(b []byte) {
|
||||
s.r.Lock()
|
||||
defer s.r.Unlock()
|
||||
close(s.nextReady)
|
||||
s.nextReady = make(chan struct{})
|
||||
s.b = b
|
||||
}
|
||||
|
||||
func (s *ByteSub) get() (b []byte, nextReady chan struct{}) {
|
||||
s.r.RLock()
|
||||
defer s.r.RUnlock()
|
||||
return s.b, s.nextReady
|
||||
}
|
||||
|
||||
// Subscribe writes the latest value in a single call to w.Write. It does not
|
||||
// guarantee that all values published to AnnounceLatest will be written.
|
||||
// However once things catch up, it will write the latest value. If no values
|
||||
// have been announced, waits for a value before writing.
|
||||
func (s *ByteSub) Subscribe(ctx context.Context, w io.Writer) error {
|
||||
nextReady := make(chan struct{})
|
||||
close(nextReady)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-nextReady:
|
||||
var b []byte
|
||||
b, nextReady = s.get()
|
||||
if b != nil {
|
||||
l, err := w.Write(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if l != len(b) {
|
||||
return partialWriteError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
131
examples/demo/bytesub/bytesub_test.go
Normal file
131
examples/demo/bytesub/bytesub_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package bytesub provides the ability for many clients to subscribe to the
|
||||
// latest value of a byte array.
|
||||
package bytesub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFastAndSlow ensures that if a slow subscriber is blocked, faster subscribers
|
||||
// nor publishers aren't blocked. It also ensures that values published while slow
|
||||
// wasn't listening are skipped.
|
||||
func TestFastAndSlow(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
fast := make(chan string)
|
||||
slow := make(chan string)
|
||||
|
||||
s := New()
|
||||
|
||||
go func() {
|
||||
s.Subscribe(ctx, chanWriter{fast})
|
||||
close(fast)
|
||||
}()
|
||||
go func() {
|
||||
s.Subscribe(ctx, chanWriter{slow})
|
||||
close(slow)
|
||||
}()
|
||||
|
||||
for _, i := range []string{"0", "1", "2", "3"} {
|
||||
s.AnnounceLatest([]byte(i))
|
||||
if v := <-fast; v != i {
|
||||
t.Errorf("Expected \"%s\", got \"%s\"", i, v)
|
||||
}
|
||||
}
|
||||
|
||||
for count := 0; true; count++ {
|
||||
if v := <-slow; v == "3" {
|
||||
if count > 1 {
|
||||
t.Error("Expected to recieve at most 1 other value on slow before recieving the latest value.")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
_, ok := <-fast
|
||||
if ok {
|
||||
t.Error("Expected subscribe to return and fast to be closed")
|
||||
}
|
||||
_, ok = <-slow
|
||||
if ok {
|
||||
t.Error("Expected subscribe to return and slow to be closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadWriter(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
s := New()
|
||||
s.AnnounceLatest([]byte{0, 1, 2})
|
||||
err := s.Subscribe(ctx, writerFunc(func(b []byte) (int, error) {
|
||||
return 1, nil
|
||||
}))
|
||||
|
||||
if err != partialWriteError {
|
||||
t.Errorf("Expected partialWriteError, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorReturned(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
expected := errors.New("Hello there.")
|
||||
|
||||
s := New()
|
||||
s.AnnounceLatest([]byte{0, 1, 2})
|
||||
err := s.Subscribe(ctx, writerFunc(func(b []byte) (int, error) {
|
||||
return 0, expected
|
||||
}))
|
||||
|
||||
if err != expected {
|
||||
t.Errorf("Expected returned error, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextError(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
s := New()
|
||||
s.AnnounceLatest([]byte{0, 1, 2})
|
||||
err := s.Subscribe(ctx, writerFunc(func(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}))
|
||||
|
||||
if err != context.Canceled {
|
||||
t.Errorf("Expected context canceled error, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
type chanWriter struct {
|
||||
c chan string
|
||||
}
|
||||
|
||||
func (cw chanWriter) Write(b []byte) (int, error) {
|
||||
cw.c <- string(b)
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
type writerFunc func(b []byte) (int, error)
|
||||
|
||||
func (w writerFunc) Write(b []byte) (int, error) {
|
||||
return w(b)
|
||||
}
|
146
examples/demo/components/clients/clients.go
Normal file
146
examples/demo/components/clients/clients.go
Normal file
@ -0,0 +1,146 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"open-match.dev/open-match/examples/demo/components"
|
||||
"open-match.dev/open-match/examples/demo/updater"
|
||||
"open-match.dev/open-match/internal/config"
|
||||
"open-match.dev/open-match/internal/rpc"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
"open-match.dev/open-match/pkg/structs"
|
||||
)
|
||||
|
||||
func Run(ds *components.DemoShared) {
|
||||
u := updater.NewNested(ds.Ctx, ds.Update)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
name := fmt.Sprintf("fakeplayer_%d", i)
|
||||
go func() {
|
||||
for !isContextDone(ds.Ctx) {
|
||||
runScenario(ds.Ctx, ds.Cfg, name, u.ForField(name))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func isContextDone(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type status struct {
|
||||
Status string
|
||||
Assignment *pb.Assignment
|
||||
}
|
||||
|
||||
func runScenario(ctx context.Context, cfg config.View, name string, update updater.SetFunc) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("pkg: %v", r)
|
||||
}
|
||||
|
||||
update(status{Status: fmt.Sprintf("Encountered error: %s", err.Error())})
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
s := status{}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Main Menu"
|
||||
update(s)
|
||||
|
||||
time.Sleep(time.Duration(rand.Int63()) % (time.Second * 15))
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Connecting to Open Match frontend"
|
||||
update(s)
|
||||
|
||||
conn, err := rpc.GRPCClientFromConfig(cfg, "api.frontend")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
fe := pb.NewFrontendClient(conn)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Creating Open Match Ticket"
|
||||
update(s)
|
||||
|
||||
var ticketId string
|
||||
{
|
||||
req := &pb.CreateTicketRequest{
|
||||
Ticket: &pb.Ticket{
|
||||
Properties: structs.Struct{
|
||||
"name": structs.String(name),
|
||||
"mode.demo": structs.Number(1),
|
||||
}.S(),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := fe.CreateTicket(ctx, req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ticketId = resp.Ticket.Id
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = fmt.Sprintf("Waiting match with ticket Id %s", ticketId)
|
||||
update(s)
|
||||
|
||||
var assignment *pb.Assignment
|
||||
{
|
||||
req := &pb.GetAssignmentsRequest{
|
||||
TicketId: ticketId,
|
||||
}
|
||||
|
||||
stream, err := fe.GetAssignments(ctx, req)
|
||||
for assignment.GetConnection() == "" {
|
||||
resp, err := stream.Recv()
|
||||
if err != nil {
|
||||
// For now we don't expect to get EOF, so that's still an error worthy of panic.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assignment = resp.Assignment
|
||||
}
|
||||
|
||||
err = stream.CloseSend()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Sleeping (pretend this is playing a match...)"
|
||||
s.Assignment = assignment
|
||||
update(s)
|
||||
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
28
examples/demo/components/components.go
Normal file
28
examples/demo/components/components.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package components
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"open-match.dev/open-match/examples/demo/updater"
|
||||
"open-match.dev/open-match/internal/config"
|
||||
)
|
||||
|
||||
type DemoShared struct {
|
||||
Ctx context.Context
|
||||
Cfg config.View
|
||||
Update updater.SetFunc
|
||||
}
|
160
examples/demo/components/director/director.go
Normal file
160
examples/demo/components/director/director.go
Normal file
@ -0,0 +1,160 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package director
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"open-match.dev/open-match/examples/demo/components"
|
||||
"open-match.dev/open-match/internal/rpc"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
)
|
||||
|
||||
func Run(ds *components.DemoShared) {
|
||||
for !isContextDone(ds.Ctx) {
|
||||
run(ds)
|
||||
}
|
||||
}
|
||||
|
||||
func isContextDone(ctx context.Context) bool {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type status struct {
|
||||
Status string
|
||||
LatestMatches []*pb.Match
|
||||
}
|
||||
|
||||
func run(ds *components.DemoShared) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
err, ok := r.(error)
|
||||
if !ok {
|
||||
err = fmt.Errorf("pkg: %v", r)
|
||||
}
|
||||
|
||||
ds.Update(status{Status: fmt.Sprintf("Encountered error: %s", err.Error())})
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
s := status{}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Connecting to backend"
|
||||
ds.Update(s)
|
||||
|
||||
conn, err := rpc.GRPCClientFromConfig(ds.Cfg, "api.backend")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
be := pb.NewBackendClient(conn)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Match Match: Sending Request"
|
||||
ds.Update(s)
|
||||
|
||||
var matches []*pb.Match
|
||||
{
|
||||
req := &pb.FetchMatchesRequest{
|
||||
Config: &pb.FunctionConfig{
|
||||
Host: ds.Cfg.GetString("api.functions.hostname"),
|
||||
Port: int32(ds.Cfg.GetInt("api.functions.grpcport")),
|
||||
Type: pb.FunctionConfig_GRPC,
|
||||
},
|
||||
Profiles: []*pb.MatchProfile{
|
||||
{
|
||||
Name: "1v1",
|
||||
Pools: []*pb.Pool{
|
||||
{
|
||||
Name: "Everyone",
|
||||
FloatRangeFilters: []*pb.FloatRangeFilter{
|
||||
{
|
||||
Attribute: "mode.demo",
|
||||
Min: -100,
|
||||
Max: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
stream, err := be.FetchMatches(ds.Ctx, req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
matches = append(matches, resp.GetMatch())
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Matches Found"
|
||||
s.LatestMatches = matches
|
||||
ds.Update(s)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Assigning Players"
|
||||
ds.Update(s)
|
||||
|
||||
for _, match := range matches {
|
||||
ids := []string{}
|
||||
|
||||
for _, t := range match.Tickets {
|
||||
ids = append(ids, t.Id)
|
||||
}
|
||||
|
||||
req := &pb.AssignTicketsRequest{
|
||||
TicketIds: ids,
|
||||
Assignment: &pb.Assignment{
|
||||
Connection: fmt.Sprintf("%d.%d.%d.%d:2222", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := be.AssignTickets(ds.Ctx, req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_ = resp
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
s.Status = "Sleeping"
|
||||
ds.Update(s)
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
37
examples/demo/components/uptime/uptime.go
Normal file
37
examples/demo/components/uptime/uptime.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package uptime
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"open-match.dev/open-match/examples/demo/components"
|
||||
)
|
||||
|
||||
func Run(ds *components.DemoShared) {
|
||||
t := time.NewTicker(time.Second)
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
ds.Update(i)
|
||||
i++
|
||||
case <-ds.Ctx.Done():
|
||||
t.Stop()
|
||||
ds.Update(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
95
examples/demo/demo.go
Normal file
95
examples/demo/demo.go
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package demo contains the core startup code for running a demo.
|
||||
package demo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/websocket"
|
||||
"open-match.dev/open-match/examples/demo/bytesub"
|
||||
"open-match.dev/open-match/examples/demo/components"
|
||||
"open-match.dev/open-match/examples/demo/updater"
|
||||
"open-match.dev/open-match/internal/config"
|
||||
"open-match.dev/open-match/internal/logging"
|
||||
"open-match.dev/open-match/internal/telemetry"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = logrus.WithFields(logrus.Fields{
|
||||
"app": "openmatch",
|
||||
"component": "examples.demo",
|
||||
})
|
||||
)
|
||||
|
||||
// Run starts the provided components, and hosts a webserver for observing the
|
||||
// output of those components.
|
||||
func Run(comps map[string]func(*components.DemoShared)) {
|
||||
cfg, err := config.Read()
|
||||
if err != nil {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"error": err.Error(),
|
||||
}).Fatalf("cannot read configuration.")
|
||||
}
|
||||
logging.ConfigureLogging(cfg)
|
||||
|
||||
logger.Info("Initializing Server")
|
||||
|
||||
fileServe := http.FileServer(http.Dir("/app/static"))
|
||||
http.Handle("/static/", http.StripPrefix("/static/", fileServe))
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fileServe.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
http.Handle(telemetry.HealthCheckEndpoint, telemetry.NewAlwaysReadyHealthCheck())
|
||||
|
||||
bs := bytesub.New()
|
||||
u := updater.New(context.Background(), func(b []byte) {
|
||||
var out bytes.Buffer
|
||||
err := json.Indent(&out, b, "", " ")
|
||||
if err == nil {
|
||||
bs.AnnounceLatest(out.Bytes())
|
||||
} else {
|
||||
bs.AnnounceLatest(b)
|
||||
}
|
||||
})
|
||||
|
||||
http.Handle("/connect", websocket.Handler(func(ws *websocket.Conn) {
|
||||
bs.Subscribe(ws.Request().Context(), ws)
|
||||
}))
|
||||
|
||||
logger.Info("Starting Server")
|
||||
|
||||
for name, f := range comps {
|
||||
go f(&components.DemoShared{
|
||||
Ctx: context.Background(),
|
||||
Cfg: cfg,
|
||||
Update: u.ForField(name),
|
||||
})
|
||||
}
|
||||
|
||||
address := fmt.Sprintf(":%d", cfg.GetInt("api.demo.httpport"))
|
||||
err = http.ListenAndServe(address, nil)
|
||||
logger.WithError(err).Warning("HTTP server closed.")
|
||||
}
|
44
examples/demo/static/dashboard.js
Normal file
44
examples/demo/static/dashboard.js
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
window.onload = function() {
|
||||
let protocol = "ws://";
|
||||
if (window.location.protocol == "https:") {
|
||||
protocol = "wss://";
|
||||
}
|
||||
const ws = new WebSocket(protocol + window.location.host + "/connect");
|
||||
|
||||
ws.onopen = function (event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ws.onmessage = function (event) {
|
||||
document.getElementById("content").textContent = event.data;
|
||||
return false;
|
||||
}
|
||||
|
||||
ws.onerror = function (event) {
|
||||
console.log("ERROR!");
|
||||
console.log(event);
|
||||
document.getElementById("error").textContent = event.toString();
|
||||
return false;
|
||||
}
|
||||
|
||||
ws.onclose = function (event) {
|
||||
console.log("WS CLOSED!");
|
||||
console.log(event);
|
||||
document.getElementById("error").textContent = event.toString();
|
||||
return false;
|
||||
}
|
||||
}
|
22
examples/demo/static/index.html
Normal file
22
examples/demo/static/index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!--
|
||||
Copyright 2019 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. -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="/static/dashboard.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<pre id="error" style="background-color: #FAA"></pre>
|
||||
<pre id="content"></pre>
|
||||
</body>
|
||||
</html>
|
137
examples/demo/updater/updater.go
Normal file
137
examples/demo/updater/updater.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package updater provides the ability for concurrently running demo pieces to
|
||||
// update a shared json object.
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// Updater is like a json object, with each field allowed to be updated
|
||||
// concurrently by a different process. After processing updates, Updater will
|
||||
// call a provided method with the json serialized value of all of its fields.
|
||||
type Updater struct {
|
||||
ctx context.Context
|
||||
children map[string]*json.RawMessage
|
||||
updates chan update
|
||||
set SetFunc
|
||||
}
|
||||
|
||||
// SetFunc serializes the value passed in into json and sets the associated field
|
||||
// to that value. If nil is passed (BUT NOT a nil value of an interface), the
|
||||
// field will be removed from the Updater's json object.
|
||||
type SetFunc func(v interface{})
|
||||
|
||||
// New creates an Updater. Set is called when fields update, using the json
|
||||
// sererialized value of Updater's tree. All updates after ctx is canceled are
|
||||
// ignored.
|
||||
func New(ctx context.Context, set func([]byte)) *Updater {
|
||||
f := func(v interface{}) {
|
||||
set([]byte(*forceMarshalJson(v)))
|
||||
}
|
||||
return NewNested(ctx, SetFunc(f))
|
||||
}
|
||||
|
||||
// NewNested creates an updater based on a field in another updater. This
|
||||
// allows for grouping of related demo pieces into a single conceptual group.
|
||||
func NewNested(ctx context.Context, set SetFunc) *Updater {
|
||||
u := create(ctx, set)
|
||||
go u.start()
|
||||
return u
|
||||
}
|
||||
|
||||
func create(ctx context.Context, set SetFunc) *Updater {
|
||||
return &Updater{
|
||||
ctx: ctx,
|
||||
children: make(map[string]*json.RawMessage),
|
||||
updates: make(chan update),
|
||||
set: set,
|
||||
}
|
||||
}
|
||||
|
||||
// ForField returns a function to set the latest value of that demo piece.
|
||||
func (u *Updater) ForField(field string) SetFunc {
|
||||
return SetFunc(func(v interface{}) {
|
||||
var r *json.RawMessage
|
||||
if v != nil {
|
||||
r = forceMarshalJson(v)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
case u.updates <- update{field, r}:
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (u *Updater) start() {
|
||||
for {
|
||||
u.set(u.children)
|
||||
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
u.set(nil)
|
||||
return
|
||||
case up := <-u.updates:
|
||||
if up.value == nil {
|
||||
delete(u.children, up.field)
|
||||
} else {
|
||||
u.children[up.field] = up.value
|
||||
}
|
||||
}
|
||||
|
||||
applyAllWaitingUpdates:
|
||||
for {
|
||||
select {
|
||||
case up := <-u.updates:
|
||||
if up.value == nil {
|
||||
delete(u.children, up.field)
|
||||
} else {
|
||||
u.children[up.field] = up.value
|
||||
}
|
||||
default:
|
||||
break applyAllWaitingUpdates
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type update struct {
|
||||
field string
|
||||
value *json.RawMessage
|
||||
}
|
||||
|
||||
// forceMarshalJson is like json.Marshal, but cannot fail. It will instead
|
||||
// encode any error encountered into the json object on the field Error.
|
||||
func forceMarshalJson(v interface{}) *json.RawMessage {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
e := struct {
|
||||
Error string
|
||||
}{
|
||||
err.Error(),
|
||||
}
|
||||
|
||||
b, err = json.Marshal(e)
|
||||
if err != nil {
|
||||
b = []byte("{\"Error\":\"There was an error encoding the json message, additional there was an error encoding that error message.\"}")
|
||||
}
|
||||
}
|
||||
|
||||
r := json.RawMessage(b)
|
||||
return &r
|
||||
}
|
131
examples/demo/updater/updater_test.go
Normal file
131
examples/demo/updater/updater_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdater(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
latest := make(chan string)
|
||||
|
||||
base := New(ctx, func(b []byte) {
|
||||
latest <- string(b)
|
||||
})
|
||||
|
||||
l := <-latest
|
||||
if l != "{}" {
|
||||
t.Errorf("Got %s, expected %s", l, "{}")
|
||||
}
|
||||
|
||||
child := NewNested(ctx, base.ForField("Foo"))
|
||||
|
||||
l = <-latest
|
||||
if l != "{\"Foo\":{}}" {
|
||||
t.Errorf("Got %s, expected %s", l, "{\"Foo\":{}}")
|
||||
}
|
||||
|
||||
child.ForField("Bar")(interface{}((*int)(nil)))
|
||||
|
||||
l = <-latest
|
||||
if l != "{\"Foo\":{\"Bar\":null}}" {
|
||||
t.Errorf("Got %s, expected %s", l, "{\"Foo\":{\"Bar\":null}}")
|
||||
}
|
||||
|
||||
child.ForField("Bar")(nil)
|
||||
|
||||
l = <-latest
|
||||
if l != "{\"Foo\":{}}" {
|
||||
t.Errorf("Got %s, expected %s", l, "{\"Foo\":{}}")
|
||||
}
|
||||
}
|
||||
|
||||
// Fully testing the updater's logic is difficult because it combines multiple
|
||||
// calls. This test method creates 100 different go routines all trying to
|
||||
// update a value to force the logic to be invoked.
|
||||
func TestUpdaterInternal(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
cancel()
|
||||
}()
|
||||
|
||||
latest := ""
|
||||
|
||||
set := SetFunc(func(v interface{}) {
|
||||
if v != nil {
|
||||
latest = string(*forceMarshalJson(v))
|
||||
}
|
||||
})
|
||||
|
||||
u := create(ctx, set)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
set := u.ForField(strconv.Itoa(i))
|
||||
go func() {
|
||||
set("Hi")
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Blocking call ensures that canceling the context will clean up the internal go routine.
|
||||
u.start()
|
||||
|
||||
expectedMap := make(map[string]string)
|
||||
for i := 0; i < 100; i++ {
|
||||
expectedMap[strconv.Itoa(i)] = "Hi"
|
||||
}
|
||||
// Not using forceMashal because it by design hides errors, and is used in the
|
||||
// code being tested.
|
||||
expectedB, err := json.Marshal(expectedMap)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expected := string(expectedB)
|
||||
|
||||
if latest != expected {
|
||||
t.Errorf("latest value is wrong. Expected '%s', got '%s'", expected, latest)
|
||||
}
|
||||
}
|
||||
|
||||
var marshalTests = []struct {
|
||||
in interface{}
|
||||
out string
|
||||
}{
|
||||
{map[string]int{"hi": 1}, "{\"hi\":1}"},
|
||||
{make(chan int), "{\"Error\":\"json: unsupported type: chan int\"}"},
|
||||
}
|
||||
|
||||
func TestForceMarshalJson(t *testing.T) {
|
||||
for _, tt := range marshalTests {
|
||||
t.Run(tt.out, func(t *testing.T) {
|
||||
s := string(*forceMarshalJson(tt.in))
|
||||
if s != tt.out {
|
||||
t.Errorf("got %s, want %s", s, tt.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
24
examples/evaluator/golang/simple/Dockerfile
Normal file
24
examples/evaluator/golang/simple/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/examples/evaluator/golang/simple
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o simple .
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /app/
|
||||
COPY --from=builder --chown=nonroot /go/src/open-match.dev/open-match/examples/evaluator/golang/simple/simple /app/
|
||||
|
||||
ENTRYPOINT ["/app/simple"]
|
54
examples/evaluator/golang/simple/evaluate/evaluator.go
Normal file
54
examples/evaluator/golang/simple/evaluate/evaluator.go
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package evaluate
|
||||
|
||||
import (
|
||||
"open-match.dev/open-match/examples"
|
||||
harness "open-match.dev/open-match/pkg/harness/evaluator/golang"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
)
|
||||
|
||||
// Evaluate is where your custom evaluation logic lives.
|
||||
// This sample evaluator sorts and deduplicates the input matches.
|
||||
func Evaluate(p *harness.EvaluatorParams) ([]*pb.Match, error) {
|
||||
scoreInDescendingOrder := func(a, b *pb.Match) bool {
|
||||
return a.GetProperties().GetFields()[examples.MatchScore].GetNumberValue() > b.GetProperties().GetFields()[examples.MatchScore].GetNumberValue()
|
||||
}
|
||||
by(scoreInDescendingOrder).Sort(p.Matches)
|
||||
|
||||
results := []*pb.Match{}
|
||||
dedup := map[string]bool{}
|
||||
|
||||
for _, match := range p.Matches {
|
||||
if isNonCollidingMatch(match, dedup) {
|
||||
for _, ticket := range match.GetTickets() {
|
||||
dedup[ticket.GetId()] = true
|
||||
}
|
||||
results = append(results, match)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func isNonCollidingMatch(match *pb.Match, validTickets map[string]bool) bool {
|
||||
for _, ticket := range match.GetTickets() {
|
||||
id := ticket.GetId()
|
||||
if _, ok := validTickets[id]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
105
examples/evaluator/golang/simple/evaluate/evaluator_test.go
Normal file
105
examples/evaluator/golang/simple/evaluate/evaluator_test.go
Normal file
@ -0,0 +1,105 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package evaluate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"open-match.dev/open-match/examples"
|
||||
harness "open-match.dev/open-match/pkg/harness/evaluator/golang"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
"open-match.dev/open-match/pkg/structs"
|
||||
)
|
||||
|
||||
func TestEvaluate(t *testing.T) {
|
||||
ticket1 := &pb.Ticket{Id: "1"}
|
||||
ticket2 := &pb.Ticket{Id: "2"}
|
||||
ticket3 := &pb.Ticket{Id: "3"}
|
||||
|
||||
ticket12Score1 := &pb.Match{
|
||||
Tickets: []*pb.Ticket{ticket1, ticket2},
|
||||
Properties: structs.Struct{
|
||||
examples.MatchScore: structs.Number(1),
|
||||
}.S(),
|
||||
}
|
||||
|
||||
ticket12Score10 := &pb.Match{
|
||||
Tickets: []*pb.Ticket{ticket2, ticket1},
|
||||
Properties: structs.Struct{
|
||||
examples.MatchScore: structs.Number(10),
|
||||
}.S(),
|
||||
}
|
||||
|
||||
ticket123Score5 := &pb.Match{
|
||||
Tickets: []*pb.Ticket{ticket1, ticket2, ticket3},
|
||||
Properties: structs.Struct{
|
||||
examples.MatchScore: structs.Number(5),
|
||||
}.S(),
|
||||
}
|
||||
|
||||
ticket3Score50 := &pb.Match{
|
||||
Tickets: []*pb.Ticket{ticket3},
|
||||
Properties: structs.Struct{
|
||||
examples.MatchScore: structs.Number(50),
|
||||
}.S(),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
testMatches []*pb.Match
|
||||
wantMatches []*pb.Match
|
||||
}{
|
||||
{
|
||||
description: "test empty request returns empty response",
|
||||
testMatches: []*pb.Match{},
|
||||
wantMatches: []*pb.Match{},
|
||||
},
|
||||
{
|
||||
description: "test input matches output when receiving one match",
|
||||
testMatches: []*pb.Match{ticket12Score1},
|
||||
wantMatches: []*pb.Match{ticket12Score1},
|
||||
},
|
||||
{
|
||||
description: "test deduplicates and expect the one with higher score",
|
||||
testMatches: []*pb.Match{ticket12Score1, ticket12Score10},
|
||||
wantMatches: []*pb.Match{ticket12Score10},
|
||||
},
|
||||
{
|
||||
description: "test first returns matches with higher score",
|
||||
testMatches: []*pb.Match{ticket123Score5, ticket12Score10},
|
||||
wantMatches: []*pb.Match{ticket12Score10},
|
||||
},
|
||||
{
|
||||
description: "test evaluator returns two matches with the highest score",
|
||||
testMatches: []*pb.Match{ticket12Score1, ticket12Score10, ticket123Score5, ticket3Score50},
|
||||
wantMatches: []*pb.Match{ticket12Score10, ticket3Score50},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.description, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
gotMatches, err := Evaluate(&harness.EvaluatorParams{Matches: test.testMatches})
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(test.wantMatches), len(gotMatches))
|
||||
for _, match := range gotMatches {
|
||||
assert.Contains(t, test.wantMatches, match)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
53
examples/evaluator/golang/simple/evaluate/sort_match.go
Normal file
53
examples/evaluator/golang/simple/evaluate/sort_match.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package evaluate
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
)
|
||||
|
||||
// by is the type of a "less" function that defines the ordering of its Planet arguments.
|
||||
type by func(p1, p2 *pb.Match) bool
|
||||
|
||||
// matchSorter joins a By function and a slice of Matches to be sorted.
|
||||
type matchSorter struct {
|
||||
matches []*pb.Match
|
||||
by func(a, b *pb.Match) bool // Closure used in the Less method.
|
||||
}
|
||||
|
||||
// Sort is a method on the function type, By, that sorts the argument slice according to the function.
|
||||
func (by by) Sort(matches []*pb.Match) {
|
||||
sort.Sort(&matchSorter{
|
||||
matches: matches,
|
||||
by: by, // The Sort method's receiver is the function (closure) that defines the sort order.
|
||||
})
|
||||
}
|
||||
|
||||
// Len is part of sort.Interface.
|
||||
func (s *matchSorter) Len() int {
|
||||
return len(s.matches)
|
||||
}
|
||||
|
||||
// Swap is part of sort.Interface.
|
||||
func (s *matchSorter) Swap(i, j int) {
|
||||
s.matches[i], s.matches[j] = s.matches[j], s.matches[i]
|
||||
}
|
||||
|
||||
// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter.
|
||||
func (s *matchSorter) Less(i, j int) bool {
|
||||
return s.by(s.matches[i], s.matches[j])
|
||||
}
|
24
examples/evaluator/golang/simple/main.go
Normal file
24
examples/evaluator/golang/simple/main.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2019 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.
|
||||
package main
|
||||
|
||||
import (
|
||||
simple "open-match.dev/open-match/examples/evaluator/golang/simple/evaluate"
|
||||
harness "open-match.dev/open-match/pkg/harness/evaluator/golang"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Invoke the harness to setup a GRPC service that handles requests to run the evaluator.
|
||||
harness.RunEvaluator(simple.Evaluate)
|
||||
}
|
24
examples/functions/golang/pool/Dockerfile
Normal file
24
examples/functions/golang/pool/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/examples/functions/golang/pool
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o matchfunction .
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /app/
|
||||
COPY --from=builder --chown=nonroot /go/src/open-match.dev/open-match/examples/functions/golang/pool/matchfunction /app/
|
||||
|
||||
ENTRYPOINT ["/app/matchfunction"]
|
35
examples/functions/golang/pool/main.go
Normal file
35
examples/functions/golang/pool/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package main a sample match function that uses the GRPC harness to set up
|
||||
// the match making function as a service. This sample is a reference
|
||||
// to demonstrate the usage of the GRPC harness and should only be used as
|
||||
// a starting point for your match function. You will need to modify the
|
||||
// matchmaking logic in this function based on your game's requirements.
|
||||
package main
|
||||
|
||||
import (
|
||||
pool "open-match.dev/open-match/examples/functions/golang/pool/mmf"
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Invoke the harness to setup a GRPC service that handles requests to run the
|
||||
// match function. The harness itself queries open match for player pools for
|
||||
// the specified request and passes the pools to the match function to generate
|
||||
// proposals.
|
||||
mmfHarness.RunMatchFunction(&mmfHarness.FunctionSettings{
|
||||
Func: pool.MakeMatches,
|
||||
})
|
||||
}
|
74
examples/functions/golang/pool/mmf/matchfunction.go
Normal file
74
examples/functions/golang/pool/mmf/matchfunction.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package mmf provides a sample match function that uses the GRPC harness to set up
|
||||
// the match making function as a service. This sample is a reference
|
||||
// to demonstrate the usage of the GRPC harness and should only be used as
|
||||
// a starting point for your match function. You will need to modify the
|
||||
// matchmaking logic in this function based on your game's requirements.
|
||||
package mmf
|
||||
|
||||
import (
|
||||
"github.com/rs/xid"
|
||||
"open-match.dev/open-match/examples"
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
"open-match.dev/open-match/pkg/structs"
|
||||
)
|
||||
|
||||
var (
|
||||
matchName = "pool-based-match"
|
||||
)
|
||||
|
||||
// MakeMatches is where your custom matchmaking logic lives.
|
||||
// This is the core match making function that will be triggered by Open Match to generate matches.
|
||||
// The goal of this function is to generate predictable matches that can be validated without flakyness.
|
||||
// This match function loops through all the pools and generates one match per pool aggregating all players
|
||||
// in that pool in the generated match.
|
||||
func MakeMatches(params *mmfHarness.MatchFunctionParams) ([]*pb.Match, error) {
|
||||
var result []*pb.Match
|
||||
for pool, tickets := range params.PoolNameToTickets {
|
||||
if len(tickets) != 0 {
|
||||
roster := &pb.Roster{Name: pool}
|
||||
|
||||
for _, ticket := range tickets {
|
||||
roster.TicketIds = append(roster.GetTicketIds(), ticket.GetId())
|
||||
}
|
||||
|
||||
result = append(result, &pb.Match{
|
||||
MatchId: xid.New().String(),
|
||||
MatchProfile: params.ProfileName,
|
||||
MatchFunction: matchName,
|
||||
Tickets: tickets,
|
||||
Rosters: []*pb.Roster{roster},
|
||||
Properties: structs.Struct{
|
||||
examples.MatchScore: structs.Number(scoreCalculator(tickets)),
|
||||
}.S(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// This match function defines the quality of a match as the sum of the attribute values of all tickets per match
|
||||
func scoreCalculator(tickets []*pb.Ticket) float64 {
|
||||
matchScore := 0.0
|
||||
for _, ticket := range tickets {
|
||||
for _, v := range ticket.GetProperties().GetFields() {
|
||||
matchScore += v.GetNumberValue()
|
||||
}
|
||||
}
|
||||
return matchScore
|
||||
}
|
109
examples/functions/golang/pool/mmf/matchfunction_test.go
Normal file
109
examples/functions/golang/pool/mmf/matchfunction_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package mmf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"open-match.dev/open-match/examples"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
"open-match.dev/open-match/pkg/structs"
|
||||
)
|
||||
|
||||
func TestMakeMatches(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
tickets := []*pb.Ticket{
|
||||
{
|
||||
Id: "1",
|
||||
Properties: structs.Struct{
|
||||
"level": structs.Number(10),
|
||||
"defense": structs.Number(100),
|
||||
}.S(),
|
||||
},
|
||||
{
|
||||
Id: "2",
|
||||
Properties: structs.Struct{
|
||||
"level": structs.Number(10),
|
||||
"attack": structs.Number(50),
|
||||
}.S(),
|
||||
},
|
||||
{
|
||||
Id: "3",
|
||||
Properties: structs.Struct{
|
||||
"level": structs.Number(10),
|
||||
"speed": structs.Number(522),
|
||||
}.S(),
|
||||
}, {
|
||||
Id: "4",
|
||||
Properties: structs.Struct{
|
||||
"level": structs.Number(10),
|
||||
"mana": structs.Number(1),
|
||||
}.S(),
|
||||
},
|
||||
}
|
||||
|
||||
poolNameToTickets := map[string][]*pb.Ticket{
|
||||
"pool1": tickets[:2],
|
||||
"pool2": tickets[2:],
|
||||
}
|
||||
|
||||
p := &mmfHarness.MatchFunctionParams{
|
||||
Logger: &logrus.Entry{},
|
||||
ProfileName: "test-profile",
|
||||
Rosters: []*pb.Roster{},
|
||||
PoolNameToTickets: poolNameToTickets,
|
||||
}
|
||||
|
||||
matches, err := MakeMatches(p)
|
||||
assert.Nil(err)
|
||||
assert.Equal(len(matches), 2)
|
||||
|
||||
actual := []*pb.Match{}
|
||||
for _, match := range matches {
|
||||
actual = append(actual, &pb.Match{
|
||||
MatchProfile: match.MatchProfile,
|
||||
MatchFunction: match.MatchFunction,
|
||||
Tickets: match.Tickets,
|
||||
Rosters: match.Rosters,
|
||||
Properties: match.Properties,
|
||||
})
|
||||
}
|
||||
|
||||
matchGen := func(poolName string, tickets []*pb.Ticket) *pb.Match {
|
||||
tids := []string{}
|
||||
for _, ticket := range tickets {
|
||||
tids = append(tids, ticket.GetId())
|
||||
}
|
||||
|
||||
return &pb.Match{
|
||||
MatchProfile: p.ProfileName,
|
||||
MatchFunction: matchName,
|
||||
Tickets: tickets,
|
||||
Rosters: []*pb.Roster{{Name: poolName, TicketIds: tids}},
|
||||
Properties: structs.Struct{
|
||||
examples.MatchScore: structs.Number(scoreCalculator(tickets)),
|
||||
}.S(),
|
||||
}
|
||||
}
|
||||
|
||||
for poolName, tickets := range poolNameToTickets {
|
||||
assert.Contains(actual, matchGen(poolName, tickets))
|
||||
}
|
||||
}
|
24
examples/functions/golang/rosterbased/Dockerfile
Normal file
24
examples/functions/golang/rosterbased/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/examples/functions/golang/rosterbased
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o matchfunction .
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /app/
|
||||
COPY --from=builder --chown=nonroot /go/src/open-match.dev/open-match/examples/functions/golang/rosterbased/matchfunction /app/
|
||||
|
||||
ENTRYPOINT ["/app/matchfunction"]
|
35
examples/functions/golang/rosterbased/main.go
Normal file
35
examples/functions/golang/rosterbased/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package main defines a sample match function that uses the GRPC harness to set up
|
||||
// the match making function as a service. This sample is a reference
|
||||
// to demonstrate the usage of the GRPC harness and should only be used as
|
||||
// a starting point for your match function. You will need to modify the
|
||||
// matchmaking logic in this function based on your game's requirements.
|
||||
package main
|
||||
|
||||
import (
|
||||
rosterbased "open-match.dev/open-match/examples/functions/golang/rosterbased/mmf"
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Invoke the harness to setup a GRPC service that handles requests to run the
|
||||
// match function. The harness itself queries open match for player pools for
|
||||
// the specified request and passes the pools to the match function to generate
|
||||
// proposals.
|
||||
mmfHarness.RunMatchFunction(&mmfHarness.FunctionSettings{
|
||||
Func: rosterbased.MakeMatches,
|
||||
})
|
||||
}
|
119
examples/functions/golang/rosterbased/mmf/matchfunction.go
Normal file
119
examples/functions/golang/rosterbased/mmf/matchfunction.go
Normal file
@ -0,0 +1,119 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package mmf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
matchName = "roster-based-matchfunction"
|
||||
emptyRosterSpot = "EMPTY_ROSTER_SPOT"
|
||||
logger = logrus.WithFields(logrus.Fields{
|
||||
"app": "openmatch",
|
||||
"component": "mmf.rosterbased",
|
||||
})
|
||||
)
|
||||
|
||||
func MakeMatches(p *mmfHarness.MatchFunctionParams) ([]*pb.Match, error) {
|
||||
// This roster based match function expects the match profile to have a
|
||||
// populated roster specifying the empty slots for each pool name and also
|
||||
// have the ticket pools referenced in the roster. It generates matches by
|
||||
// populating players from the specified pools into rosters.
|
||||
wantTickets, err := wantPoolTickets(p.Rosters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matches []*pb.Match
|
||||
for {
|
||||
insufficientTickets := false
|
||||
matchTickets := []*pb.Ticket{}
|
||||
matchRosters := []*pb.Roster{}
|
||||
// Loop through each pool wanted in the rosters and pick the number of
|
||||
// wanted players from the respective Pool.
|
||||
for poolName, want := range wantTickets {
|
||||
have, ok := p.PoolNameToTickets[poolName]
|
||||
if !ok {
|
||||
// A wanted Pool was not found in the Pools specified in the profile.
|
||||
insufficientTickets = true
|
||||
break
|
||||
}
|
||||
|
||||
if len(have) < want {
|
||||
// The Pool in the profile has fewer tickets than what the roster needs.
|
||||
insufficientTickets = true
|
||||
break
|
||||
}
|
||||
|
||||
// Populate the wanted tickets from the Tickets in the corresponding Pool.
|
||||
matchTickets = append(matchTickets, have[0:want]...)
|
||||
p.PoolNameToTickets[poolName] = have[want:]
|
||||
var ids []string
|
||||
for _, ticket := range matchTickets {
|
||||
ids = append(ids, ticket.Id)
|
||||
}
|
||||
|
||||
matchRosters = append(matchRosters, &pb.Roster{
|
||||
Name: poolName,
|
||||
TicketIds: ids,
|
||||
})
|
||||
}
|
||||
|
||||
if insufficientTickets {
|
||||
// Ran out of Tickets. Matches cannot be created from the remaining Tickets.
|
||||
break
|
||||
}
|
||||
|
||||
matches = append(matches, &pb.Match{
|
||||
MatchId: fmt.Sprintf("profile-%v-time-%v", p.ProfileName, time.Now().Format("2006-01-02T15:04:05.00")),
|
||||
MatchProfile: p.ProfileName,
|
||||
MatchFunction: matchName,
|
||||
Tickets: matchTickets,
|
||||
Rosters: matchRosters,
|
||||
})
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
// wantPoolTickets parses the roster to return a map of the Pool name to the
|
||||
// number of empty roster slots for that Pool.
|
||||
func wantPoolTickets(rosters []*pb.Roster) (map[string]int, error) {
|
||||
wantTickets := make(map[string]int)
|
||||
for _, r := range rosters {
|
||||
if _, ok := wantTickets[r.Name]; ok {
|
||||
// We do not expect multiple Roster Pools to have the same name.
|
||||
logger.Errorf("multiple rosters with same name not supported")
|
||||
return nil, status.Error(codes.InvalidArgument, "multiple rosters with same name not supported")
|
||||
}
|
||||
|
||||
wantTickets[r.Name] = 0
|
||||
for _, slot := range r.TicketIds {
|
||||
if slot == emptyRosterSpot {
|
||||
wantTickets[r.Name] = wantTickets[r.Name] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wantTickets, nil
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/examples/functions/golang/simple
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o matchfunction .
|
||||
|
||||
FROM gcr.io/distroless/static
|
||||
COPY --from=builder /go/src/open-match.dev/open-match/examples/functions/golang/simple/matchfunction .
|
||||
|
||||
ENTRYPOINT ["/matchfunction"]
|
@ -1,79 +0,0 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package main a sample match function that uses the GRPC harness to set up
|
||||
// the match making function as a service. This sample is a reference
|
||||
// to demonstrate the usage of the GRPC harness and should only be used as
|
||||
// a starting point for your match function. You will need to modify the
|
||||
// matchmaking logic in this function based on your game's requirements.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
goHarness "open-match.dev/open-match/internal/harness/golang"
|
||||
"open-match.dev/open-match/internal/pb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Invoke the harness to setup a GRPC service that handles requests to run the
|
||||
// match function. The harness itself queries open match for player pools for
|
||||
// the specified request and passes the pools to the match function to generate
|
||||
// proposals.
|
||||
goHarness.RunMatchFunction(&goHarness.FunctionSettings{
|
||||
FunctionName: "simple-matchfunction",
|
||||
Func: makeMatches,
|
||||
})
|
||||
}
|
||||
|
||||
// makeMatches is where your custom matchmaking logic lives.
|
||||
func makeMatches(p *goHarness.MatchFunctionParams) []*pb.Match {
|
||||
// This simple match function does the following things
|
||||
// 1. Deduplicates the tickets from the pools into a single list.
|
||||
// 2. Groups players into 1v1 matches.
|
||||
|
||||
tickets := map[string]*pb.Ticket{}
|
||||
|
||||
for _, pool := range p.PoolNameToTickets {
|
||||
for _, ticket := range pool {
|
||||
tickets[ticket.Id] = ticket
|
||||
}
|
||||
}
|
||||
|
||||
var matches []*pb.Match
|
||||
|
||||
t := time.Now().Format("2006-01-02T15:04:05.00")
|
||||
|
||||
thisMatch := make([]*pb.Ticket, 0, 2)
|
||||
matchNum := 0
|
||||
|
||||
for _, ticket := range tickets {
|
||||
thisMatch = append(thisMatch, ticket)
|
||||
|
||||
if len(thisMatch) >= 2 {
|
||||
matches = append(matches, &pb.Match{
|
||||
MatchId: fmt.Sprintf("profile-%s-time-%s-num-%d", p.ProfileName, t, matchNum),
|
||||
MatchProfile: p.ProfileName,
|
||||
MatchFunction: "a-simple-matchfunction",
|
||||
Ticket: thisMatch,
|
||||
})
|
||||
|
||||
thisMatch = make([]*pb.Ticket, 0, 2)
|
||||
matchNum++
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
24
examples/functions/golang/soloduel/Dockerfile
Normal file
24
examples/functions/golang/soloduel/Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright 2019 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.
|
||||
|
||||
FROM open-match-base-build as builder
|
||||
|
||||
WORKDIR /go/src/open-match.dev/open-match/examples/functions/golang/soloduel
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o matchfunction .
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
WORKDIR /app/
|
||||
COPY --from=builder --chown=nonroot /go/src/open-match.dev/open-match/examples/functions/golang/soloduel/matchfunction /app/
|
||||
|
||||
ENTRYPOINT ["/app/matchfunction"]
|
35
examples/functions/golang/soloduel/main.go
Normal file
35
examples/functions/golang/soloduel/main.go
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package main defines a sample match function that uses the GRPC harness to set up
|
||||
// the match making function as a service. This sample is a reference
|
||||
// to demonstrate the usage of the GRPC harness and should only be used as
|
||||
// a starting point for your match function. You will need to modify the
|
||||
// matchmaking logic in this function based on your game's requirements.
|
||||
package main
|
||||
|
||||
import (
|
||||
soloduel "open-match.dev/open-match/examples/functions/golang/soloduel/mmf"
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Invoke the harness to setup a GRPC service that handles requests to run the
|
||||
// match function. The harness itself queries open match for player pools for
|
||||
// the specified request and passes the pools to the match function to generate
|
||||
// proposals.
|
||||
mmfHarness.RunMatchFunction(&mmfHarness.FunctionSettings{
|
||||
Func: soloduel.MakeMatches,
|
||||
})
|
||||
}
|
71
examples/functions/golang/soloduel/mmf/matchfunction.go
Normal file
71
examples/functions/golang/soloduel/mmf/matchfunction.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Package mmf provides a sample match function that uses the GRPC harness to set up 1v1 matches.
|
||||
// This sample is a reference to demonstrate the usage of the GRPC harness and should only be used as
|
||||
// a starting point for your match function. You will need to modify the
|
||||
// matchmaking logic in this function based on your game's requirements.
|
||||
package mmf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
matchName = "a-simple-1v1-matchfunction"
|
||||
)
|
||||
|
||||
// MakeMatches is where your custom matchmaking logic lives.
|
||||
func MakeMatches(p *mmfHarness.MatchFunctionParams) ([]*pb.Match, error) {
|
||||
// This simple match function does the following things
|
||||
// 1. Deduplicates the tickets from the pools into a single list.
|
||||
// 2. Groups players into 1v1 matches.
|
||||
|
||||
tickets := map[string]*pb.Ticket{}
|
||||
|
||||
for _, pool := range p.PoolNameToTickets {
|
||||
for _, ticket := range pool {
|
||||
tickets[ticket.GetId()] = ticket
|
||||
}
|
||||
}
|
||||
|
||||
var matches []*pb.Match
|
||||
|
||||
t := time.Now().Format("2006-01-02T15:04:05.00")
|
||||
|
||||
thisMatch := make([]*pb.Ticket, 0, 2)
|
||||
matchNum := 0
|
||||
|
||||
for _, ticket := range tickets {
|
||||
thisMatch = append(thisMatch, ticket)
|
||||
|
||||
if len(thisMatch) >= 2 {
|
||||
matches = append(matches, &pb.Match{
|
||||
MatchId: fmt.Sprintf("profile-%s-time-%s-num-%d", p.ProfileName, t, matchNum),
|
||||
MatchProfile: p.ProfileName,
|
||||
MatchFunction: matchName,
|
||||
Tickets: thisMatch,
|
||||
})
|
||||
|
||||
thisMatch = make([]*pb.Ticket, 0, 2)
|
||||
matchNum++
|
||||
}
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
73
examples/functions/golang/soloduel/mmf/matchfunction_test.go
Normal file
73
examples/functions/golang/soloduel/mmf/matchfunction_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package mmf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
mmfHarness "open-match.dev/open-match/pkg/harness/function/golang"
|
||||
)
|
||||
|
||||
func TestMakeMatchesDeduplicate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
poolNameToTickets := map[string][]*pb.Ticket{
|
||||
"pool1": {{Id: "1"}},
|
||||
"pool2": {{Id: "1"}},
|
||||
}
|
||||
|
||||
p := &mmfHarness.MatchFunctionParams{
|
||||
Logger: &logrus.Entry{},
|
||||
ProfileName: "test-profile",
|
||||
Rosters: []*pb.Roster{},
|
||||
PoolNameToTickets: poolNameToTickets,
|
||||
}
|
||||
|
||||
matches, err := MakeMatches(p)
|
||||
assert.Nil(err)
|
||||
assert.Equal(len(matches), 0)
|
||||
}
|
||||
|
||||
func TestMakeMatches(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
poolNameToTickets := map[string][]*pb.Ticket{
|
||||
"pool1": {{Id: "1"}, {Id: "2"}, {Id: "3"}},
|
||||
"pool2": {{Id: "4"}},
|
||||
"pool3": {{Id: "5"}, {Id: "6"}, {Id: "7"}},
|
||||
}
|
||||
|
||||
p := &mmfHarness.MatchFunctionParams{
|
||||
Logger: &logrus.Entry{},
|
||||
ProfileName: "test-profile",
|
||||
Rosters: []*pb.Roster{},
|
||||
PoolNameToTickets: poolNameToTickets,
|
||||
}
|
||||
|
||||
matches, err := MakeMatches(p)
|
||||
assert.Nil(err)
|
||||
assert.Equal(len(matches), 3)
|
||||
|
||||
for _, match := range matches {
|
||||
assert.Equal(2, len(match.Tickets))
|
||||
assert.Equal(matchName, match.MatchFunction)
|
||||
assert.Equal(p.ProfileName, match.MatchProfile)
|
||||
assert.Nil(match.Rosters)
|
||||
}
|
||||
}
|
183
examples/scale/backend/backend.go
Normal file
183
examples/scale/backend/backend.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"open-match.dev/open-match/examples/scale/profiles"
|
||||
"open-match.dev/open-match/internal/config"
|
||||
"open-match.dev/open-match/internal/logging"
|
||||
"open-match.dev/open-match/internal/rpc"
|
||||
"open-match.dev/open-match/pkg/pb"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = logrus.WithFields(logrus.Fields{
|
||||
"app": "openmatch",
|
||||
"component": "scale.backend",
|
||||
})
|
||||
|
||||
// TODO: Add metrics to track matches created, tickets assigned, deleted.
|
||||
matchCount uint64
|
||||
assigned uint64
|
||||
deleted uint64
|
||||
)
|
||||
|
||||
// Run triggers execution of functions that continuously fetch, assign and
|
||||
// delete matches.
|
||||
func Run() {
|
||||
cfg, err := config.Read()
|
||||
if err != nil {
|
||||
logger.WithFields(logrus.Fields{
|
||||
"error": err.Error(),
|
||||
}).Fatalf("cannot read configuration.")
|
||||
}
|
||||
|
||||
logging.ConfigureLogging(cfg)
|
||||
beConn, err := rpc.GRPCClientFromConfig(cfg, "api.backend")
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to connect to Open Match Backend, got %v", err)
|
||||
}
|
||||
|
||||
defer beConn.Close()
|
||||
be := pb.NewBackendClient(beConn)
|
||||
|
||||
feConn, err := rpc.GRPCClientFromConfig(cfg, "api.frontend")
|
||||
if err != nil {
|
||||
logger.Fatalf("failed to connect to Open Match Frontend, got %v", err)
|
||||
}
|
||||
|
||||
defer feConn.Close()
|
||||
fe := pb.NewFrontendClient(feConn)
|
||||
|
||||
// The buffered channels attempt to decouple fetch, assign and delete. It is
|
||||
// best effort and these operations may still block each other if buffers are full.
|
||||
matches := make(chan *pb.Match, 1000)
|
||||
deleteIds := make(chan string, 1000)
|
||||
|
||||
go doFetch(cfg, be, matches)
|
||||
go doAssign(be, matches, deleteIds)
|
||||
go doDelete(fe, deleteIds)
|
||||
|
||||
// The above goroutines run forever and so the main goroutine needs to block.
|
||||
select {}
|
||||
}
|
||||
|
||||
// doFetch continuously fetches all profiles in a loop and queues up the fetched
|
||||
// matches for assignment.
|
||||
func doFetch(cfg config.View, be pb.BackendClient, matches chan *pb.Match) {
|
||||
startTime := time.Now()
|
||||
mprofiles := profiles.Generate(cfg)
|
||||
for {
|
||||
var wg sync.WaitGroup
|
||||
for _, p := range mprofiles {
|
||||
wg.Add(1)
|
||||
p := p
|
||||
go func(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
fetch(be, p, matches)
|
||||
}(&wg)
|
||||
}
|
||||
|
||||
// Wait for all FetchMatches calls to complete before proceeding.
|
||||
wg.Wait()
|
||||
logger.Infof("FetchedMatches:%v, AssignedTickets:%v, DeletedTickets:%v in time %v", atomic.LoadUint64(&matchCount), atomic.LoadUint64(&assigned), atomic.LoadUint64(&deleted), time.Since(startTime))
|
||||
}
|
||||
}
|
||||
|
||||
func fetch(be pb.BackendClient, p *pb.MatchProfile, matches chan *pb.Match) {
|
||||
req := &pb.FetchMatchesRequest{
|
||||
Config: &pb.FunctionConfig{
|
||||
Host: "om-function",
|
||||
Port: 50502,
|
||||
Type: pb.FunctionConfig_GRPC,
|
||||
},
|
||||
Profiles: []*pb.MatchProfile{p},
|
||||
}
|
||||
|
||||
stream, err := be.FetchMatches(context.Background(), req)
|
||||
if err != nil {
|
||||
logger.Errorf("FetchMatches failed, got %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("FetchMatches failed, got %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
matches <- resp.GetMatch()
|
||||
atomic.AddUint64(&matchCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// doAssign continuously assigns matches that were queued in the matches channel
|
||||
// by doFetch and after successful assignment, queues all the tickets to deleteIds
|
||||
// channel for deletion by doDelete.
|
||||
func doAssign(be pb.BackendClient, matches chan *pb.Match, deleteIds chan string) {
|
||||
for match := range matches {
|
||||
ids := []string{}
|
||||
for _, t := range match.Tickets {
|
||||
ids = append(ids, t.Id)
|
||||
}
|
||||
|
||||
req := &pb.AssignTicketsRequest{
|
||||
TicketIds: ids,
|
||||
Assignment: &pb.Assignment{
|
||||
Connection: fmt.Sprintf("%d.%d.%d.%d:2222", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)),
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := be.AssignTickets(context.Background(), req); err != nil {
|
||||
logger.Errorf("AssignTickets failed, got %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
atomic.AddUint64(&assigned, uint64(len(ids)))
|
||||
for _, id := range ids {
|
||||
deleteIds <- id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doDelete deletes all the tickets whose ids get added to the deleteIds channel.
|
||||
func doDelete(fe pb.FrontendClient, deleteIds chan string) {
|
||||
for id := range deleteIds {
|
||||
req := &pb.DeleteTicketRequest{
|
||||
TicketId: id,
|
||||
}
|
||||
|
||||
if _, err := fe.DeleteTicket(context.Background(), req); err != nil {
|
||||
logger.Errorf("DeleteTicket failed for ticket %v, got %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
atomic.AddUint64(&deleted, 1)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user