diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c75b162..ef1a95e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -579,7 +579,7 @@ Minimum runtime-level status set: All game-related `message_type` include `game_id`. Gateway enriches them with authenticated `user_id` and routes them to `Game Master`. -`Game Master` checks whether this user may access this running game, using membership data sourced from `Game Lobby`, then routes the command to the correct engine container. +`Game Master` checks whether this user may access this running game, using membership data sourced from `Game Lobby`, then routes the command to the correct engine container using [Game Engine](./game/README.md)'s API. The gateway never routes directly to game engine containers. @@ -649,7 +649,7 @@ After a game has started, two different actions exist: This distinction is architectural and must remain explicit. -## 9. Runtime Manager +## 9. [Runtime Manager](rtmanager/README.md) `Runtime Manager` is the only internal service allowed to access Docker API directly. @@ -1275,11 +1275,11 @@ Recommended order for implementation is: 4. **Mail Service** (implemented) Internal email delivery for auth codes and platform notification mail. -5. **Notification Service** (implemented) +5. **Notification Service** (implemented) Unified async delivery of push and non-auth email notifications, with real Gateway and Mail Service boundary coverage. -6. **Game Lobby Service** +6. **Game Lobby Service** (implemented) Platform game records, membership, invites, applications, approvals, schedules, user-facing lists, pre-start lifecycle. 7. **Runtime Manager** diff --git a/game/README.md b/game/README.md new file mode 100644 index 0000000..ba126b2 --- /dev/null +++ b/game/README.md @@ -0,0 +1,8 @@ +# Game Service Engine + +Galaxy game engine — hosts a single game instance and exposes a REST API for +game initialization, turn advancement, player reports, and command execution. + +## API + +The REST contract is documented in [`openapi.yaml`](openapi.yaml). diff --git a/game/go.mod b/game/go.mod index 309b22f..600c144 100644 --- a/game/go.mod +++ b/game/go.mod @@ -3,6 +3,7 @@ module galaxy/game go 1.26.0 require ( + github.com/getkin/kin-openapi v0.135.0 github.com/gin-gonic/gin v1.12.0 github.com/go-playground/validator/v10 v10.30.2 github.com/google/uuid v1.6.0 @@ -17,23 +18,32 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.1 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.21 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/oasdiff/yaml v0.0.9 // indirect + github.com/oasdiff/yaml3 v0.0.9 // indirect github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.25.0 // indirect golang.org/x/crypto v0.50.0 // indirect diff --git a/game/go.sum b/game/go.sum index f7dc1e8..c010075 100644 --- a/game/go.sum +++ b/game/go.sum @@ -1,16 +1,27 @@ github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg= +github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI= github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= +github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -18,7 +29,11 @@ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/Nu github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= +github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -26,6 +41,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -36,20 +53,34 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48= +github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM= +github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g= +github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -65,14 +96,22 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/game/openapi.yaml b/game/openapi.yaml new file mode 100644 index 0000000..f3c6119 --- /dev/null +++ b/game/openapi.yaml @@ -0,0 +1,964 @@ +openapi: 3.0.3 +info: + title: Galaxy Game Service REST API + version: v1 + description: | + This specification documents the REST contract of `galaxy/game`. + + The service hosts a single game instance and exposes endpoints for game + initialization, turn advancement, game-state queries, player reports, and + batched player command execution. + + Transport rules: + - request bodies are JSON + - `PUT /api/v1/command` is rate-limited to one concurrent execution; + requests that cannot acquire the execution slot within 100 ms receive + `504 Gateway Timeout` + - `501 Not Implemented` is returned without a body when the game has not + been initialized + - validation errors return `400` with `{"error": "message"}` + - game-engine errors return `500` with `{"generic_error": "message", "code": integer}` + - other internal errors return `500` with `{"error": "message"}` +servers: + - url: http://localhost:8080 + description: Default local listener for Game Service. +tags: + - name: GameLifecycle + description: Game initialization, state retrieval, and turn advancement. + - name: PlayerActions + description: Player command execution, order validation, and turn-report retrieval. +paths: + /api/v1/status: + get: + tags: + - GameLifecycle + operationId: getGameStatus + summary: Get the current game state + description: | + Returns the current game state including turn number, stage, and a + summary of all players. Returns `501` if the game has not yet been + initialized. + responses: + "200": + description: Current game state. + content: + application/json: + schema: + $ref: "#/components/schemas/StateResponse" + "501": + description: Game has not been initialized yet. + "500": + $ref: "#/components/responses/InternalError" + /api/v1/init: + post: + tags: + - GameLifecycle + operationId: initGame + summary: Initialize a new game + description: | + Generates a new game instance with the supplied list of races. + Requires at least 10 race entries. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/InitRequest" + responses: + "201": + description: Game initialized successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/StateResponse" + "400": + $ref: "#/components/responses/ValidationError" + "500": + $ref: "#/components/responses/InternalError" + /api/v1/report: + get: + tags: + - PlayerActions + operationId: getReport + summary: Get a player turn report + description: | + Returns the full game report for the specified player and turn. + `player` must be a non-blank race name. `turn` defaults to `0`. + parameters: + - $ref: "#/components/parameters/PlayerParam" + - $ref: "#/components/parameters/TurnParam" + responses: + "200": + description: Player turn report. + content: + application/json: + schema: + $ref: "#/components/schemas/Report" + "400": + $ref: "#/components/responses/ValidationError" + "500": + $ref: "#/components/responses/InternalError" + /api/v1/command: + put: + tags: + - PlayerActions + operationId: executeCommands + summary: Execute a batch of player commands + description: | + Applies one or more game commands for the specified actor. Serialized + to one concurrent execution; requests that cannot acquire the execution + slot within 100 ms return `504 Gateway Timeout`. Returns `204 No + Content` on success. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CommandRequest" + responses: + "204": + description: All commands applied successfully. + "400": + $ref: "#/components/responses/ValidationError" + "504": + description: Command execution slot not acquired within 100 ms. + "500": + $ref: "#/components/responses/InternalError" + /api/v1/order: + put: + tags: + - PlayerActions + operationId: validateOrder + summary: Validate and store a player order without executing it + description: | + Validates and stores the game commands structurally without executing them. + Returns `204 No Content` if the order is valid and accepted. + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CommandRequest" + responses: + "204": + description: Order is structurally valid. + "400": + $ref: "#/components/responses/ValidationError" + "500": + $ref: "#/components/responses/InternalError" + /api/v1/turn: + put: + tags: + - GameLifecycle + operationId: generateTurn + summary: Advance the game to the next turn + description: | + Processes the current turn and generates the next one. Returns the + updated game state. + responses: + "200": + description: Updated game state after turn generation. + content: + application/json: + schema: + $ref: "#/components/schemas/StateResponse" + "500": + $ref: "#/components/responses/InternalError" +components: + parameters: + PlayerParam: + name: player + in: query + required: true + description: Race name of the player requesting the report. Must be non-blank. + schema: + type: string + minLength: 1 + TurnParam: + name: turn + in: query + required: false + description: Turn number to load the report for. Defaults to 0. + schema: + type: integer + minimum: 0 + default: 0 + schemas: + StateResponse: + type: object + description: Summary game state returned after initialization and at each turn boundary. + required: + - id + - turn + - stage + - player + properties: + id: + type: string + format: uuid + description: Unique identifier of this game instance. + turn: + type: integer + minimum: 0 + description: Current turn number. + stage: + type: integer + minimum: 0 + description: Current stage within the turn for games that support state modification. + player: + type: array + description: Summary state for each player participating in the game. + items: + $ref: "#/components/schemas/PlayerState" + PlayerState: + type: object + description: Brief player state returned as part of the game state response. + required: + - id + - raceName + - planets + - population + - extinct + properties: + id: + type: string + format: uuid + description: Unique player identifier within this game. + raceName: + type: string + description: Name of the player's race. + planets: + type: integer + minimum: 0 + description: Number of planets currently owned by the player. + population: + type: number + description: Total population summed across all player planets. + extinct: + type: boolean + description: True when the race has been eliminated or voluntarily quit. + InitRequest: + type: object + description: Initialization request specifying the race list for a new game. + required: + - races + properties: + races: + type: array + description: List of participating races. Minimum 10 entries required. + minItems: 10 + items: + $ref: "#/components/schemas/InitRace" + InitRace: + type: object + description: A single race entry in an initialization request. + required: + - raceName + properties: + raceName: + type: string + description: Name of the race. Must be non-blank and satisfy the entity-name format. + minLength: 1 + CommandRequest: + type: object + description: | + Batch command payload. `actor` identifies the race submitting the commands. + Each element of `cmd` is a polymorphic command object discriminated by the + `@type` field. At least one command is required. + required: + - actor + - cmd + properties: + actor: + type: string + description: Race name of the actor submitting the commands. Must be non-blank. + minLength: 1 + cmd: + type: array + description: One or more commands to execute in order. + minItems: 1 + items: + $ref: "#/components/schemas/Command" + Command: + type: object + description: | + Polymorphic game command. The `@type` field identifies the variant. + Each variant extends the base fields with additional type-specific + parameters documented in `pkg/model/order/order.go`. + required: + - "@type" + - cmdId + properties: + "@type": + $ref: "#/components/schemas/CommandType" + cmdId: + type: string + format: uuid + description: Unique command identifier (RFC 4122 UUID). + cmdApplied: + type: boolean + description: Set in command-result responses; true when the command was applied. + cmdErrorCode: + type: integer + description: Set in command-result responses; non-zero when the command was rejected. + CommandType: + type: string + description: Discriminator identifying the game command variant carried in a `cmd` element. + enum: + - raceQuit + - raceVote + - raceRelation + - shipClassCreate + - shipClassMerge + - shipClassRemove + - shipGroupBreak + - shipGroupLoad + - shipGroupUnload + - shipGroupSend + - shipGroupUpgrade + - shipGroupMerge + - shipGroupDismantle + - shipGroupTransfer + - shipGroupJoinFleet + - fleetMerge + - fleetSend + - scienceCreate + - scienceRemove + - planetRename + - planetProduce + - planetRouteSet + - planetRouteRemove + Report: + type: object + description: | + Full game report for one player at one turn boundary. Optional array + fields are omitted when empty. + required: + - version + - turn + - mapWidth + - mapHeight + - mapPlanets + - race + - votes + - voteFor + - player + properties: + version: + type: integer + minimum: 0 + description: Report format version. + turn: + type: integer + minimum: 0 + description: Turn number this report covers. + mapWidth: + type: integer + minimum: 0 + description: Width of the star map. + mapHeight: + type: integer + minimum: 0 + description: Height of the star map. + mapPlanets: + type: integer + minimum: 0 + description: Total number of planets on the map. + race: + type: string + description: Race name of the report recipient. + votes: + type: number + description: Fraction of alliance votes held by this race. + voteFor: + type: string + description: Race name this player is currently voting for. + player: + type: array + description: Diplomatic and aggregate statistics for each known player. + items: + $ref: "#/components/schemas/ReportPlayer" + localScience: + type: array + description: Science projects owned by this race. + items: + $ref: "#/components/schemas/Science" + otherScience: + type: array + description: Science projects owned by other known races. + items: + $ref: "#/components/schemas/OtherScience" + localShipClass: + type: array + description: Ship classes designed by this race. + items: + $ref: "#/components/schemas/ShipClass" + otherShipClass: + type: array + description: Ship classes belonging to other known races. + items: + $ref: "#/components/schemas/OtherShipClass" + battle: + type: array + description: UUIDs of battle reports relevant to this turn. + items: + type: string + format: uuid + bombing: + type: array + description: Bombing events that occurred during this turn. + items: + $ref: "#/components/schemas/Bombing" + incomingGroup: + type: array + description: Identified ship groups inbound toward this race's planets. + items: + $ref: "#/components/schemas/IncomingGroup" + localPlanet: + type: array + description: Full state of planets owned by this race. + items: + $ref: "#/components/schemas/LocalPlanet" + shipProduction: + type: array + description: Active ship construction status on this race's planets. + items: + $ref: "#/components/schemas/ShipProduction" + route: + type: array + description: Cargo route configuration per planet. + items: + $ref: "#/components/schemas/Route" + otherPlanet: + type: array + description: Partial state of planets owned by other known races. + items: + $ref: "#/components/schemas/OtherPlanet" + uninhabitedPlanet: + type: array + description: Uninhabited planets within sensor range. + items: + $ref: "#/components/schemas/UninhabitedPlanet" + unidentifiedPlanet: + type: array + description: Unidentified planet positions within sensor range. + items: + $ref: "#/components/schemas/UnidentifiedPlanet" + localFleet: + type: array + description: Named fleets belonging to this race. + items: + $ref: "#/components/schemas/LocalFleet" + localGroup: + type: array + description: Ship groups belonging to this race. + items: + $ref: "#/components/schemas/LocalGroup" + otherGroup: + type: array + description: Ship groups belonging to other known races. + items: + $ref: "#/components/schemas/OtherGroup" + unidentifiedGroup: + type: array + description: Unidentified ship group positions within sensor range. + items: + $ref: "#/components/schemas/UnidentifiedGroup" + ReportPlayer: + type: object + description: Diplomatic and aggregate statistics for one player as seen in a report. + required: + - name + - drive + - weapons + - shields + - cargo + - population + - industry + - planets + - relation + - votes + - extinct + properties: + name: + type: string + description: Race name. + drive: + type: number + description: Current drive technology level. + weapons: + type: number + description: Current weapons technology level. + shields: + type: number + description: Current shields technology level. + cargo: + type: number + description: Current cargo technology level. + population: + type: number + description: Total population across all planets. + industry: + type: number + description: Total industrial output. + planets: + type: integer + minimum: 0 + description: Number of planets owned. + relation: + type: string + description: Current diplomatic relation toward this race. + votes: + type: number + description: Fraction of alliance votes held. + extinct: + type: boolean + description: True when the race has been eliminated or quit. + Science: + type: object + description: A science project describing technology investment ratios. + required: + - name + - drive + - weapons + - shields + - cargo + properties: + name: + type: string + description: Science project name. + drive: + type: number + description: Investment ratio for drive technology (0–1). + weapons: + type: number + description: Investment ratio for weapons technology (0–1). + shields: + type: number + description: Investment ratio for shields technology (0–1). + cargo: + type: number + description: Investment ratio for cargo technology (0–1). + OtherScience: + allOf: + - $ref: "#/components/schemas/Science" + - type: object + required: + - race + properties: + race: + type: string + description: Race that owns this science project. + ShipClass: + type: object + description: Design parameters for one ship class. + required: + - name + - drive + - armament + - weapons + - shields + - cargo + - mass + properties: + name: + type: string + description: Ship class name. + drive: + type: number + description: Drive technology level. + armament: + type: integer + minimum: 0 + description: Number of weapon mounts (ammo units). + weapons: + type: number + description: Weapons technology multiplier. + shields: + type: number + description: Shields technology multiplier. + cargo: + type: number + description: Cargo technology multiplier. + mass: + type: number + description: Computed ship mass. + OtherShipClass: + allOf: + - $ref: "#/components/schemas/ShipClass" + - type: object + required: + - race + properties: + race: + type: string + description: Race that owns this ship class. + Bombing: + type: object + description: Record of a bombing event that occurred during turn processing. + required: + - planet + - planetName + - owner + - attacker + - production + - industry + - population + - colonists + - capital + - material + - attack + - wiped + properties: + planet: + type: integer + minimum: 0 + description: Global planet number. + planetName: + type: string + description: Planet name. + owner: + type: string + description: Race name of the planet owner. + attacker: + type: string + description: Race name of the attacker. + production: + type: string + description: Production type active on the planet at the time of bombing. + industry: + type: number + description: Industrial capacity remaining after the bombing. + population: + type: number + description: Population remaining after the bombing. + colonists: + type: number + description: Colonist units remaining after the bombing. + capital: + type: number + description: Capital reserves remaining after the bombing. + material: + type: number + description: Material reserves remaining after the bombing. + attack: + type: number + description: Aggregate attack power applied during the bombing. + wiped: + type: boolean + description: True when all population was eliminated by the bombing. + IncomingGroup: + type: object + description: An identified ship group inbound toward a planet of this race. + required: + - origin + - destination + - distance + - speed + - mass + properties: + origin: + type: integer + minimum: 0 + description: Planet number where this group originated. + destination: + type: integer + minimum: 0 + description: Planet number this group is heading toward. + distance: + type: number + description: Remaining travel distance. + speed: + type: number + description: Travel speed. + mass: + type: number + description: Total mass of the group. + UnidentifiedPlanet: + type: object + description: Minimal sensor reading for an unidentified planet. + required: + - x + - "y" + - number + properties: + x: + type: number + description: Horizontal map coordinate. + "y": + type: number + description: Vertical map coordinate. + number: + type: integer + minimum: 0 + description: Global planet number. + UninhabitedPlanet: + allOf: + - $ref: "#/components/schemas/UnidentifiedPlanet" + - type: object + description: An uninhabited planet within sensor range. + required: + - size + - name + - resources + - capital + - material + properties: + size: + type: number + description: Planet size. + name: + type: string + description: Planet name. + resources: + type: number + description: Natural resource yield (R). + capital: + type: number + description: Capital reserves stored on the planet (CAP). + material: + type: number + description: Material reserves stored on the planet (MAT). + LocalPlanet: + allOf: + - $ref: "#/components/schemas/UninhabitedPlanet" + - type: object + description: Full state for a planet owned by this race. + required: + - industry + - population + - colonists + - production + - freeIndustry + properties: + industry: + type: number + description: Industrial capacity of the planet (I). + population: + type: number + description: Population of the planet (P). + colonists: + type: number + description: Number of colonists on the planet (COL). + production: + type: string + description: Current production assignment. + freeIndustry: + type: number + description: Unused industrial capacity available for new production (L). + OtherPlanet: + allOf: + - $ref: "#/components/schemas/LocalPlanet" + - type: object + description: Partial planet state for a planet owned by another race. + required: + - owner + properties: + owner: + type: string + description: Race name of the planet owner. + Route: + type: object + description: Cargo route configuration originating from one planet. + required: + - planet + - route + properties: + planet: + type: integer + minimum: 0 + description: Source planet number. + route: + type: object + description: | + Mapping from destination planet number (as a string key) to the + cargo load type being routed (MAT, CAP, COL, EMP). + additionalProperties: + type: string + ShipProduction: + type: object + description: Active ship construction progress on one planet. + required: + - planet + - class + - cost + - prodUsed + - percent + - free + properties: + planet: + type: integer + minimum: 0 + description: Planet number where construction is taking place. + class: + type: string + description: Name of the ship class being built. + cost: + type: number + description: Total production cost for one unit of this class. + prodUsed: + type: number + description: Production units already spent on this build. + percent: + type: number + description: Completion percentage of the current build. + free: + type: number + description: Remaining free industrial capacity on this planet. + LocalFleet: + type: object + description: A named fleet belonging to this race. + required: + - name + - groups + - destination + - speed + - state + properties: + name: + type: string + description: Fleet name. + groups: + type: integer + minimum: 0 + description: Number of ship groups in this fleet. + destination: + type: integer + minimum: 0 + description: Destination planet number. + origin: + type: integer + minimum: 0 + description: Origin planet number when the fleet is in transit. + range: + type: number + description: Remaining travel range when the fleet is in transit. + speed: + type: number + description: Travel speed. + state: + type: string + description: Fleet operational state. + OtherGroup: + type: object + description: A ship group visible to this race belonging to another race. + required: + - number + - class + - tech + - cargo + - load + - destination + - speed + - mass + properties: + number: + type: integer + minimum: 0 + description: Group identifier number. + class: + type: string + description: Ship class name. + tech: + type: object + description: Technology levels of this group keyed by tech type name. + additionalProperties: + type: number + cargo: + type: string + description: Type of cargo carried by this group. + load: + type: number + description: Current cargo load quantity. + destination: + type: integer + minimum: 0 + description: Destination planet number. + origin: + type: integer + minimum: 0 + description: Origin planet number when the group is in transit. + range: + type: number + description: Remaining travel range when the group is in transit. + speed: + type: number + description: Travel speed. + mass: + type: number + description: Total mass of the group. + LocalGroup: + allOf: + - $ref: "#/components/schemas/OtherGroup" + - type: object + description: A ship group belonging to this race with full ownership detail. + required: + - id + - state + properties: + id: + type: string + format: uuid + description: Unique group identifier. + state: + type: string + description: Group operational state. + fleet: + type: string + nullable: true + description: Name of the fleet this group belongs to, or null if ungrouped. + UnidentifiedGroup: + type: object + description: Positional reading for an unidentified ship group. + required: + - x + - "y" + properties: + x: + type: number + description: Horizontal map coordinate. + "y": + type: number + description: Vertical map coordinate. + ValidationErrorResponse: + type: object + description: Validation error returned for malformed requests. + required: + - error + properties: + error: + type: string + description: Human-readable validation error message from the binding layer. + InternalErrorResponse: + type: object + description: | + Internal server error. The shape depends on the error source: + - `{"error": "message"}` for generic runtime errors + - `{"generic_error": "message", "code": integer}` for game-engine errors + properties: + error: + type: string + description: Generic runtime error message. + generic_error: + type: string + description: Game-engine error message. + code: + type: integer + description: Game-engine error code. + responses: + ValidationError: + description: Request body or query parameters are invalid. + content: + application/json: + schema: + $ref: "#/components/schemas/ValidationErrorResponse" + examples: + validationError: + value: + error: "Key: 'InitRequest.Races' Error:Field validation for 'Races' failed on the 'gte' tag" + InternalError: + description: Internal Game Service error. + content: + application/json: + schema: + $ref: "#/components/schemas/InternalErrorResponse" diff --git a/game/openapi_contract_test.go b/game/openapi_contract_test.go new file mode 100644 index 0000000..342ce28 --- /dev/null +++ b/game/openapi_contract_test.go @@ -0,0 +1,268 @@ +package game + +import ( + "context" + "net/http" + "path/filepath" + "runtime" + "slices" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestGameOpenAPISpecValidates(t *testing.T) { + t.Parallel() + + loadOpenAPISpec(t) +} + +func TestGameOpenAPISpecFreezesResponseSchemas(t *testing.T) { + t.Parallel() + + doc := loadOpenAPISpec(t) + + tests := []struct { + name string + path string + method string + status int + wantRef string + }{ + { + name: "get game status", + path: "/api/v1/status", + method: http.MethodGet, + status: http.StatusOK, + wantRef: "#/components/schemas/StateResponse", + }, + { + name: "init game", + path: "/api/v1/init", + method: http.MethodPost, + status: http.StatusCreated, + wantRef: "#/components/schemas/StateResponse", + }, + { + name: "get report", + path: "/api/v1/report", + method: http.MethodGet, + status: http.StatusOK, + wantRef: "#/components/schemas/Report", + }, + { + name: "generate turn", + path: "/api/v1/turn", + method: http.MethodPut, + status: http.StatusOK, + wantRef: "#/components/schemas/StateResponse", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + operation := getOpenAPIOperation(t, doc, tt.path, tt.method) + assertSchemaRef(t, responseSchemaRef(t, operation, tt.status), tt.wantRef, tt.name+" response schema") + }) + } +} + +func TestGameOpenAPISpecFreezesInitRequest(t *testing.T) { + t.Parallel() + + doc := loadOpenAPISpec(t) + operation := getOpenAPIOperation(t, doc, "/api/v1/init", http.MethodPost) + + assertSchemaRef(t, requestSchemaRef(t, operation), "#/components/schemas/InitRequest", "init request schema") + + schema := componentSchemaRef(t, doc, "InitRequest") + assertRequiredFields(t, schema, "races") + + racesSchema := schema.Value.Properties["races"] + require.NotNil(t, racesSchema, "InitRequest.races schema must exist") + require.Equal(t, uint64(10), racesSchema.Value.MinItems, "InitRequest.races minItems must be 10") +} + +func TestGameOpenAPISpecFreezesCommandRequest(t *testing.T) { + t.Parallel() + + doc := loadOpenAPISpec(t) + + for _, path := range []string{"/api/v1/command", "/api/v1/order"} { + t.Run(path, func(t *testing.T) { + t.Parallel() + + operation := getOpenAPIOperation(t, doc, path, http.MethodPut) + assertSchemaRef(t, requestSchemaRef(t, operation), "#/components/schemas/CommandRequest", path+" command request schema") + }) + } + + schema := componentSchemaRef(t, doc, "CommandRequest") + assertRequiredFields(t, schema, "actor", "cmd") + + cmdSchema := schema.Value.Properties["cmd"] + require.NotNil(t, cmdSchema, "CommandRequest.cmd schema must exist") + require.Equal(t, uint64(1), cmdSchema.Value.MinItems, "CommandRequest.cmd minItems must be 1") +} + +func TestGameOpenAPISpecCommandTypeEnumIsComplete(t *testing.T) { + t.Parallel() + + doc := loadOpenAPISpec(t) + schema := componentSchemaRef(t, doc, "CommandType") + + enumValues := make([]string, 0, len(schema.Value.Enum)) + for _, v := range schema.Value.Enum { + s, ok := v.(string) + require.True(t, ok, "CommandType enum entry must be a string") + enumValues = append(enumValues, s) + } + + require.ElementsMatch(t, []string{ + "raceQuit", + "raceVote", + "raceRelation", + "shipClassCreate", + "shipClassMerge", + "shipClassRemove", + "shipGroupBreak", + "shipGroupLoad", + "shipGroupUnload", + "shipGroupSend", + "shipGroupUpgrade", + "shipGroupMerge", + "shipGroupDismantle", + "shipGroupTransfer", + "shipGroupJoinFleet", + "fleetMerge", + "fleetSend", + "scienceCreate", + "scienceRemove", + "planetRename", + "planetProduce", + "planetRouteSet", + "planetRouteRemove", + }, enumValues) +} + +// helpers (modelled after user/openapi_contract_test.go) + +func loadOpenAPISpec(t *testing.T) *openapi3.T { + t.Helper() + + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + require.FailNow(t, "runtime.Caller failed") + } + + specPath := filepath.Join(filepath.Dir(thisFile), "openapi.yaml") + loader := openapi3.NewLoader() + doc, err := loader.LoadFromFile(specPath) + if err != nil { + require.Failf(t, "test failed", "load spec %s: %v", specPath, err) + } + if doc == nil { + require.Failf(t, "test failed", "load spec %s: returned nil document", specPath) + } + if doc.Info == nil { + require.Failf(t, "test failed", "load spec %s: missing info section", specPath) + } + if doc.Info.Version != "v1" { + require.Failf(t, "test failed", "spec %s version = %q, want v1", specPath, doc.Info.Version) + } + if err := doc.Validate(context.Background()); err != nil { + require.Failf(t, "test failed", "validate spec %s: %v", specPath, err) + } + + return doc +} + +func getOpenAPIOperation(t *testing.T, doc *openapi3.T, path string, method string) *openapi3.Operation { + t.Helper() + + if doc.Paths == nil { + require.Failf(t, "test failed", "spec is missing paths while looking up %s %s", method, path) + } + pathItem := doc.Paths.Value(path) + if pathItem == nil { + require.Failf(t, "test failed", "spec is missing path %s", path) + } + operation := pathItem.GetOperation(method) + if operation == nil { + require.Failf(t, "test failed", "spec is missing %s operation for path %s", method, path) + } + + return operation +} + +func requestSchemaRef(t *testing.T, operation *openapi3.Operation) *openapi3.SchemaRef { + t.Helper() + + if operation.RequestBody == nil || operation.RequestBody.Value == nil { + require.FailNow(t, "operation is missing request body") + } + mediaType := operation.RequestBody.Value.Content.Get("application/json") + if mediaType == nil || mediaType.Schema == nil { + require.FailNow(t, "operation is missing application/json request schema") + } + + return mediaType.Schema +} + +func responseSchemaRef(t *testing.T, operation *openapi3.Operation, status int) *openapi3.SchemaRef { + t.Helper() + + if operation.Responses == nil { + require.Failf(t, "test failed", "operation is missing responses for status %d", status) + } + response := operation.Responses.Status(status) + if response == nil || response.Value == nil { + require.Failf(t, "test failed", "operation is missing response for status %d", status) + } + mediaType := response.Value.Content.Get("application/json") + if mediaType == nil || mediaType.Schema == nil { + require.Failf(t, "test failed", "operation response %d is missing application/json schema", status) + } + + return mediaType.Schema +} + +func componentSchemaRef(t *testing.T, doc *openapi3.T, name string) *openapi3.SchemaRef { + t.Helper() + + if doc.Components == nil { + require.Failf(t, "test failed", "spec is missing components while looking up schema %s", name) + } + schema := doc.Components.Schemas[name] + if schema == nil || schema.Value == nil { + require.Failf(t, "test failed", "spec is missing schema %s", name) + } + + return schema +} + +func assertSchemaRef(t *testing.T, schemaRef *openapi3.SchemaRef, want string, name string) { + t.Helper() + + if schemaRef == nil { + require.Failf(t, "test failed", "%s schema ref is nil", name) + } + if schemaRef.Ref != want { + require.Failf(t, "test failed", "%s ref = %q, want %q", name, schemaRef.Ref, want) + } +} + +func assertRequiredFields(t *testing.T, schemaRef *openapi3.SchemaRef, fields ...string) { + t.Helper() + + required := append([]string(nil), schemaRef.Value.Required...) + slices.Sort(required) + want := append([]string(nil), fields...) + slices.Sort(want) + if !slices.Equal(required, want) { + require.Failf(t, "test failed", "schema required fields = %v, want %v", required, want) + } +} diff --git a/go.work b/go.work index 6fb2587..bd13729 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.26.1 +go 1.26.2 use ( ./authsession @@ -21,6 +21,7 @@ use ( ./pkg/storage ./pkg/transcoder ./pkg/util + ./rtmanager ./user ) diff --git a/rtmanager/go.mod b/rtmanager/go.mod new file mode 100644 index 0000000..bccce7f --- /dev/null +++ b/rtmanager/go.mod @@ -0,0 +1,3 @@ +module galaxy/rtmanager + +go 1.26.2