From 6ca82246b4a72f64bc01db2d02fb07a5b07d296f Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Sat, 3 Jun 2023 17:48:39 -0400 Subject: [PATCH] Initial commit. --- .github/workflows/go.yml | 87 ++ .gitignore | 3 + LICENSE.md | 250 ++++++ README.md | 15 + admin/handler.go | 43 + buf.gen.yaml | 10 + buf.work.yaml | 3 + cmd/gameserver/main.go | 77 ++ cmd/gameserver/resource_windows_386.syso | Bin 0 -> 104090 bytes cmd/gameserver/resource_windows_amd64.syso | Bin 0 -> 104090 bytes cmd/gameserver/resource_windows_arm.syso | Bin 0 -> 104090 bytes cmd/gameserver/resource_windows_arm64.syso | Bin 0 -> 104090 bytes cmd/gameserver/versioninfo.json | 43 + cmd/loginserver/main.go | 77 ++ cmd/loginserver/resource_windows_386.syso | Bin 0 -> 104098 bytes cmd/loginserver/resource_windows_amd64.syso | Bin 0 -> 104098 bytes cmd/loginserver/resource_windows_arm.syso | Bin 0 -> 104098 bytes cmd/loginserver/resource_windows_arm64.syso | Bin 0 -> 104098 bytes cmd/loginserver/versioninfo.json | 43 + cmd/messageserver/main.go | 76 ++ cmd/messageserver/resource_windows_386.syso | Bin 0 -> 104106 bytes cmd/messageserver/resource_windows_amd64.syso | Bin 0 -> 104106 bytes cmd/messageserver/resource_windows_arm.syso | Bin 0 -> 104106 bytes cmd/messageserver/resource_windows_arm64.syso | Bin 0 -> 104106 bytes cmd/messageserver/versioninfo.json | 43 + cmd/migrate/main.go | 87 ++ cmd/minibox/README.md | 3 + cmd/minibox/cli.go | 52 ++ cmd/minibox/config.go | 129 +++ cmd/minibox/config_test.go | 38 + cmd/minibox/lang/dict/lang.go | 204 +++++ cmd/minibox/lang/embed.go | 18 + cmd/minibox/lang/en.json | 232 +++++ cmd/minibox/lang/ja.json | 232 +++++ cmd/minibox/lang/update/main.go | 235 +++++ cmd/minibox/main_nogui.go | 29 + cmd/minibox/main_wingui.go | 697 +++++++++++++++ cmd/minibox/minibox.manifest | 15 + cmd/minibox/resource_windows_386.syso | Bin 0 -> 105084 bytes cmd/minibox/resource_windows_amd64.syso | Bin 0 -> 105084 bytes cmd/minibox/resource_windows_arm.syso | Bin 0 -> 105084 bytes cmd/minibox/resource_windows_arm64.syso | Bin 0 -> 105084 bytes cmd/minibox/status-healthy.png | Bin 0 -> 933 bytes cmd/minibox/status-offline.png | Bin 0 -> 984 bytes cmd/minibox/status-unhealthy.png | Bin 0 -> 887 bytes cmd/minibox/versioninfo.json | 43 + cmd/packetparse/main.go | 140 +++ cmd/shimclient/main.go | 119 +++ cmd/topologyserver/main.go | 81 ++ cmd/topologyserver/resource_windows_386.syso | Bin 0 -> 104138 bytes .../resource_windows_amd64.syso | Bin 0 -> 104138 bytes cmd/topologyserver/resource_windows_arm.syso | Bin 0 -> 104138 bytes .../resource_windows_arm64.syso | Bin 0 -> 104138 bytes cmd/topologyserver/versioninfo.json | 43 + common/bufconn/bufconn.go | 317 +++++++ common/error.go | 38 + common/hash/bcrypt.go | 38 + common/hash/hasher.go | 27 + common/hash/null.go | 35 + common/packetbuilder.go | 104 +++ common/pstring.go | 31 + common/pubsub/postgres.go | 138 +++ common/pubsub/pubsub.go | 43 + common/pycrypto/xtea.go | 67 ++ common/restruct.go | 25 + common/server.go | 87 ++ common/serverconn.go | 159 ++++ common/table.go | 78 ++ common/topology/client.go | 74 ++ common/topology/server.go | 79 ++ common/topology/storage.go | 158 ++++ database/accounts/service.go | 169 ++++ database/dialect.go | 61 ++ database/postgres_test.go | 67 ++ database/query_test.go | 75 ++ database/sqlite_test.go | 48 + flake.lock | 60 ++ flake.nix | 26 + game/conn.go | 496 +++++++++++ game/message.go | 38 + game/msgclient.go | 314 +++++++ game/msgserver.go | 666 ++++++++++++++ game/server.go | 82 ++ gen.go | 22 + gen/dbmodels/character.sql.go | 101 +++ gen/dbmodels/db.go | 31 + gen/dbmodels/models.go | 33 + gen/dbmodels/player.sql.go | 103 +++ gen/dbmodels/session.sql.go | 148 +++ gen/proto/go/topologypb/topology.pb.go | 841 ++++++++++++++++++ .../topologypbconnect/topology.connect.go | 166 ++++ go.mod | 135 +++ go.sum | 752 ++++++++++++++++ login/conn.go | 247 +++++ login/message.go | 38 + login/model_test.go | 80 ++ login/msgclient.go | 69 ++ login/msgserver.go | 141 +++ login/server.go | 73 ++ message/conn.go | 76 ++ message/message.go | 38 + message/msgclient.go | 31 + message/msgserver.go | 39 + message/server.go | 71 ++ migrations/0001_create_player_table.sql | 12 + migrations/0002_create_character_table.sql | 10 + migrations/0003_create_sessions_table.sql | 15 + migrations/embed.go | 31 + minibox/admin.go | 74 ++ minibox/gameserver.go | 87 ++ minibox/loginserver.go | 80 ++ minibox/messageserver.go | 80 ++ minibox/minibox.go | 364 ++++++++ minibox/qa.go | 74 ++ minibox/rugburn.go | 152 ++++ minibox/service.go | 166 ++++ minibox/topology.go | 154 ++++ minibox/web.go | 102 +++ pangya/config.go | 26 + pangya/iff/common.go | 44 + pangya/iff/course.go | 28 + pangya/iff/file.go | 76 ++ pangya/player.go | 151 ++++ pangya/rank.go | 96 ++ pangya/server.go | 43 + pangya/systemtime.go | 23 + proto/buf.yaml | 7 + proto/topologypb/topology.proto | 84 ++ qa/authserv/server.go | 45 + queries/character.sql | 21 + queries/player.sql | 20 + queries/session.sql | 28 + res/embed.go | 23 + res/pangbox.ico | Bin 0 -> 102134 bytes res/pangbox.png | Bin 0 -> 5367 bytes res/pangbox.svg | 174 ++++ sqlc.yaml | 8 + tools.go | 28 + web/assets/style.css | 53 ++ web/data.go | 52 ++ web/data/extracontents.xml | 6 + web/data/pangya_default.xml | 43 + web/data/translation.xml | 5 + web/handler.go | 91 ++ web/templates/register.html | 37 + web/templates/register_complete.html | 17 + web/ui.go | 106 +++ web/updatelist.go | 162 ++++ 148 files changed, 12900 insertions(+) create mode 100644 .github/workflows/go.yml create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100755 README.md create mode 100644 admin/handler.go create mode 100644 buf.gen.yaml create mode 100644 buf.work.yaml create mode 100644 cmd/gameserver/main.go create mode 100644 cmd/gameserver/resource_windows_386.syso create mode 100644 cmd/gameserver/resource_windows_amd64.syso create mode 100644 cmd/gameserver/resource_windows_arm.syso create mode 100644 cmd/gameserver/resource_windows_arm64.syso create mode 100755 cmd/gameserver/versioninfo.json create mode 100644 cmd/loginserver/main.go create mode 100644 cmd/loginserver/resource_windows_386.syso create mode 100644 cmd/loginserver/resource_windows_amd64.syso create mode 100644 cmd/loginserver/resource_windows_arm.syso create mode 100644 cmd/loginserver/resource_windows_arm64.syso create mode 100755 cmd/loginserver/versioninfo.json create mode 100644 cmd/messageserver/main.go create mode 100644 cmd/messageserver/resource_windows_386.syso create mode 100644 cmd/messageserver/resource_windows_amd64.syso create mode 100644 cmd/messageserver/resource_windows_arm.syso create mode 100644 cmd/messageserver/resource_windows_arm64.syso create mode 100755 cmd/messageserver/versioninfo.json create mode 100644 cmd/migrate/main.go create mode 100755 cmd/minibox/README.md create mode 100755 cmd/minibox/cli.go create mode 100644 cmd/minibox/config.go create mode 100644 cmd/minibox/config_test.go create mode 100644 cmd/minibox/lang/dict/lang.go create mode 100644 cmd/minibox/lang/embed.go create mode 100644 cmd/minibox/lang/en.json create mode 100644 cmd/minibox/lang/ja.json create mode 100644 cmd/minibox/lang/update/main.go create mode 100644 cmd/minibox/main_nogui.go create mode 100644 cmd/minibox/main_wingui.go create mode 100644 cmd/minibox/minibox.manifest create mode 100755 cmd/minibox/resource_windows_386.syso create mode 100755 cmd/minibox/resource_windows_amd64.syso create mode 100644 cmd/minibox/resource_windows_arm.syso create mode 100644 cmd/minibox/resource_windows_arm64.syso create mode 100644 cmd/minibox/status-healthy.png create mode 100644 cmd/minibox/status-offline.png create mode 100644 cmd/minibox/status-unhealthy.png create mode 100755 cmd/minibox/versioninfo.json create mode 100644 cmd/packetparse/main.go create mode 100755 cmd/shimclient/main.go create mode 100755 cmd/topologyserver/main.go create mode 100644 cmd/topologyserver/resource_windows_386.syso create mode 100644 cmd/topologyserver/resource_windows_amd64.syso create mode 100644 cmd/topologyserver/resource_windows_arm.syso create mode 100644 cmd/topologyserver/resource_windows_arm64.syso create mode 100644 cmd/topologyserver/versioninfo.json create mode 100644 common/bufconn/bufconn.go create mode 100755 common/error.go create mode 100755 common/hash/bcrypt.go create mode 100755 common/hash/hasher.go create mode 100755 common/hash/null.go create mode 100644 common/packetbuilder.go create mode 100755 common/pstring.go create mode 100755 common/pubsub/postgres.go create mode 100755 common/pubsub/pubsub.go create mode 100644 common/pycrypto/xtea.go create mode 100644 common/restruct.go create mode 100755 common/server.go create mode 100755 common/serverconn.go create mode 100644 common/table.go create mode 100644 common/topology/client.go create mode 100755 common/topology/server.go create mode 100755 common/topology/storage.go create mode 100755 database/accounts/service.go create mode 100644 database/dialect.go create mode 100644 database/postgres_test.go create mode 100644 database/query_test.go create mode 100644 database/sqlite_test.go create mode 100644 flake.lock create mode 100644 flake.nix create mode 100755 game/conn.go create mode 100644 game/message.go create mode 100755 game/msgclient.go create mode 100755 game/msgserver.go create mode 100755 game/server.go create mode 100755 gen.go create mode 100644 gen/dbmodels/character.sql.go create mode 100644 gen/dbmodels/db.go create mode 100644 gen/dbmodels/models.go create mode 100644 gen/dbmodels/player.sql.go create mode 100644 gen/dbmodels/session.sql.go create mode 100644 gen/proto/go/topologypb/topology.pb.go create mode 100644 gen/proto/go/topologypb/topologypbconnect/topology.connect.go create mode 100755 go.mod create mode 100755 go.sum create mode 100755 login/conn.go create mode 100644 login/message.go create mode 100755 login/model_test.go create mode 100755 login/msgclient.go create mode 100755 login/msgserver.go create mode 100755 login/server.go create mode 100755 message/conn.go create mode 100644 message/message.go create mode 100755 message/msgclient.go create mode 100755 message/msgserver.go create mode 100755 message/server.go create mode 100644 migrations/0001_create_player_table.sql create mode 100644 migrations/0002_create_character_table.sql create mode 100644 migrations/0003_create_sessions_table.sql create mode 100644 migrations/embed.go create mode 100644 minibox/admin.go create mode 100644 minibox/gameserver.go create mode 100644 minibox/loginserver.go create mode 100644 minibox/messageserver.go create mode 100644 minibox/minibox.go create mode 100644 minibox/qa.go create mode 100644 minibox/rugburn.go create mode 100644 minibox/service.go create mode 100644 minibox/topology.go create mode 100644 minibox/web.go create mode 100755 pangya/config.go create mode 100644 pangya/iff/common.go create mode 100644 pangya/iff/course.go create mode 100644 pangya/iff/file.go create mode 100755 pangya/player.go create mode 100755 pangya/rank.go create mode 100755 pangya/server.go create mode 100644 pangya/systemtime.go create mode 100644 proto/buf.yaml create mode 100755 proto/topologypb/topology.proto create mode 100755 qa/authserv/server.go create mode 100644 queries/character.sql create mode 100644 queries/player.sql create mode 100644 queries/session.sql create mode 100644 res/embed.go create mode 100755 res/pangbox.ico create mode 100644 res/pangbox.png create mode 100755 res/pangbox.svg create mode 100644 sqlc.yaml create mode 100644 tools.go create mode 100644 web/assets/style.css create mode 100644 web/data.go create mode 100644 web/data/extracontents.xml create mode 100644 web/data/pangya_default.xml create mode 100644 web/data/translation.xml create mode 100755 web/handler.go create mode 100644 web/templates/register.html create mode 100644 web/templates/register_complete.html create mode 100644 web/ui.go create mode 100644 web/updatelist.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..1e27f91 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,87 @@ +name: Go + +on: + push: + branches: [ master ] + tags: [ v* ] + pull_request: + branches: [ master ] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.20+ + uses: actions/setup-go@v2 + with: + go-version: ^1.20 + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: go get -v -t -d ./... + + - name: Build (Smoke test) + run: CGO_ENABLED=0 go build -v ./... + + - name: Test + run: CGO_ENABLED=0 go test -v ./... + + - name: Build Releases + run: | + mkdir bin + + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/minibox-macos-amd64 ./cmd/minibox + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/minibox-macos-arm64 ./cmd/minibox + CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/minibox-freebsd-amd64 ./cmd/minibox + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/minibox-linux-amd64 ./cmd/minibox + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags -H=windowsgui -o bin/minibox-windows-amd64.exe ./cmd/minibox + + - name: Upload builds + uses: actions/upload-artifact@v2 + with: + name: bin + path: bin/* + + release: + name: Release + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: build + steps: + - name: Download builds + uses: actions/download-artifact@v1 + with: + name: bin + + - name: Zip builds + run: | + for i in bin/* + do + OUT="$PWD/$(basename $i).zip" + cd "$(dirname $i)" + zip "$OUT" "$(basename $i)" + cd - + done + + - name: Get the tag name + id: tag + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Create Release + id: create_release + uses: actions/create-release@v1.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.tag.outputs.VERSION }} + release_name: ${{ steps.tag.outputs.VERSION }} + draft: true + + - { uses: actions/upload-release-asset@v1, env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }, with: { upload_url: "${{ steps.create_release.outputs.upload_url }}", asset_path: minibox-macos-amd64.zip, asset_name: minibox-macos-amd64.zip, asset_content_type: application/zip } } + - { uses: actions/upload-release-asset@v1, env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }, with: { upload_url: "${{ steps.create_release.outputs.upload_url }}", asset_path: minibox-macos-arm64.zip, asset_name: minibox-macos-arm64.zip, asset_content_type: application/zip } } + - { uses: actions/upload-release-asset@v1, env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }, with: { upload_url: "${{ steps.create_release.outputs.upload_url }}", asset_path: minibox-freebsd-amd64.zip, asset_name: minibox-freebsd-amd64.zip, asset_content_type: application/zip } } + - { uses: actions/upload-release-asset@v1, env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }, with: { upload_url: "${{ steps.create_release.outputs.upload_url }}", asset_path: minibox-linux-amd64.zip, asset_name: minibox-linux-amd64.zip, asset_content_type: application/zip } } + - { uses: actions/upload-release-asset@v1, env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }, with: { upload_url: "${{ steps.create_release.outputs.upload_url }}", asset_path: minibox-windows-amd64.exe.zip, asset_name: minibox-windows-amd64.exe.zip, asset_content_type: application/zip } } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8196aa3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +pangbox.sqlite3 +/minibox.exe +/minibox.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7b8bf75 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,250 @@ +# ISC License + +All Pangbox Server code, unless specified otherwise, is licensed under the ISC +license. + +Copyright © 2018-2023, John Chadwick + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +# Modified BSD License + +Some code based on the polyglot project are BSD 3-clause licensed. + +- cmd/minibox/lang/dict/lang.go +- cmd/minibox/lang/update/main.go + +Copyright (c) 2012 The polyglot Authors. +Copyright (c) 2023 John Chadwick + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the `` nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL `` BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Apache License + +Some code imported from the Protobuf project are Apache 2.0-licensed. + +- common/bufconn/bufconn.go + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + diff --git a/README.md b/README.md new file mode 100755 index 0000000..5edba36 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +> **Warning**: This project is still in the early phases of development; **You can't use this to play the game right now**. There is a lot of work left to be done. Pangbox Server is not, at this time, capable of providing a complete PangYa experience, or in fact much of the PangYa experience at all, in its current state. It is provided AS IS as a proof-of-concept that I hope will be useful to the community, and I hope that the groundwork laid will enable us to make a robust set of server implementations for PangYa. +> +> Although the project is actually quite old, it is still in constant flux, and many of the older bits are still being gutted and rewritten while the project progresses. +> +> Thanks for your interest. + +# Pangbox Server +Pangbox Server is a PangYa server for the unmodified PangYa U.S. client. + +## Minibox +Minibox is an all-in-one PangYa server, including everything you need to run PangYa locally. + +* Unfinished but functional PangYa game, login and messaging server. +* Real-time updatelist server; you can edit `.pak` files and launch PangYa immediately to see your changes. +* Simple Windows GUI for editing settings and managing the server. diff --git a/admin/handler.go b/admin/handler.go new file mode 100644 index 0000000..1146180 --- /dev/null +++ b/admin/handler.go @@ -0,0 +1,43 @@ +// Copyright (C) 2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2023 John Chadwick +// SPDX-License-Identifier: ISC + +package admin + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" +) + +type Options struct { +} + +type Handler struct { + router httprouter.Router +} + +func New(opt Options) *Handler { + return &Handler{ + router: *httprouter.New(), + } +} + +func (l *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Debugf("ADMIN: %s %s", r.Method, r.URL.String()) + l.router.ServeHTTP(w, r) +} diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..a2f9926 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,10 @@ +version: v1 +plugins: + - plugin: go + path: ["go", "run", "google.golang.org/protobuf/cmd/protoc-gen-go"] + out: gen/proto/go + opt: paths=source_relative + - plugin: connect-go + out: gen/proto/go + path: ["go", "run", "github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go"] + opt: paths=source_relative diff --git a/buf.work.yaml b/buf.work.yaml new file mode 100644 index 0000000..7e7c9c9 --- /dev/null +++ b/buf.work.yaml @@ -0,0 +1,3 @@ +version: v1 +directories: +- proto diff --git a/cmd/gameserver/main.go b/cmd/gameserver/main.go new file mode 100644 index 0000000..8d82365 --- /dev/null +++ b/cmd/gameserver/main.go @@ -0,0 +1,77 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "context" + "flag" + + "github.com/pangbox/server/common/hash" + "github.com/pangbox/server/common/topology" + "github.com/pangbox/server/database" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/game" + log "github.com/sirupsen/logrus" + "github.com/xo/dburl" +) + +//go:generate go run github.com/josephspurrier/goversioninfo/cmd/goversioninfo -platform-specific=true + +var ( + listenAddr = ":20202" + topologyURL = "h2c://localhost:41141" + databaseURI = "sqlite://pangbox.sqlite3" +) + +func init() { + flag.StringVar(&topologyURL, "topology_url", topologyURL, "URL of topology server") + flag.StringVar(&listenAddr, "addr", listenAddr, "Address to listen on for game server connections.") + flag.StringVar(&databaseURI, "database", databaseURI, "Database URI.") + flag.Parse() +} + +func main() { + ctx := context.Background() + + url, err := dburl.Parse(databaseURI) + if err != nil { + log.Fatalf("Error parsing database URL: %v", err) + } + + db, err := database.OpenDBWithDriver(url.Driver, url.DSN) + if err != nil { + log.Fatalf("Failed to open DB: %v\n", err) + } + + topologyClient, err := topology.NewClient(topology.ClientOptions{ + BaseURL: topologyURL, + }) + if err != nil { + log.Fatalf("Error creating topology client: %v", err) + } + + log.Println("Listening for game server on", listenAddr) + gameServer := game.New(game.Options{ + TopologyClient: topologyClient, + AccountsService: accounts.NewService(accounts.Options{ + Database: db, + Hasher: hash.Bcrypt{}, + }), + }) + log.Fatalln(gameServer.Listen(ctx, listenAddr)) +} diff --git a/cmd/gameserver/resource_windows_386.syso b/cmd/gameserver/resource_windows_386.syso new file mode 100644 index 0000000000000000000000000000000000000000..43b0615cdeb595d22a7cd16d6842416604c8fb47 GIT binary patch literal 104090 zcmeHw37k~bneQcv>6fug*qM)EGqCm6As-g&jsMz2F3MwXQP@&KJ{`b~bed_CbZr!T7 z)m?>EaQi#UcfR%9bI(2ZEG4~qS@>1Y>SZ~hb9-Ghe!}=ccB=E|)Lp%-|4qRk5e|Om z)797hj~M^XvCg63_be;=#{^Fi{3hbPtSABP_YLc76sw?o;795Ixdi7B{5rwM34Vj% zc7o>-Y$r%S`}MLepaR!AqI5~GFr-g5q}LhJ=NQr#xuhfMn_cvD)ZcMcK>N-yl)-F&+inZqP3TGU1zqn zo#Ge6`0*e4&;t2BboH|TQo!Y>D7$|6HDz~4$IVISY8dg$Qk1QVgl~BBz#et-gZIrRUVc%~7@ai6!cz6YnK(j~{(Y zEohvg9=m&ndU4A$iF8^1-NSFFCw_CMT5@Ntg8nS)(Ibb{qM0$ZX~kl-b@j4Dx-9?t z%g-zLh0E#i)!p0Gu5~NbN5_vP^11xBt-n*tm!DRTJaR^@Tlb}U;)yTSgAbmu)7G#5 z(oRSI;>Bm|^!f8U6w07{Y+!iG*3BQ==_rG1@#51q1ih6j9a;!4Sa3$6EW#**veC%p z|Gc2#cD3?>S!rDh8g5flB3Jy+XteLSu}HrUT3b%3nKPAYZTgVvWoWa!Rgi`}#;Ga` z9beMFKbk+Qsw_OIZqmJWUc=1~b;Qa``gSQ_8vYmLp?v+MyH%_#Jb`hd(dgHz%0mxL zA3a4?N5Z#rx`fadIK+Da?mayp@mN`iS`F_XaAaBU63Myev{_bcqh-||wyfR1v!4~# zw+ZE9y1d#Mrb4#)ZfxKcKMUj{T1*$PVdyn;o?bj(?OMMokZbMYdsY3gYi5Hsd;XQ%V2TjyRTs=+y#Cz|keLFX+V{aW&Xe$xs_R?g-bmU+1 z>l!t``X)7V!a&tJAwuCvc3Wtojr4Tr|N5pO3T^DNxijpx7-6JkN+0d*`|li1lt;aV zJzytX2qO*Q7q_faZqHdS`P&|QNX-~CfOMRf#7CF+!}pJ>Ik%6tpK~)O4ph&#tx3q^ z$3E}8`I>F#`VrT5dA_0i!9Bawys8N{&6l@tw4Y--e=qs*Jba-3R(0p(VfM4fC?KT=yZ zf2y`_{ZwsO|Eb#8_Nm&u=~LCV!HFXc`8#$vka3XDIP2FrVbGHFmn{C=e*QiAwDF_o?$oNtmGqqNu%GpKmgB0sX)f@dcd)wtt%`(R01o5Zu;urPp7~TRJcUcd z%KH5$O8cUza%(4U>lsx&tb%aE*D{`jROUM1%p_dq(Kz)P(EmI-@bdqt<5xvW>Z^xD zR7<=s(>P@f@K$>D36MuRtO73Rw={jAs)yWwaEm;pouN zpGiC!2M*cx2+CAfjI?Fq{*}bNWAvrx5%2rxIvgDo{*KHy#iQl@`cRoe)CYRFxqpA{ zZOpwy?RA&`iF=MY)l{a9-mIg^T{bfjLk`MEr#e^L)N`yhb&hpd)mmSwH}TGHzvcBM z=%*iMV9_nfw*ROewu~!3hG$PMFYu@=57kBc_s5Wg{lh)*B)uoOJx}y@KZr2iiR zaPN+dY1uGS{n0(&wKu3IYXFw)j8y4SZ1nev}?>u|N6`dz5QO{1=} zFWfup9(ll)$IIt>UZ(ufHf>n8z}6GCKwlbk(HERMb!?#ezp(Ks_0F3IlW4xNcenk{ zq3eP$-aU|q@SbgLYRi*LT;C74oFDz~o;pTNAAFTsKCe~1bLiD1Ihc-wkNkSNUFMDn zgY0@D-yay?4}Y|ksM~iAM?W>G(J3jRBc=H zn3_KH7pj&nyo0a~65_tZeSr5fCyuycGdC`7%MB9L8sy^Kp9DAX6qNMudHN4k`o5qmyfE<7+% zzz`t*gFy#nB>x%Jdzk-9S-d)O#XnQI4veSEa<$_cP;M{YWo0=Tx8HZmL;GrPY!Gxh zRL#hVpwm%B-x*{%#A!U?9?0H!`{RkSGh(|`EE4{GwEQv*sr&jxBcUHsnNujsy5Kz; zw8=hmO+@An9r|_3Ur%ETs&UF{)fnBVss{F_awQd}GN$xcS;=B{$U3KB7}AKHLeBy=OjBJ$~?`(<2pY03XEDT;fDa8{z9d-t@lYGF%dUR{u>dQd;K zbx*q8@6^7n!Z_}seJ}d%(`NI$&!jA5gN)EY_d|7g=*N_%c5m+d=AQ}u?kKZ+)7tp+ zrt?{cZ`_a{?-R-PwP+ zvOnf=t$XBt8XLJQsVzpEhcx5?hvmo==0bkV8Nk>rX!C_JyTy0ZB;rU1PW!7bXQe|f zr3}Feeh< zs<55_W9GAGJL?LNhWU?3M;h`ttmEL@7_UVz(b3DA!jS1Lk`4Crl7BPJnFI}7kpIOO zKTmv%gd9lYOaE@i_>htF0U5`O4$mnCKJx$sO_`JYq$3R3(v_d<5999H>>G(q8KnBQ*%P$UOMmg{RFRqii zeN6JxJh$6Xr_jde^#}Y>UZ=tSMH+142M4%Ze}Ip2Zgkl0z~QjY8?r%0$O@S|$FD*I zkhcH%Z3_4A{P_I~K0#c%4C&J2vXBX~K}JXZ@C__~bkNWLl)mwOhIuu3=g5aP(oF`) zhPg~6|EF^PYGrxJ-Oy{sm;v_tb-L@Ua*dC7e(oI(lJkh%DxYNpyefh!>vmbw!+#3Bh3L<{s*G>320(&{mfRUAJmn0 zhpHYr)bV8`JlsStRXWYd8)fqvHjLy?UMs;oQMamxl(PK4onCt7x4h_*4<>>puKJtr zbF~ZRFd(14!SBlE`#a`E@O{uY)mihvax<)(co)k~u=AUdNr(76kv3Zgj0a3zo1{Z? z-J2>l$R4NtEH>cs@0;lOO{eXUYgcr3XlIG$L5Hq$Pmo@BXES$2 zr>}?YIW6VkJ9S|Dyu{B1uFFYx5Fvl(KpkRqi|LP^>yqYaQ&+cS(J-YdSb$X5We-ktE0)38UoMUZCL zb;4;Kh@Z3wlHE>PWJQ9fdeOYE-j;Rx9HrVWv8)cjX_lqVQ>vZptXh=%7%*;k%+u;i zSCp==(mX9vxA@D#0+{bfeW%g%EZs(Zkl)4ITz*~`9)u2&&{p(m^)-ZUCW;Qe^lwPt z7ojueb73DAyq^~G1sz&$wCASL`($`G%@zD>H{IiFa&6tQ2M4}Kxs5dxVj9M#FpdGg z!Uk^jfa>}fkRJDshN^-R{jc>l9qa^iQISM%(ur3(Eo>}iJif_gs-{Su@hZ&7Q^-U};Ndz-MH z-RXbq>&1K-V$IL9s~)9&4`{z2XZ=qS4%R8n+TEo;-kre{b3%E~9DHkszAOvE*q;>s1`hUaKpy6W2VDo4XLsq( zbsu}+>AEbQS)I^-3C*+9^X|HNn4Oo4d!W1Z$G)}TiFt&&j=1+(7PO6e9@YgQ4>r=% zg9tOv?(+Yt1+#5k>ME{D(gkz8vF-#HuPtExfD1XX9}1@h1-n~+%ujvvu6nhd_E7e# z!}hf+>@x4PzvjD)U)n+Gk9{gJuNTiRj4%1+p9vo35MwU#yviHxc|6!(XZgJ|lh#D| zr5lv~*e8c+VSF)Hc0m2X-Zachhges@YjC<-|K~QWw)gANT*aM^7AkN(*GwYTIKq#TtD8gS+;*V?OcRmry%L-^tk=Qdz#(;k*+S!zn#c? zf%dO}t>p7BNZH(I>GHber<=y~;78~JozO>*xBuH+^dE=3J0}gz>$Q$tkJE)%BZ;$~ zpd;qDO8ukdeb1*kV8_`8s0Vy6q{}Ox@Sy|trDxq~PuKTMd%9BIjClRvdUDpmD4iW5 zea~^4Cv5LUd^XzvYqD5h3FW@l)kDhCYm>FLtMU8Hc&3u%ZHn_cKK9AXRBKwPU!b>@ z+FG-CUB3MPn0VQ3KzZmh<`tuJmULRF!Y%dj=L@wl4vqBpVV1HHoe?Y#Ab6ybgY$VG zaIO5OrhA|HlD(H`_^PGhsBeCaQ=it`i%+mmIn&J)#`mxNlw|@B)-M&r`bIA2z>H)TbCZSm+{gL@ zHr%${$g{cjHR9#WVqR%4yy`)h^(G!4L#Art3Eonkrf8cjYeH_? zyCIW(3?OH8(B=O`bWV2Cx{}Umcp<^FwdpvY^#?njp}j^z+34q|EbZ}s1$aRw?1cas z*akCVJE;vy)I-ShQ8aSJfA&Mygi}>o@)M$C@4Wz?RgtUce1tW$c76-3_rDH2?2Dco zx0z@JosKgn6MP3D}`tIV&TV|D5P`+90m8#_KLxP(iDB zyXOa9IPXE@uj`HTwSPzJ_WvzNX^7Wyfz1(Q-ro5vbICJW6$$6h;1?uw;|ad@WPW8 zHhi!yJmtXNrz~vvVD+X=BLJ~!1a16aT|%2g(8n!nnzDK;b&ASH0jX@;B}!qli1ukp zbr9}JML)t7KkO3`r3+8*;yw{DMDbU&K+yt43luF-v_R1UnOFd0VD@+=o!v(7X8U<> z4Sr86LTAjVTkYV?@7Pc8t*fxdD)u_c#J65$u(uG=pG*G!vRifQIv$lg9#qVCv&R`4 zGgyK#WiOxl&8sXexw3el_rQIfeck>o#?<}#*$ofpqSF1I_+Ey4ALr;ieowPK_vGN7 zUFz_`eZ{K>Z2%u&9uU?Lz&E0O#`lzQZoW;GMr}wxDrOn^=Qu2#b{?_yD{SxEzM%ni`i|eaw+d|TP>zZYWHMH@1@Z0FX@L%aZ z_6pO!>Gx{&N3D7A?xOE$o^2u30qilM`w;t(d-BtOXswKTddYo7-xt`u&-EW`*L7b~IOY5`!db1CdF`d=ivBN9 z|KrR~-Io+z$?d!e5>C?j73foPeJaAj>H{2m3v*?Y#xC}o{Lbletf%Pn!t%NFm+*g= z{h`_K3=@emM)&uEJlixXA;uoUdabCqX zI+qA%`CvXQAmV<`9m2UZIO9N;O=mCue6pGJC;Qn1C4N5Xl1dhwBY`txVsx&C)C2Jt z&0&9N#x!+k|4UuIYvbHTPr8_7!CY6IO(SLZ zilZ&q`OK4cJwVtCSLWedlw6+;XVNv@bSb;Z@0>32)8*F__#2ijNXvh$BhDlBd*5>z zhBzmtCv;7hC(Fx_{cM7a$1q)fJ%NwC%~@WC^}~OumfzE2pJN3$W6TX*a2Qv@8Dj<{ z%05nIK}+Jub6}L~{__`hPF^sjZQf54#-DJf%5i>@c+X1X~LOpRS zE6+a!Pd-z^4L@Jfq-{)o=X8mmFM62d*s}7GF8*Kg>l#-df`(ULq3yAUjOF;uO5Hy4 zd~1`8J(;e{i!l4y1SNh?%I=3Zd?sb~I$s{+J6+oH_}GBg_|URBja~Q@LLX4t#^iTS zm-v2s(33bgpIXYi_zv^^4_VQ7lV#Apd+DK{*U4qk_IQ=ahdh>*A^X_`8INH;=-?%T zcJ@)ir#L^?tIVRAu>>ybnX1n4nY@e-h`xsxpM1#EeFmGUE(e zuQE?Bo^PLRj`Pj=LYh|@{ajq;_vV2;o@Kn~byptC%8>nRf{e$oyX4H2&qv3Pb$LI> zIB=%)Oy%U*GR6eJ+k~Gl>Fh^_Cckq!<1x$^J-p=D{KS$(pRRgfKegkTCj$2iXo9wF z7kc*xPcNDBC6E2cko|0ejK?rv^hhPg8+&)#WBIvklR+Ebjle6FJo%E(eq?C!JEt=q z!=Ux}@c2<0lYKxfXq;m2D}?d8pn3E(y|>>upf;^oq!zYRs(F<+D!g0VS2IcB9TNT3 z6UW}}Hl5jz4B5{n$aoBcwx!&k4Buj(_|2W_Y^VqD`?9%B#`yvb@{ai_WtU>%rF5UH04aT@RW@U9XNDd?jafK({TzCcksK#FzfdHvZDq_4d6k z`N(*o)Pq-cZBaFYjrgT_YSpd-AaXVN7G!R)ep|iNgE*jY4SU#OMEx~ z1(k++fd1h0!Kvrop{}5Bh3_{(@z0jdv#TC;z27m?@zdcI6mB~8 z0AnjXF&EcmZ%x%auFSXp_CLR^i~sR0DkvK=9qCV#-#K04Go7A*Tn`fC?uP#L%iA{w zZnsqW?%w=#7yr$#p5$AOV@KZXQXX@WQ}InVU;2~%Y=RO$U4B90VX6CirQMs> zdY02^_zg?>_&zs5iSL(BCU}MFch(i-dTw)x@f^o^-%0UY!n-QY(2_Pr9OH;+Q}L~{_)I4#h_$tn2jYe3cikpf z3;LVd$*#J&>5K7yd(OG&Bj_}P2ut23zjM084?+f=2If8KJn)Rf8lFPB-*tUt_{A;j zXw2#+*KY&R8}E$RqbAq$epoKu)(EqoO;F-9ogg9JbDq5aE_*KR+2VI;AB^upPmBkw zr}sVVG4smKEjexLr3{Emf13Qx=@Q?KW-;ykob}g&wypFh``H8~e$et1>2+`2k^VIK zozo?L(O=!`ub}NM{mFhdL5Ux&UCdhm^pW_nBd>QX){8ap9zKo~RNBT?p z)8uzfm-v#Nlt;!>$(JfW6}?pDWxkB}G=1hT<&ts6lQ0#Y^q2G}``H8~zN9DRk?~aW zrOHo5FI9P&FXKH;pZQCv{#W&$`I@6Po z^Ck3)$K@nG$2ng@&S%K^9G5WLeCaRgPxiA3N_N{-#NXiEc83QjNF$Z)%cIpr_kc5?BCP+c*!Tn ze~!O|#bpPXAEWJ)GTk1Xj+tc>RmcN&Mv!xwGc`x}eHn022WsTYa zlixYLsyw_#_y5M_3zKN3`u-Dyjy*}k%f3A+7xOmBhi`K5jqd+0d*eq!tJu#b7%LBt z)%`!~*5O^&SAq}PfwS>hV6rdUzo(&;4?e(sFMW@7kzLM2i-bp+{Lbmo($L?L|4-|6 z!0(zyz3vsm-8;`i+8cYx=stu`t0PzZE&JIE67GF^SHt+%Z`B)S0c1D@)x6;svRQ{Iv(V?L~W1kmc z^4^YxW{=wIF8>qh-&FBCMqhefq3Kza*R`H@M5BGrP33R5e6r`UqD@_GD*M*?9Dm2n zw|>%SEND~F{{`>=Hb1_0!>Njd=6d7xXIm}X=I;0UN(Xb z+1kOYd{yKMPa9R1hvsBzkE*hgQZL z{#aEFyvEf&;7Q+lUkdv9h&Gq?af40KO@ZmxIIZj13O3J3XnoK=3z8mePW|PLZuV)M z`b=Q+%FI9$kp?)sx@v*gMO)+>)O-O(>T^!7M_#~9lC=i+2dBWi4M~p6K<}3 zmG%?%{Lb_HXe9K*?$C*ORF#)h=sL-8_0WN;rRf7-bznwpr(rB3yeU_G8P+405Mu)L z9{#Mf4aRMtvs)W4(^?vj(!1~to%gxRm>BUGl1n{Iv`Mdry6C3KS5r}uvJNy=?V`GU zwVQtLL@cAyfY3)Gu(4nfO;hlt@_-0r)q3ij#2^x{m;Glib)ATAM z;j)B&nr)C)H-2W)cU0J_de{)R`y!Es_lszGzi;-mUf@4Ha_OIw9tTYNAw4$edU`L| zMb~zE$BQKNkY(t*MRPvrlnWWW-ko6+*k(qIo`-|3Gup?t7xndP28aJ57cyo_Gd>Td zUHUVvcv<)n^qDf1A>YfzzY#4>%O;|&pt5h~!@MUqTi4uh)nAf)FJ`FUI*gmR(LI~e zqW$}SL$0w;!(MyQpUh`_;F*-}ym9J7?ud@)|A+akSCF#N+NwVwpKnRS?->8yHof%9 zZv`nwu4v)Cpejd_fJ4Ofn{7-WwOTN;DFX>zMZa+36oX2{)pP#NBvF3>6ZlTLK zHixc-qB=kybz~$dRn{L8E1$&Y4%J~iw|H;l%F0E$MGF)yP_#hN0!0fHEdUFsY=7;#9nVuLs>gdN>euK$ z)uG2MYuf4L_$8;3GUd|GK_oO=9b3|AcCszLEeX(~;EG?n zgV@#?w-MVN@ga$gc+wFMlGu8ka>Roqy+P)*BOWBV1aXykCdUJnb}Ide2R3s&5#WRj zAZcrT!}{ho0sB}Lv`^UAtZ#qYiPc>|7PtEJabk-I=UnUJi{r8HTGrRCzI~n8_kL;- zm1r~`JHMC3A964L`j5!2*H~9uW%T#otg9$=sda@Frue1w_unau-@0=@g5@d1%eozS ziJc41=O%LENmeUK+l%N;w8mS*tufXxqBh(bWep+9W2~XpSVA0S<6Lb0$hz4230*(3 zeoB8WtFQGfD*La5Gn)PlA^eHf1WNB`MQlm~DQB|P$ND+Nk$*K^eli`)D!2ZI@L9f}5uFLvAY1Nn#CbSz8-rqra=#*4?%exO+0oWbq-TUm zjwKpc6!jzP0*(GPwwx0*x|6IQ5Z=X!3%vST7g8D6YNCxlhA58Gv=|oGXM&yU=zbA} zC)0)BIO`v%dSKFx*DTG>ck@Zg$Dj|%)6n4_alSSYU_{H%ka*}I8 zADe$4(r_$c!Ixu*0!qQkkOyH$R>#9E^by3T2A$T)|Zm~6BIh~q^d`JVE zCCeeL1DD7B#IPr7t!{KCol+^7A_y5?w z=dQ5n!jDdU<$DS-+^Lpb8<2Yuc^hqjbFcQJa$||dO}0#4_v(-Bdnp-T-cfG%)6a-k zFKXpb7g;LYT~146S>K^H%(9$XJBBoO?Am$nUvTc@=U&^3Q0RA_RgnlyPK4?bp*e}r bqC{v-BD6gbx@i1_@q-+aBpcdu$ngIGkW|93 literal 0 HcmV?d00001 diff --git a/cmd/gameserver/resource_windows_amd64.syso b/cmd/gameserver/resource_windows_amd64.syso new file mode 100644 index 0000000000000000000000000000000000000000..e30b385e77dfbfc8ef8fcb7cfb7a96eb31c55a4a GIT binary patch literal 104090 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 aMTyXwL}+^=bm4@F69zjZNjA`h4F4bVc*89K literal 0 HcmV?d00001 diff --git a/cmd/gameserver/resource_windows_arm.syso b/cmd/gameserver/resource_windows_arm.syso new file mode 100644 index 0000000000000000000000000000000000000000..e72cd3369a2ad3109f0b2cccb16af707f2400192 GIT binary patch literal 104090 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 +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "context" + "flag" + + "github.com/pangbox/server/common/hash" + "github.com/pangbox/server/common/topology" + "github.com/pangbox/server/database" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/login" + log "github.com/sirupsen/logrus" + "github.com/xo/dburl" +) + +//go:generate go run github.com/josephspurrier/goversioninfo/cmd/goversioninfo -platform-specific=true + +var ( + listenAddr = ":10101" + topologyURL = "h2c://localhost:41141" + databaseURI = "sqlite://pangbox.sqlite3" +) + +func init() { + flag.StringVar(&topologyURL, "topology_url", topologyURL, "URL of topology server") + flag.StringVar(&listenAddr, "addr", listenAddr, "Address to listen on for game server connections.") + flag.StringVar(&databaseURI, "database", databaseURI, "Database URI.") + flag.Parse() +} + +func main() { + ctx := context.Background() + + url, err := dburl.Parse(databaseURI) + if err != nil { + log.Fatalf("Error parsing database URL: %v", err) + } + + db, err := database.OpenDBWithDriver(url.Driver, url.DSN) + if err != nil { + log.Fatalf("Failed to open DB: %v\n", err) + } + + topologyClient, err := topology.NewClient(topology.ClientOptions{ + BaseURL: topologyURL, + }) + if err != nil { + log.Fatalf("Error creating topology client: %v", err) + } + + log.Println("Listening for login server on", listenAddr) + loginServer := login.New(login.Options{ + TopologyClient: topologyClient, + AccountsService: accounts.NewService(accounts.Options{ + Database: db, + Hasher: hash.Bcrypt{}, + }), + }) + log.Fatalln(loginServer.Listen(ctx, listenAddr)) +} diff --git a/cmd/loginserver/resource_windows_386.syso b/cmd/loginserver/resource_windows_386.syso new file mode 100644 index 0000000000000000000000000000000000000000..2ce45cb19d469bee4f92aec0809d5f91589a7e3b GIT binary patch literal 104098 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 ag^AGWL}*(gbm903;|IAUNj}g8oBt1pgTvYY literal 0 HcmV?d00001 diff --git a/cmd/loginserver/resource_windows_arm.syso b/cmd/loginserver/resource_windows_arm.syso new file mode 100644 index 0000000000000000000000000000000000000000..fe532ae72b1aee8ee1e93df2d1592dbf00b5c7b1 GIT binary patch literal 104098 zcmeHw37k|#n&+dnTb@Q5)MZGc$l|vC*Q8^S8ltUCK4mnj6K@b%SJU~IkZnaUNV!wa9{IVjydhs%| z@?}*eX2Pp4jxWA5Uc7km;@I4)mxEu!%wCQgI=k0}<0p(Cq*LR6DDPh=_%8(UJD0A$ z`afd)JIgtXg5P(X=pPe2N$>*1dpS`8#_wCsHz-y{`M{6T|Fa3sCiqQ)PZ0bT!EFT3 zA=pZgfbr|)oJR$&bw%lrUS>(3WJ#~Fq|dgbFZ4)9(ghv#bmd%a!S8Fq-$wZR=_-R! z;=jIRkec&2Mz%TMaTdb3@!z;U6#pK_9Opa#PImukd%SNA#m{XQq^!A@b6tD3vyI{x z!}#$Z`OpFRK6Lfce<|ScQ8$U_$X^DfBfh>>iu_G)%$P1r9M3RuBU8%4bY%J0Uw&Ra(il~no?NUxKK_0J_t=rQ)%^O& z>hXJ~tCu!En@E@C-#hfCdh$1Ssl|6yE9lR%9yxqaEu0Zk8<#IqTUISiq|5Seyz+vA zU$~qJU)!}!?OeM;eSGX_BA?4|-SRuNY}qOG=%c6A+O=P)C!hREJ@n9Nowjb>S2`W} zix!>M>GS5bDU?C^*ue1QEt@{k=_rG1(V|ltg5HW1E-i%T&p)kD7Gab@*=XeQf1Y1= zhg$LA%#5!2b+@a@kt_aZG}`y1T!W9YVQ? zF28n$sgP~H7aMrR&jPuK7Sjc+8+y&`XBN#jL?}E_w}m#^NY8}+uWufr(8eyEGhMgE2qUet^wHja@a~~RdDL6j19rlN zFwziyY4cj;^_=ySzvc0V)$}m~NXNNJd`x*i`rwF~eaC41oSSjeK=ne)>V!P$*yr81 zUe|W69dT`k=Nrl&*u7KDt(c%`zOrqDevX;^{p82<@WI;K)LoN?>1WRg}|9XLytxX<{RGnK~C^ob^DR}BvT#k|iD-J5_%ChjIUwS?-f1ssa$w6mxz`0`%jei zWkdOvcHHLEs&ZHv;fAkeJPE1Hb-P1*>fKLy;X;iFXDZI4nP@2W%5p`mX}JQ)WL z+4eBXRF;j@GI9S(;@&y>(sPLS{d66Q4hnx)=G)@Y(tdrY%t7h{J<`~}zwtKaU84HB z%m2hX$DV2{(?W07(c~_horom|<)c&Ft7XbrPK!FrIi#wcuhd(3XV-6eeF>)153{i7 zl4R{as)tSEN{`~%lgkS{%1c8v(f<80B%yz}2cDw$B(LX*+3p7s#(Nyz$;^9&!ta^S z{UG@G(ZG8!@q8ztKeao4__@d<{?v@PKZJ9^!;fY@^GCnw`SsKfq;UXT=o9YQz9AzU zc2p&9CH}~w)?;NHs2;`+M+f;wb=9hfXk($|2hW7vot+$j6eW8OONb^s&YCj&!?f zdHf;$9<+2$gJ+p^_~SiI^QG}ZlaF8c;Hxj`cSp1hc&3`;j%K-Z_?!4ahrReOc;rjn zpMCSqlWNl@H;l1GgaL7(zsq5yn-}MqWjT%Ef(2h{TYB*a4lXWl(&jMdOB}CsmYwOc z{II1Tf7ajB6Jax*JB&pt@> zKT1N2F#elv8<$O8gr2xRY5e#R=8yMb_@hg&lJNgx{xw7UpHIBMj1BySu7A>w&5fUF z+5FyVnHJt{p$E!HIy0KTWF5bVUVkY*DJFhY=4q5`tUcVJ-1OKB@p9pTi2{}Y@gEF2 zC?ok#uiVZ2SIFX(kt_b0%C%uUU6!jJSBG+Y@GdLM$+&*sEe-9hzOhcwX;W1rZvvgR z3i{3<%OOtV3HL(w`a7OTl${>isbZ1v@1v!cVMsl#Uo;Z>5tTWKvaAcwxH@KuTu5V4XR>be=1j0W-DV$kChaSMA?SuMxwP+KbMK_M!f6U${-!@ z%OvMoEi1`OV>k_d_TfdER%XEM$X>(82UWWohWgl%{rVYX9cn8TwsOX4l3w@#jtZvku?5 zAwS+zx;ueCKw36*yRs}~h0F!{Gn?>V=40p9UfZkV z_r-4RarVczbjXTvKFEskJ(|0vDhKvU%&UBp@__T3>PbCLetaiD-NqOY*8@E-l)fi$ z9C^S2E~j+{!UmZ>f8l$Kx9@xYxvY4b@}jQcI|b%L;Tz52*VAj8IwSw)74dNf(j9dr zTRqUGV*DGj@Krsme^R?^>S~5NBR}S|ZlyK^zHa2T`uk#SN%e7}GUOJ~<7m02=NkB7N)u&52N(aA=B zrg`Ay9W-Z6&n?liV9qkh^4c~Mj!<@-u65o{YZ3*8Xk-lg9#=zyr{5>i6hGh#9{7v~%h4KAvZft^< z755YR8JJTD+3{>M=d^p#>Wuw2E?=nW!`_{h+mz+KLvQFgijajb%)Q6=Ruj)F+!^^X z$Mfaq)^*C>UNUeRzHRK?u`#1>9G#J$`Sk?oC;#Er57qtiPU-P+d>6u;NPMfpdIF4@ z&zj|~D?l3NKO!Az$m6hygKuNJ7QsfxENcrxrngBp*w0V?O*Cf`G;l%wmtOiJ@huW^ zAdN5cyB*_0M$QLh96vfdrxf_i0}wP#ogrH)`8Uw~R#SeIH)R7a$ix?TN*v@d4VFI@ z4)Ebm=*hAp4g8RfFl5VAey%@|6LieFv15np3yA|6ATO?TaUe6wfd_bToiy!Zlb`0f z-GMrVHpZ+!;E(bq4fZe6V3TxkfXnp<_$cQ^hwTm=4x79o8)Ss6khy*QDl`CT`(D_p zaR1JW-@o7!#AV8mDLpO=nIIcvbmb4$W{Q+qVI;{PIy4Az4@{;i7rtAn~ z{x-`UqwlM!>S=R)UOCF7^Zj1(LmB1=TE6_+zwc|?#s-A`UeZIHzQMezX%4(vH8*|X zOLKbFo`j$1d)Y8AdVXp8J9OO7pyeljQ+>S6aq)NC=#u>9Y?Fra=X`0_-Rky73IAwx zME^gu(X^#wo&=Ts0Q^A9PyV{wmLzP2wFy?51FrlJMDJ73#N7HB&2B%aBkeX-IdrJ& z%Sd>*jb5sBnv*w5^BOjc0qKLH!hW%>&ELu;!+_S$2Z$-;8WJ#OH~$XdN&fFl9}W4vjT$sn{Sr zPWyRmz~w)%(MdO*>cc-0uit}aV@+!kulSk|SALrRVOwV*c~#wT)n5|LR#%5B6yFPt z)vvYtvZSb3mg{W1JW><^FVqw8fbB8AgubP-?GMsC87x0d`-iobMDvK&f#$tT8;fyp z{r#B6?m{(b6G{`2#slkIUj0<{xM`;B?`5)f#bk$emS`Su>ALp>>2*&wb5~6IW?0W@ zDGlFc@}lsYot2$wPm5go=R{|XOE)|#7qT-i(5CidPF#nl(^-$iW&I%5ZR44pOWB#f z_5<+(Pfk0NA?6AxQu+jxq`fj)0BN;7!zkjt;v0*64It~?4lg+cd$c+PX@*@poZ5!? z35Our?Sw;CBzUqH&HL)@IH%51s^t>LX#H0d& z(;{^Xzbq_(`JU8w8comAt<(qkUA)cZ=XL%;=nx5QL7&!KL+E9q=-^BLhV*?2I%7T; z_F=*MX+B@jq3K3FH;vvW!@Fp%;9q;`9$%BIb;lkY_#Wjo){u{B7@NX42K)*ec+~@{ z>tm2Fzn$fyzBZW`G*`IyFWn2W4$%F+9r|M~fUSJKr@y&(m)0L`o7A7%x-+TXOMgK( z_?WIwI_U4FpRvAYTg3IJo>KCU(;4qfg{)_?_B6&<@V~w{&Q!m%UhL4rm8-o?SkJEXKlb%v zz6`PE=ed=S(Y^DUbYW=ZqEqG!cp{XP8eU=4nqnU?w0my@m%=93_%(JWf zzjFR8txHYWHA%W)jyKkw;NrCftRHY8C-y_(w4h*D>yP=VkKJ9Xw$UES>FTg;&2nAl z-PYIBE|V_pp!CN+m6+Fy=NHD8(&gV7Jj@}+T;#dsH|lvj*k5PaeKV5QM5IeMDE+Zd z4%5Q;Vy^6f`h&e`n3oB$u7KCzbhZA^uV1D2>oMgC+zyzJO+IHHXhU%?@f>ZW^EGO)+f$A&otCX&65)Zy*nwX1jj?H18DeN7Ee5`MfWo>b$7i9Yn zJ7Apy_OWI9fUv{<7u|l1O&-7W<#dmO4)*lozLSZMJ#dhZKBJlL7iRf_)Svlb{S5Y( zV;+Ey&8$sZHm1W6{UXqS9XZW}@qHO}U(!tX3$tuN>JQ&wofh^`X8M3=w|I{?KbdTK ziEkFPUe9Z$|mFccWqDD6Kx)^ z;Xpfpy^dKItl^bqZE-G>t^VkIHMi=s zKv4fW^L!0Tf0irXU_pO(zG15Rzi0Oeb??3Inh?y}{QUFJljflEJlbqwuIRE{bLXB` zdtW(`XphtNzo|Rx4_WzQ`nEi&mlf7j~;LTw|VG44taM?9NMGTI&wYE6k?4e&U%85nBOY( zkCyg5m*#*SV;i6z@V$^JubzYt9k4Gw>rQ*RzHi&pmGY*?>j&49GY&@S>=5aDj?+A0 zy%+JBYy+&xVtpl)`&w5HDaou&HrB4j?=$O}N|LuN&g=NtCo@v5X{COF*;X2B&Ej?W z^8an)rQ3kg(C5r6M&~S#XjXsx3e(5f9+>16L_$GsUX%jayb_kEl4E?&et@{ z)J}RRZGL)vV8Ztu&(=!C+m?^>j#wt}NToODSC6Ys(x1-j{Z|_gGd<~^8+Z+!7j_~g zPn;=+v(>bm^c)Ss_bgMq4jk9CBmL-%*W)SWX{cUq_BH%*8x-B=rK4Y3?ZEIK5wHC&KeXpz zy>?mJTc4#j)a_Ri?#Fa4!QcAP%_gsM&}F@e$0v}fl6Zo*l&2xuqGe6UO?x+VW*-B{ z869-_KM|c1?X<3>a~fVu@N8~4#%KM(&ZlXwkx(}JrBjyn_`d?YAQSdNfDCMd>9HNu z1|{ktWcoN7x#B;kL)V5=QC##BqNDd-0MClZRdhbWYFazLnb!MX2Oj#O=f>?e8bPPy z%*h1bLD%YYAL^&lw{qH_gVPC8KPIz#-X>>x=VKbvAh2O$#b$6>r!4zzgR+ z82nAWalZENXx;w51t|^j`tNjoAbFDC=jEckOC`IuL1}2Vl!xLU;%t;u! zd!KZ$;e*qgHjMzprV+I9gL4UO58wI4YEte>T%_3T-D%D1~Clvh% zSNyO~M3gQ(y$kz9zz~IB!2$&f6f97%K*0h93v|W;7z5Mem2`F+y_@ahy*2nftpMFI zqb{|BJHKNey|=E!9;?{vs58FxD}%j-i2fY%_g7u2TgUOJvm7bK!Vx zp-~UoVv(X>r1H0!ulGxg&l_dm<1DVPvu*Q9_bscJCf3l#>%niM1H*r1`q(c_`=;M# z)E~9_p?eCxr+K#dR0pufgy}=;<8wWh_!n8q2Q@&*pbdmt#EzpXZm)rN4y#JM0h5erG7~ z3-^AWw*6cWq(9luHYo9#P66g;J0SgO^E;sz=}-2v4N82bQ-Jwd52QbB ze&=+FU+{Z=`knnI{mFi|L5a_F3NSzGf%K=%@0>323x3Z}zq8+@KiSVVDDjz20p@2t zkp8s!ozo?L!SDI$clMj~C;Qn3B|g(B!2GNS(w{cJbGpPY_&q=U&VG~rWIx-W#AiAM zn4k4P`qSojPM7!vzvrjl*>BRH>}MO4_)Mn&^RpgEf7<-c=@LI5{f_f0w$iyoILim~ zVF3|O=iDKjOM^2GWZ6vi;_pc|lm28s+n~hnNxGzx1?NcM%$OLRt0DD3JVtZaAD%u{ z9o+YFhws`rw-I@K_KYkKnoDNYE7Yx2a`dEpsfW$)oG$Tuk}iI-v>w=_nkPg&{VsKy zHsoqLw`_UBkGS8fhFq0aS)5IQH7|a$^rSqgJNwxNC4NuR#U=~py5ejaDZ5`BZNZLb zpVIXJVLx1%hjUSKeKwp;*G$u;>^8r1y2Q_vUw7cIUphY{|FMoZk2Kx;p35-AIWgU# zYo|`JK}xemXq56X(4{Z*=UlQG1TL&!${B)9k_8+dBAB>N~&g4t1Px z&9XSdxI@2?c;rK0Q0m8iwn2$++My?gvG!HUjOVkLtT-oP`e=JQfVEDjCthXc`G?@i zXG(bC_mnhg8=K!bUE=o?J#2DpUh!xL|1bV^m8TCu!>_N<^7zBna(rf`X&-sMwN1wE zOgH64nEh;n62CiTPlq>rCS~?JUmoK-9oq8v*nr>o(9+rU9r)x!A5hxH=66n)`04nd zJ8^J6wUl|$o%Z`5vZC)M%bo`e>}MNfJcd0%2R|8%vyT!! z#re5@WfsneC2(2KRCR{W$4BY)MQzYbpcUJr0zSJU@Iwp%)I)*U0&+i&d4X zvb@*Wi^($|>%o>)9roKxyB;)*x?UYV@M_NLfN5KVZGPu;i7)+^ZT#ge>-4=Y`N(*_ z)Pq-dZdO%;_zSe`eu?ZdgKWFlolyd{Ir=nNew{iKR!0Wd3r_Jx2F7eaxUr;#ibD_!8Hc~F` zA7D;LQ2aBYgT3{<=9$0x{Fb!jSvI%1L*03x=GL^zW+JDw8~fP?C4MHng2K(D9$;*x zJLckg?5(L9$Cae*zkM%k?cjfWiweqyOh@|D=66n)_)MoeAlHM$xVxo){mQltf!i&W zzPmO()4_l9Dkr8b$I-)YbtsRy$f@{dnlJsyezrl0pDDi}@$qg2n@X8vJl!@oNZC;5 zq#UW{9hm%4w`KXd5x*+vz}>eeI8rzaO~WOka705AJ=bga0u8hqWkP_%iMJ z^{YCR$2fq*%Qi0SnC-sjbcvrW|4hnf;`iq9r3&+}y6YSKM;{zfUUNb=Kb78hVXt=W zv6;1BOaG%Ugn9R}?s>&=rojeUd($0#K3SJBXH3diQ+AC)+aPhx`1&Q@>y>tGT;p5L zq>*k|%E$M)4NClU`E&*^U;WOyVqDK_E-{|t81FkFo=bRF#Ti=C#)xAa5p4?74hl+M zHotSa#1BdilMec8_#Q5KdBwS%&xLJFnK~QBH-0a9_Bk>y`c>k-w!)0 zgI_txkNs?e65o$@XY$hhR@T$~&bnh9=+2u4Tm35S=@rKsL#7url>BUd=X8l5v^?;u z*u9p04hg7nVvdQ%5trpt&L^?LNzH@!F_wpQ{$JRkjT+5~Gs ze^WilQ#UVtG5)XToQpn!Ni&GB)JKM2+Ps#= ztZw%FHUPcx&WJr~ay{>d7QsR!=Xb7{{Mzf1dId>49RJYXHY z?_rObS9fgAXaQzZSG@r9auvHYo9fmZw0kYwM2mr_Jx2 zF7XTg>RNvVZExvM_OlI2{GjD2(CgazB>idgJEu#0FMl0+>qGVU(vxcD%r-S+hDy3V z|NL`>OQ!eKc)rOi?Fb(%d*Y;e@9pEBeG=(U_OlI2eCdDqe%7pxerLJz&7S!tFWXtd z*|Sf3)&c2Ho8LKI;!FSIzL&hsINQ?(OCym2pmIXv^nx8PAvkgjoNzbdzH+5k7ZP3Iu!!~?QXIvTQbcVKkPPai>J{2D0GfWqs z@wl9X+2q4F(qGb_HotSa#FzA>yfU7t{9iYGwwKgf#u+bDDE%e<$$qv$i7)A;k|&iO zGGE3eeVH%g`G(K(NPAvkgjoNl(fn9`ERbJ-Hcz4rh{!%U(XFLg0;Yoi z5~hpC+moVFW=`ZO|_OlI2d`VBrE8{G`4N82Dr;BgH<8-DcA?Hh& zE*_VY_#Eea2|1r3=W|@bZ1bhRq(5zb=k$t_(C^GLa$ky6<3CcLe2b^De|PKSC!ZYu zIriS+f{f~38%sRDniU0U-ciSgh{(kn&mUa;3{p7>gyy+{CHER29 ze&_Ux((r22{~MMqNTQkQ`%e%$b|(!#`*x>X%-bd(zRAHirvE$ajUNfEWIx+rtTa5< z^#9D;hId$B2|j2C&ctVd&Ax2^?uJr6_yG64^gY%^b~qC)5*}sqJEupBLw`s9KV{Z| zbk{VRb*~Wa+Ib$*-q=gV^dWp&8M)$b+0TBEaPK$EVy^n$9h;Kc7Rj#=?_PhK_TRf> zV~0I-Vx^(Eem*zzP*2GJpUFCacAz`f+MBu+!uEOw-(X*IxwllTr0CPi;-bGceVi&> zUK|=``k%w-58&JzycfuG(h9z@`zBYmfe+Bn$C`MrGs?8zibEq(`CH~khlak*J}MM)uk5XCvs4 ztsVTzS46JxwNZI#Xm)4qQBhJL=YS2vPvQfGAws$8rjbi1F8po5?P zvdxPPwC^dS<%)!F=&Y`lgD&gsr`xnK6*<*ydetj_{%?bPoz*R~;qua= zP*A$L?OSM%*E%Hi1H0c4*q1FHkg zji0EBf!BE22R!Lp@5@0yAJOKrK3=dPx-l^Q>Zf)*TS4=Tgw_S^vmoig=G0%_;ANls zDbEIGpPHLyS$*&F4qqFjUZADh0H1z9Z18oex#4*BI#OS$YWQd`{Z2)T!xsjvSJ3pR z4fsd8zLIqSIt9@W)cqpA=YDBud#;|36~*Bnl3q!3!%RJ^Mol6ePiIvJ8m6xG@;yDz zLN!7ADp&N%OT%@hT{zqj&GKD|-nFaBhI`sa^FX~DSoB|Wr5EVZyTAe&@r9p<;WdbgaIy&Z};p%!hVg+A3Ol*+pcdFQlESZ>;TK_ccTNte`VvHFX_?2m;Mde=tu`UMXplQQ#>b|8$P1`UYVC|5^2=Wc(A){(b&KL zdA9dm*0-{31l56$JnMS>WI6*a(l4P4mmzqvvNZJL?$Rr0z7@qqKY>n$ey+}>J8U5D zdBpK9oyzw_Z+&<7i_ssXI`~2d-Hdjia_CUiRQGN|Pkb}1nb7h4+XRhBXmHRz=x%!D zk#I>uKf^Xis~JBd={qWHRXJ>k*L{&l!}~?FwBH5Ytrz%Di(LBWq{n`nen^iEx}M$( zcG9(t-ti&{J!BdBZqe8ibjpPce(%n(32ZYxM$f}R*IDhO?L~e4s=?vE$c2oZr5T?G z(<=R$QM@GlDEdsDm7%AXi+>|pl95eBTR~;t>Iw6nylh={!&QGt^1aka{nldK#Eb5k zlosvZ|66j6eFpa0gZ^Yswg;X`na&%hKIHc3i2i@rll2NxHd~@ zU-@T2%8@HtcrU1k6fH|fKlB}=gMR*}xss))(uFVSTlTJWY(zMZcXvNOQ#)eK5y{;| zmvwF|}*A)ot1)Stha^!ucvjx@GM?~Xy(F`sF~ zzo&RT3r_Upxh|R5oaUi^kNj`;3m?ZbFcTVQGA`P7dN*8@xJM$PS7QS%|G}B0b6m$c zcn8ksG5uWmBtCbj2IIMfdm~p?F3>GlpkRT51qv1@SfF45SU_d_Yc=h7j#5!G-b+!x z#{8)^Gv+u`PbJ4MIhh>qE#oJW^Up8DlMGU@N0K?5f;|fMFfGvI>w|3XWL&nFsp7J| z^^W5qpv^SZ&9}!j8a3h6*A%5vPKPUxIvChT=e*ia0du?ItJUm$=E4OFtKp(6l;Q(y2OGi?1aCS`=LI zYjqJ@+T$9r)fFF-Xv7n)c#uTvbWvloxXkD*!O?x z5S3^&9y_;}!yj@l{^rGG*K3@sof7){Z_ZT|y41PC2~+%1`upz`#&7M}AH(t#;$__~ zyhP`M^Vx}F+>5SU}MOGuq&(UW0rC(rDiV?_`|xW>9-QRg+jM#4OmXM_8}kA zz-GyENNc&)1zsPjm#hVBj5~7>!BLtM?n!7ij<8%y0T-oRYY!)W?!7pitdEkW9pU~T z>-+8sP1n6IQD6DKLhZ%f?C9D6EiWW*qaASX*M3xPEb+Km%j9>z{#f5j$@ub)^17dX zM!b4aONYA1Qnj~6j`Lk=#~jD4wPQ$g*RJjN|9NLW0R-pTUigdFxGWKxlnB)%LbDU0 bg^AGWL}*(gbm903;|IAUNj|jeVDtY0zEQ)i literal 0 HcmV?d00001 diff --git a/cmd/loginserver/resource_windows_arm64.syso b/cmd/loginserver/resource_windows_arm64.syso new file mode 100644 index 0000000000000000000000000000000000000000..5a217830e32c01f16596e5044c5449cebf981cdd GIT binary patch literal 104098 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 bg^AGWL}*(gbm903;|IAUNj|jeVDtY0xTeGQ literal 0 HcmV?d00001 diff --git a/cmd/loginserver/versioninfo.json b/cmd/loginserver/versioninfo.json new file mode 100755 index 0000000..c51b971 --- /dev/null +++ b/cmd/loginserver/versioninfo.json @@ -0,0 +1,43 @@ +{ + "FixedFileInfo": { + "FileVersion": { + "Major": 1, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "ProductVersion": { + "Major": 1, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "FileFlagsMask": "3f", + "FileFlags ": "00", + "FileOS": "040004", + "FileType": "01", + "FileSubType": "00" + }, + "StringFileInfo": { + "Comments": "Pangbox LoginServer", + "CompanyName": "Pangbox", + "FileDescription": "PangYa LoginServer emulator.", + "FileVersion": "v1.0.0.0", + "InternalName": "loginserver.exe", + "LegalCopyright": "Copyright (c) 2018-2023 John Chadwick", + "LegalTrademarks": "PangYa is a registered trademark of Ntreev Soft Co., Ltd. Corporation. Pangbox is not endorsed or related to Ntreev Soft Co., Ltd. Corporation in any way. PangYa and related trademarks are used strictly for purposes of identification.", + "OriginalFilename": "main.go", + "PrivateBuild": "", + "ProductName": "Pangbox", + "ProductVersion": "v1.0.0.0", + "SpecialBuild": "" + }, + "VarFileInfo": { + "Translation": { + "LangID": "0409", + "CharsetID": "04B0" + } + }, + "IconPath": "../../res/pangbox.ico", + "ManifestPath": "" +} \ No newline at end of file diff --git a/cmd/messageserver/main.go b/cmd/messageserver/main.go new file mode 100644 index 0000000..472628c --- /dev/null +++ b/cmd/messageserver/main.go @@ -0,0 +1,76 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "context" + "flag" + + "github.com/pangbox/server/common/hash" + "github.com/pangbox/server/common/topology" + "github.com/pangbox/server/database" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/message" + log "github.com/sirupsen/logrus" + "github.com/xo/dburl" +) + +//go:generate go run github.com/josephspurrier/goversioninfo/cmd/goversioninfo -platform-specific=true + +var ( + listenAddr = ":30303" + topologyURL = "h2c://localhost:41141" + databaseURI = "sqlite://pangbox.sqlite3" +) + +func init() { + flag.StringVar(&listenAddr, "addr", listenAddr, "Address to listen on for message server connections.") + flag.StringVar(&databaseURI, "database", databaseURI, "Database URI.") + flag.Parse() +} + +func main() { + ctx := context.Background() + + url, err := dburl.Parse(databaseURI) + if err != nil { + log.Fatalf("Error parsing database URL: %v", err) + } + + db, err := database.OpenDBWithDriver(url.Driver, url.DSN) + if err != nil { + log.Fatalf("Failed to open DB: %v\n", err) + } + + topologyClient, err := topology.NewClient(topology.ClientOptions{ + BaseURL: topologyURL, + }) + if err != nil { + log.Fatalf("Error creating topology client: %v", err) + } + + log.Println("Listening for message server on", listenAddr) + messageServer := message.New(message.Options{ + TopologyClient: topologyClient, + AccountsService: accounts.NewService(accounts.Options{ + Database: db, + Hasher: hash.Bcrypt{}, + }), + }) + log.Fatal(messageServer.Listen(ctx, listenAddr)) +} diff --git a/cmd/messageserver/resource_windows_386.syso b/cmd/messageserver/resource_windows_386.syso new file mode 100644 index 0000000000000000000000000000000000000000..7b401fa8720a77f6e7cf5617e7b108da57babd1c GIT binary patch literal 104106 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⃥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 aMX}JDSZI4JbkT%~6NWe>NjA`h4F4Z3s>YZA literal 0 HcmV?d00001 diff --git a/cmd/messageserver/resource_windows_arm.syso b/cmd/messageserver/resource_windows_arm.syso new file mode 100644 index 0000000000000000000000000000000000000000..38152e3c29153d6ef80523fe2b6270160ab4201e GIT binary patch literal 104106 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 ajD=RmLfc}Y3&&3wKiDBjvY~y44F4ZuP{wNj literal 0 HcmV?d00001 diff --git a/cmd/messageserver/resource_windows_arm64.syso b/cmd/messageserver/resource_windows_arm64.syso new file mode 100644 index 0000000000000000000000000000000000000000..d62ac0129ecd7c070241a67a422703610ac27edc GIT binary patch literal 104106 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 ajD=RmLfc}Y3&&3wKiDBjvY~y44F4Zq?Z&hK literal 0 HcmV?d00001 diff --git a/cmd/messageserver/versioninfo.json b/cmd/messageserver/versioninfo.json new file mode 100755 index 0000000..a9dc0fe --- /dev/null +++ b/cmd/messageserver/versioninfo.json @@ -0,0 +1,43 @@ +{ + "FixedFileInfo": { + "FileVersion": { + "Major": 1, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "ProductVersion": { + "Major": 1, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "FileFlagsMask": "3f", + "FileFlags ": "00", + "FileOS": "040004", + "FileType": "01", + "FileSubType": "00" + }, + "StringFileInfo": { + "Comments": "Pangbox MessageServer", + "CompanyName": "Pangbox", + "FileDescription": "PangYa MessageServer emulator.", + "FileVersion": "v1.0.0.0", + "InternalName": "messageserver.exe", + "LegalCopyright": "Copyright (c) 2018-2023 John Chadwick", + "LegalTrademarks": "PangYa is a registered trademark of Ntreev Soft Co., Ltd. Corporation. Pangbox is not endorsed or related to Ntreev Soft Co., Ltd. Corporation in any way. PangYa and related trademarks are used strictly for purposes of identification.", + "OriginalFilename": "main.go", + "PrivateBuild": "", + "ProductName": "Pangbox", + "ProductVersion": "v1.0.0.0", + "SpecialBuild": "" + }, + "VarFileInfo": { + "Translation": { + "LangID": "0409", + "CharsetID": "04B0" + } + }, + "IconPath": "../../res/pangbox.ico", + "ManifestPath": "" +} \ No newline at end of file diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go new file mode 100644 index 0000000..95c228f --- /dev/null +++ b/cmd/migrate/main.go @@ -0,0 +1,87 @@ +// Copyright (C) 2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "flag" + "fmt" + "os" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/pangbox/server/database" + _ "github.com/pangbox/server/migrations" + "github.com/pressly/goose/v3" + log "github.com/sirupsen/logrus" + "github.com/xo/dburl" + _ "modernc.org/sqlite" +) + +const usageString = `Usage: %s COMMAND [ARGUMENTS...] + +COMMANDS: + up Migrate the DB to the most recent version available + up-by-one Migrate the DB up by 1 + up-to VERSION Migrate the DB to a specific VERSION + down Roll back the version by 1 + down-to VERSION Roll back to a specific VERSION + fix Apply sequential ordering to migrations + redo Re-run the latest migration + reset Roll back all migrations + status Dump the migration status for the current DB + version Print the current version of the database + create NAME [sql|go] Creates new migration file with the current timestamp + +FLAGS: +` + +func main() { + dbstring := flag.String("database", "pgx://localhost", "Database URL.") + flag.Parse() + args := flag.Args() + + if len(args) < 1 { + fmt.Fprintf(os.Stderr, usageString, os.Args[0]) + flag.CommandLine.PrintDefaults() + return + } + + url, err := dburl.Parse(*dbstring) + if err != nil { + log.Fatalf("error parsing URL: %v", err) + } + + db, err := database.OpenDBWithDriver(url.Driver, url.DSN) + if err != nil { + log.Fatalf("goose: failed to open DB: %v\n", err) + } + + defer func() { + if err := db.Close(); err != nil { + log.Fatalf("goose: failed to close DB: %v\n", err) + } + }() + + arguments := []string{} + if len(args) > 1 { + arguments = append(arguments, args[1:]...) + } + + if err := goose.Run(args[0], db, ".", arguments...); err != nil { + log.Fatalf("goose %v: %v", args[0], err) + } +} diff --git a/cmd/minibox/README.md b/cmd/minibox/README.md new file mode 100755 index 0000000..02100b6 --- /dev/null +++ b/cmd/minibox/README.md @@ -0,0 +1,3 @@ +# Minibox + +Minibox is a simple Pangbox server that contains everything necessary to host a PangYa server in a single executable. \ No newline at end of file diff --git a/cmd/minibox/cli.go b/cmd/minibox/cli.go new file mode 100755 index 0000000..48b037a --- /dev/null +++ b/cmd/minibox/cli.go @@ -0,0 +1,52 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "context" + "runtime" + + _ "github.com/pangbox/server/migrations" + "github.com/pangbox/server/minibox" + log "github.com/sirupsen/logrus" + _ "modernc.org/sqlite" +) + +//go:generate go run github.com/josephspurrier/goversioninfo/cmd/goversioninfo -manifest minibox.manifest -platform-specific=true + +func cliMain() { + ctx := context.Background() + log.SetLevel(log.DebugLevel) + log.Println("Welcome to Pangbox. Main thread started.") + + server := minibox.NewServer(ctx, log.WithContext(ctx)) + if err := server.ConfigureDatabase(dbOpts); err != nil { + log.Fatalf("Error setting up database: %v", err) + } + + if err := server.ConfigureServices(opts); err != nil { + log.Fatalf("Error setting up services: %v -- try setting -pangya_dir?", err) + } + + log.Infof("Web server listening on %s", opts.WebAddr) + log.Infof("QA auth server listening on %s", opts.QAAuthAddr) + log.Infof("Login server listening on %s", opts.LoginAddr) + log.Infof("Game server listening on %s", opts.GameAddr) + log.Infof("Message server listening on %s", opts.MessageAddr) + runtime.Goexit() +} diff --git a/cmd/minibox/config.go b/cmd/minibox/config.go new file mode 100644 index 0000000..04e796e --- /dev/null +++ b/cmd/minibox/config.go @@ -0,0 +1,129 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "encoding/json" + "flag" + "os" + "reflect" + + "github.com/pangbox/server/minibox" +) + +var ( + opts = minibox.Options{ + WebAddr: ":8080", + AdminAddr: ":8081", + QAAuthAddr: ":8090", + LoginAddr: ":10101", + GameAddr: ":20202", + MessageAddr: ":30303", + ServerIP: "127.0.0.1", + GameServerName: "Pangbox", + GameChannelName: "Snowblind", + PangyaRegion: "", + PangyaDir: ".", + PangyaIFF: "", + } + dbOpts = minibox.DataOptions{ + DatabaseURI: "sqlite://pangbox.sqlite3", + } + // Only used in GUI. + language = "" +) + +func init() { + flag.StringVar(&opts.WebAddr, "web_addr", opts.WebAddr, "Address to listen on for webserver connections.") + flag.StringVar(&opts.AdminAddr, "admin_addr", opts.AdminAddr, "Address to listen on for admin control panel.") + flag.StringVar(&opts.QAAuthAddr, "qaauth_addr", opts.QAAuthAddr, "Address to listen on for QA authentication connections.") + flag.StringVar(&opts.LoginAddr, "login_addr", opts.LoginAddr, "Address to listen on for login server connections.") + flag.StringVar(&opts.GameAddr, "game_addr", opts.GameAddr, "Address to listen on for game server connections.") + flag.StringVar(&opts.MessageAddr, "message_addr", opts.MessageAddr, "Address to listen on for message server connections.") + flag.StringVar(&opts.ServerIP, "server_ip", opts.ServerIP, "IP address to advertise.") + flag.StringVar(&opts.GameServerName, "game_server_name", opts.GameServerName, "Name of game server.") + flag.StringVar(&opts.GameChannelName, "game_channel_name", opts.GameChannelName, "Name of game channel.") + flag.StringVar(&opts.PangyaRegion, "pangya_region", opts.PangyaRegion, "Region of client, or auto-detect.") + flag.StringVar(&opts.PangyaDir, "pangya_dir", opts.PangyaDir, "Directory of PangYa client.") + flag.StringVar(&opts.PangyaIFF, "pangya_iff", opts.PangyaIFF, "OPTIONAL: Client IFF to load. Overrides the IFF found in the pak files if specified.") + flag.StringVar(&dbOpts.DatabaseURI, "database", dbOpts.DatabaseURI, "Database URI.") + flag.StringVar(&language, "lang", language, "Language to use in the UI, if enabled.") +} + +type pangboxConfig struct { + Database minibox.DataOptions `json:"Database"` + Options minibox.Options `json:"Options"` + Language string `json:"Language,omitempty"` +} + +// saveConfiguration saves the configuration to disk. +func saveConfiguration(file string) error { + f, err := os.Create(file) + if err != nil { + return err + } + + return json.NewEncoder(f).Encode(pangboxConfig{ + Database: dbOpts, + Options: opts, + Language: language, + }) +} + +// loadConfiguration loads the configuration from disk. +func loadConfiguration(file string) error { + f, err := os.Open(file) + if err != nil { + return err + } + + config := pangboxConfig{} + err = json.NewDecoder(f).Decode(&config) + if err != nil { + return err + } + + copyNotEmptyFields(&dbOpts, &config.Database) + copyNotEmptyFields(&opts, &config.Options) + if config.Language != "" { + language = config.Language + } + return nil +} + +func copyNotEmptyFields(dst, src any) { + dstv := reflect.ValueOf(dst) + srcv := reflect.ValueOf(src) + for dstv.Type().Kind() == reflect.Ptr { + dstv = dstv.Elem() + } + for srcv.Type().Kind() == reflect.Ptr { + srcv = srcv.Elem() + } + if dstv.Type() != srcv.Type() { + panic("bad usage") + } + for i := 0; i < dstv.NumField(); i++ { + sf := srcv.Field(i) + if reflect.New(sf.Type()).Elem().Equal(sf) { + // field is equal to zero value + continue + } + dstv.Field(i).Set(sf) + } +} diff --git a/cmd/minibox/config_test.go b/cmd/minibox/config_test.go new file mode 100644 index 0000000..db5eebd --- /dev/null +++ b/cmd/minibox/config_test.go @@ -0,0 +1,38 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "testing" + + "github.com/pangbox/server/minibox" + "github.com/stretchr/testify/assert" +) + +func TestCopyNotEmptyFields(t *testing.T) { + dst := minibox.Options{ + AdminAddr: ":1010", + } + src := minibox.Options{ + AdminAddr: "", + WebAddr: ":8080", + } + copyNotEmptyFields(&dst, &src) + assert.Equal(t, ":1010", dst.AdminAddr) + assert.Equal(t, ":8080", dst.WebAddr) +} diff --git a/cmd/minibox/lang/dict/lang.go b/cmd/minibox/lang/dict/lang.go new file mode 100644 index 0000000..13dd8b7 --- /dev/null +++ b/cmd/minibox/lang/dict/lang.go @@ -0,0 +1,204 @@ +// Copyright (c) 2012 The polyglot Authors. +// Copyright (c) 2023 John Chadwick +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. The names of the authors may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// SPDX-FileCopyrightText: Copyright (c) 2012 The polyglot Authors. +// SPDX-FileCopyrightText: Copyright (c) 2023 John Chadwick +// SPDX-License-Identifier: BSD-3-Clause +// +// Based on https://github.com/lxn/polyglot. + +package dict + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "path" + "strings" + + "github.com/pangbox/server/cmd/minibox/lang" +) + +var ( + // ErrInvalidLocale is returned if a specified locale is invalid. + ErrInvalidLocale = errors.New("invalid locale") +) + +// Dict provides translated strings appropriate for a specific locale. +type Dict struct { + dirPath string + locales []string + locale2SourceKey2Trans map[string]map[string]string +} + +// NewDict returns a new Dict with the specified locale. +func NewDict(locale string) (*Dict, error) { + locales := localesChainForLocale(locale) + if len(locales) == 0 { + return nil, ErrInvalidLocale + } + + d := &Dict{ + locales: locales, + locale2SourceKey2Trans: make(map[string]map[string]string), + } + + if err := d.loadTranslations(); err != nil { + return nil, err + } + + return d, nil +} + +// DirPath returns the translations directory path of the Dict. +func (d *Dict) DirPath() string { + return d.dirPath +} + +// Locale returns the locale of the Dict. +func (d *Dict) Locale() string { + return d.locales[0] +} + +// Translation returns a translation of the source string to the locale of the +// Dict or the source string, if no matching translation was found. +// +// Provided context arguments are used for disambiguation. +func (d *Dict) Translation(source string, context ...string) string { + if d == nil { + return source + } + + for _, locale := range d.locales { + if sourceKey2Trans, ok := d.locale2SourceKey2Trans[locale]; ok { + if trans, ok := sourceKey2Trans[sourceKey(source, context)]; ok && trans != "" { + return trans + } + } + } + + return source +} + +func (d *Dict) loadTranslation(reader io.Reader, locale string) error { + var trf lang.Translation + + if err := json.NewDecoder(reader).Decode(&trf); err != nil { + return err + } + + sourceKey2Trans, ok := d.locale2SourceKey2Trans[locale] + if !ok { + sourceKey2Trans = make(map[string]string) + + d.locale2SourceKey2Trans[locale] = sourceKey2Trans + } + + for _, m := range trf.Messages { + if m.Translation != "" { + sourceKey2Trans[sourceKey(m.Source, m.Context)] = m.Translation + } + } + + return nil +} + +func (d *Dict) loadTranslations() error { + dirPath := "." + + entries, err := lang.TranslationFS.ReadDir(dirPath) + if err != nil { + return err + } + + for _, entry := range entries { + fullPath := path.Join(dirPath, entry.Name()) + if locale := d.matchingLocaleFromFileName(entry.Name()); locale != "" { + file, err := lang.TranslationFS.Open(fullPath) + if err != nil { + return err + } + defer file.Close() + + if err := d.loadTranslation(file, locale); err != nil { + return err + } + } + } + + return nil +} + +func (d *Dict) matchingLocaleFromFileName(name string) string { + for _, locale := range d.locales { + if name == fmt.Sprintf("%s.json", locale) { + return locale + } + } + + return "" +} + +func sourceKey(source string, context []string) string { + if len(context) == 0 { + return source + } + + return fmt.Sprintf("__%s__%s__", source, strings.Join(context, "__")) +} + +func localesChainForLocale(locale string) []string { + parts := strings.Split(locale, "_") + if len(parts) > 2 { + return nil + } + + if len(parts[0]) != 2 { + return nil + } + + for _, r := range parts[0] { + if r < rune('a') || r > rune('z') { + return nil + } + } + + if len(parts) == 1 { + return []string{parts[0]} + } + + if len(parts[1]) < 2 || len(parts[1]) > 3 { + return nil + } + + for _, r := range parts[1] { + if r < rune('A') || r > rune('Z') { + return nil + } + } + + return []string{locale, parts[0]} +} diff --git a/cmd/minibox/lang/embed.go b/cmd/minibox/lang/embed.go new file mode 100644 index 0000000..77586d5 --- /dev/null +++ b/cmd/minibox/lang/embed.go @@ -0,0 +1,18 @@ +package lang + +import ( + "embed" +) + +//go:embed *.json +var TranslationFS embed.FS + +type Message struct { + Source string `json:"Source"` + Context []string `json:"Context,omitempty"` + Translation string `json:"Translation"` +} + +type Translation struct { + Messages []*Message `json:"Messages"` +} diff --git a/cmd/minibox/lang/en.json b/cmd/minibox/lang/en.json new file mode 100644 index 0000000..75d4ded --- /dev/null +++ b/cmd/minibox/lang/en.json @@ -0,0 +1,232 @@ +{ + "Messages": [ + { + "Source": "\u0026Patch", + "Translation": "\u0026Patch" + }, + { + "Source": "\u0026Quit", + "Translation": "\u0026Quit" + }, + { + "Source": "\u0026Save", + "Translation": "\u0026Save" + }, + { + "Source": "\u0026Unpatch", + "Translation": "\u0026Unpatch" + }, + { + "Source": "Admin Server", + "Translation": "Admin Server" + }, + { + "Source": "Auto-detect (slower)", + "Translation": "Auto-detect (slower)" + }, + { + "Source": "Browse", + "Translation": "Browse" + }, + { + "Source": "Channel Name", + "Translation": "Channel Name" + }, + { + "Source": "Checking...", + "Translation": "Checking..." + }, + { + "Source": "Click the tray icon to configure. Right click the tray icon to exit.", + "Translation": "Click the tray icon to configure. Right click the tray icon to exit." + }, + { + "Source": "Database", + "Translation": "Database" + }, + { + "Source": "Database Configuration", + "Translation": "Database Configuration" + }, + { + "Source": "Database URI", + "Translation": "Database URI" + }, + { + "Source": "E\u0026xit", + "Translation": "E\u0026xit" + }, + { + "Source": "English", + "Translation": "English" + }, + { + "Source": "Error", + "Translation": "Error" + }, + { + "Source": "Error configuring services: %s - Either move Minibox to your PangYa directory, OR set the PangYa directory to the location of your client.", + "Translation": "Error configuring services: %s - Either move Minibox to your PangYa directory, OR set the PangYa directory to the location of your client." + }, + { + "Source": "Error loading settings from disk: %s", + "Translation": "Error loading settings from disk: %s" + }, + { + "Source": "Error saving to disk: %s; settings may not be retained next time you start Minibox.", + "Translation": "Error saving to disk: %s; settings may not be retained next time you start Minibox." + }, + { + "Source": "Fatal Error", + "Translation": "Fatal Error" + }, + { + "Source": "Game Server", + "Translation": "Game Server" + }, + { + "Source": "Japanese", + "Translation": "Japanese" + }, + { + "Source": "Language", + "Translation": "Language" + }, + { + "Source": "Listen Addresses", + "Translation": "Listen Addresses" + }, + { + "Source": "Login Server", + "Translation": "Login Server" + }, + { + "Source": "Logs", + "Translation": "Logs" + }, + { + "Source": "Main", + "Translation": "Main" + }, + { + "Source": "Main Configuration", + "Translation": "Main Configuration" + }, + { + "Source": "Message Server", + "Translation": "Message Server" + }, + { + "Source": "Minibox - All-in-one PangYa Server", + "Translation": "Minibox - All-in-one PangYa Server" + }, + { + "Source": "Minibox PangYa Server", + "Translation": "Minibox PangYa Server" + }, + { + "Source": "Network", + "Translation": "Network" + }, + { + "Source": "Note: Consider moving Minibox to your PangYa install directory and running from there.", + "Translation": "Note: Consider moving Minibox to your PangYa install directory and running from there." + }, + { + "Source": "PangYa Path", + "Translation": "PangYa Path" + }, + { + "Source": "PangYa Region", + "Translation": "PangYa Region" + }, + { + "Source": "Patch your PangYa installation with Rugburn to bypass GameGuard and redirect network requests. Visit rugburn.gg for more information.", + "Translation": "Patch your PangYa installation with Rugburn to bypass GameGuard and redirect network requests. Visit rugburn.gg for more information." + }, + { + "Source": "Patching failed: %v", + "Translation": "Patching failed: %v" + }, + { + "Source": "QA Auth Server", + "Translation": "QA Auth Server" + }, + { + "Source": "Re-\u0026patch", + "Translation": "Re-\u0026patch" + }, + { + "Source": "Rugburn", + "Translation": "Rugburn" + }, + { + "Source": "SQLite3 Database (*.sqlite3)|*.sqlite3", + "Translation": "SQLite3 Database (*.sqlite3)|*.sqlite3" + }, + { + "Source": "Server Name", + "Translation": "Server Name" + }, + { + "Source": "Server Names", + "Translation": "Server Names" + }, + { + "Source": "Set PangYa Path", + "Translation": "Set PangYa Path" + }, + { + "Source": "Set SQLite3 Database", + "Translation": "Set SQLite3 Database" + }, + { + "Source": "Start", + "Translation": "Start" + }, + { + "Source": "Status: Error (Set pangya dir?)", + "Translation": "Status: Error (Set pangya dir?)" + }, + { + "Source": "Status: Patched (ver: %s)", + "Translation": "Status: Patched (ver: %s)" + }, + { + "Source": "Status: Unknown Rugburn version", + "Translation": "Status: Unknown Rugburn version" + }, + { + "Source": "Status: Unknown ijl15 patch", + "Translation": "Status: Unknown ijl15 patch" + }, + { + "Source": "Status: Unpatched", + "Translation": "Status: Unpatched" + }, + { + "Source": "Stop", + "Translation": "Stop" + }, + { + "Source": "The database will be created automatically if it does not exist.", + "Translation": "The database will be created automatically if it does not exist." + }, + { + "Source": "Topology Server", + "Translation": "Topology Server" + }, + { + "Source": "Unpatching failed: %v", + "Translation": "Unpatching failed: %v" + }, + { + "Source": "Warning", + "Translation": "Warning" + }, + { + "Source": "Web Server", + "Translation": "Web Server" + } + ] +} diff --git a/cmd/minibox/lang/ja.json b/cmd/minibox/lang/ja.json new file mode 100644 index 0000000..93c86aa --- /dev/null +++ b/cmd/minibox/lang/ja.json @@ -0,0 +1,232 @@ +{ + "Messages": [ + { + "Source": "\u0026Patch", + "Translation": "パッチ適用(\u0026P)" + }, + { + "Source": "\u0026Quit", + "Translation": "終了(\u0026X)" + }, + { + "Source": "\u0026Save", + "Translation": "保存(\u0026S)" + }, + { + "Source": "\u0026Unpatch", + "Translation": "パッチ解除(\u0026U)" + }, + { + "Source": "Admin Server", + "Translation": "管理サーバ" + }, + { + "Source": "Auto-detect (slower)", + "Translation": "自動検出(遅延あり)" + }, + { + "Source": "Browse", + "Translation": "ブラウズ" + }, + { + "Source": "Channel Name", + "Translation": "チャネル名" + }, + { + "Source": "Checking...", + "Translation": "確認中..." + }, + { + "Source": "Click the tray icon to configure. Right click the tray icon to exit.", + "Translation": "トレイアイコンをクリックして設定します。トレイアイコンを右クリックして終了します。" + }, + { + "Source": "Database", + "Translation": "データベース" + }, + { + "Source": "Database Configuration", + "Translation": "データベース" + }, + { + "Source": "Database URI", + "Translation": "データベースURI" + }, + { + "Source": "E\u0026xit", + "Translation": "終了(\u0026X)" + }, + { + "Source": "English", + "Translation": "英語 (English)" + }, + { + "Source": "Error", + "Translation": "エラー" + }, + { + "Source": "Error configuring services: %s - Either move Minibox to your PangYa directory, OR set the PangYa directory to the location of your client.", + "Translation": "サービス設定エラー:%s - Miniboxをパンヤディレクトリに移動するか、パンヤのディレクトリをクライアントの場所に設定してください。" + }, + { + "Source": "Error loading settings from disk: %s", + "Translation": "ファイルからの設定の読み込みエラー:%s" + }, + { + "Source": "Error saving to disk: %s; settings may not be retained next time you start Minibox.", + "Translation": "Miniboxを次回起動するとき、設定が保持されない可能性があります。ファイルへの保存エラー:%s" + }, + { + "Source": "Fatal Error", + "Translation": "Fatal Error" + }, + { + "Source": "Game Server", + "Translation": "ゲームサーバー" + }, + { + "Source": "Japanese", + "Translation": "日本語" + }, + { + "Source": "Language", + "Translation": "言語" + }, + { + "Source": "Listen Addresses", + "Translation": "リッスンアドレス" + }, + { + "Source": "Login Server", + "Translation": "ログインサーバー" + }, + { + "Source": "Logs", + "Translation": "ログ" + }, + { + "Source": "Main", + "Translation": "メイン" + }, + { + "Source": "Main Configuration", + "Translation": "メイン" + }, + { + "Source": "Message Server", + "Translation": "メッセージサーバー" + }, + { + "Source": "Minibox - All-in-one PangYa Server", + "Translation": "Minibox - 全機能一体型パンヤサーバ" + }, + { + "Source": "Minibox PangYa Server", + "Translation": "Miniboxパンヤサーバ" + }, + { + "Source": "Network", + "Translation": "ネットワーク" + }, + { + "Source": "Note: Consider moving Minibox to your PangYa install directory and running from there.", + "Translation": "注意:Miniboxをパンヤのインストールディレクトリに移動して、そこから起動することを検討してください。" + }, + { + "Source": "PangYa Path", + "Translation": "パンヤのパス" + }, + { + "Source": "PangYa Region", + "Translation": "パンヤの地域" + }, + { + "Source": "Patch your PangYa installation with Rugburn to bypass GameGuard and redirect network requests. Visit rugburn.gg for more information.", + "Translation": "GameGuardをバイパスし、ネットワークリクエストをリダイレクトするために、パンヤのインストールにRugburnをパッチしてください。詳細はrugburn.ggをご覧ください。" + }, + { + "Source": "Patching failed: %v", + "Translation": "パッチ適用に失敗しました:%v" + }, + { + "Source": "QA Auth Server", + "Translation": "QA認証サーバ" + }, + { + "Source": "Re-\u0026patch", + "Translation": "再適用" + }, + { + "Source": "Rugburn", + "Translation": "Rugburn" + }, + { + "Source": "SQLite3 Database (*.sqlite3)|*.sqlite3", + "Translation": "SQLite3データベース (*.sqlite3)|*.sqlite3" + }, + { + "Source": "Server Name", + "Translation": "サーバーの名前" + }, + { + "Source": "Server Names", + "Translation": "サーバー名" + }, + { + "Source": "Set PangYa Path", + "Translation": "パンヤのパスを設定" + }, + { + "Source": "Set SQLite3 Database", + "Translation": "SQLite3データベースを設定" + }, + { + "Source": "Start", + "Translation": "スタート" + }, + { + "Source": "Status: Error (Set pangya dir?)", + "Translation": "ステータス:エラー(パンヤのパスを設定しましたか?)" + }, + { + "Source": "Status: Patched (ver: %s)", + "Translation": "ステータス:パッチ適用済み(ver:%s)" + }, + { + "Source": "Status: Unknown Rugburn version", + "Translation": "ステータス:不明なRugburnバージョン" + }, + { + "Source": "Status: Unknown ijl15 patch", + "Translation": "ステータス:不明なijl15パッチ" + }, + { + "Source": "Status: Unpatched", + "Translation": "ステータス:パッチ未適用" + }, + { + "Source": "Stop", + "Translation": "ストップ" + }, + { + "Source": "The database will be created automatically if it does not exist.", + "Translation": "データベースが存在しない場合、自動的に作成されます" + }, + { + "Source": "Topology Server", + "Translation": "トポロジーサーバ" + }, + { + "Source": "Unpatching failed: %v", + "Translation": "パッチ解除に失敗しました:%v" + }, + { + "Source": "Warning", + "Translation": "警告" + }, + { + "Source": "Web Server", + "Translation": "Webサーバー" + } + ] +} diff --git a/cmd/minibox/lang/update/main.go b/cmd/minibox/lang/update/main.go new file mode 100644 index 0000000..48b0f66 --- /dev/null +++ b/cmd/minibox/lang/update/main.go @@ -0,0 +1,235 @@ +// Copyright (c) 2012 The polyglot Authors. +// Copyright (c) 2023 John Chadwick +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. The names of the authors may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// SPDX-License-Identifier: BSD-3-Clause +// +// Based on https://github.com/lxn/polyglot. + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "path" + "path/filepath" + "runtime/debug" + "strings" + + "github.com/pangbox/server/cmd/minibox/lang" + "golang.org/x/exp/slices" +) + +var ( + outPath = flag.String("out", "", "The directory to output locale files into.") + srcPath = flag.String("src", "", "The directory path where to recursively search for Go files.") + locales = flag.String("locales", "", `Comma-separated list of locales, for which to generate or update tr files. e.g.: "de_AT,de_DE,de,es,fr,it".`) + defLocale = flag.String("default", "en", "Locale that should be treated as the default locale.") +) + +func logFatal(err error) { + log.Fatalf(`An error occurred: %s + + Stack: + %s`, + err, debug.Stack()) +} + +func sourceKey(source string, context []string) string { + if len(context) == 0 { + return source + } + + return fmt.Sprintf("__%s__%s__", source, strings.Join(context, "__")) +} + +type visitor struct { + fileSet *token.FileSet + sourceKey2Message map[string]*lang.Message +} + +func (v visitor) Visit(node ast.Node) (w ast.Visitor) { + if callExpr, ok := node.(*ast.CallExpr); ok { + if ident, ok := callExpr.Fun.(*ast.Ident); !ok || ident.Name != "tr" { + return v + } + + if len(callExpr.Args) > 0 { + if basicLit, ok := callExpr.Args[0].(*ast.BasicLit); ok { + source := string(basicLit.Value[1 : len(basicLit.Value)-1]) + var context []string + for _, arg := range callExpr.Args[1:] { + if basicLit, ok := arg.(*ast.BasicLit); ok { + c := string(basicLit.Value[1 : len(basicLit.Value)-1]) + context = append(context, c) + } + } + srcKey := sourceKey(source, context) + v.sourceKey2Message[srcKey] = &lang.Message{Source: source, Context: context} + } + } + } + + return v +} + +func (v visitor) scanDir(dirPath string) { + dir, err := os.Open(dirPath) + if err != nil { + logFatal(err) + } + defer dir.Close() + + names, err := dir.Readdirnames(-1) + if err != nil { + logFatal(err) + } + + for _, name := range names { + fullPath := path.Join(dirPath, name) + + fi, err := os.Stat(fullPath) + if err != nil { + logFatal(err) + } + + if fi.IsDir() { + v.scanDir(fullPath) + } else if !fi.IsDir() && strings.HasSuffix(fullPath, ".go") { + astFile, err := parser.ParseFile(v.fileSet, fullPath, nil, 0) + if err != nil { + logFatal(err) + } + + ast.Walk(v, astFile) + } + } +} + +func readTranslation(filePath string) map[string]*lang.Message { + sk2m := make(map[string]*lang.Message) + + if fi, _ := os.Stat(filePath); fi == nil { + return sk2m + } + + file, err := os.Open(filePath) + if err != nil { + logFatal(err) + } + defer file.Close() + + var trf lang.Translation + if err := json.NewDecoder(file).Decode(&trf); err != nil { + logFatal(err) + } + + for _, msg := range trf.Messages { + sk2m[sourceKey(msg.Source, msg.Context)] = msg + } + + return sk2m +} + +func writeTranslation(filePath string, trf lang.Translation) { + slices.SortFunc(trf.Messages, func(a, b *lang.Message) bool { + if a.Source == b.Source { + return strings.Join(a.Context, "") < strings.Join(b.Context, "") + } + return a.Source < b.Source + }) + + file, err := os.Create(filePath) + if err != nil { + logFatal(err) + } + defer file.Close() + + enc := json.NewEncoder(file) + enc.SetIndent("", "\t") + if err := enc.Encode(trf); err != nil { + logFatal(err) + } +} + +func writeUpdatedTranslation(filePath string, sourceKey2Message, oldSourceKey2Message map[string]*lang.Message, loc string) { + var trf lang.Translation + for _, msg := range sourceKey2Message { + msgCopy := *msg + + if oldMsg, ok := oldSourceKey2Message[sourceKey(msg.Source, msg.Context)]; ok { + msgCopy.Translation = oldMsg.Translation + } + + trf.Messages = append(trf.Messages, &msgCopy) + } + + writeTranslation(filePath, trf) +} + +func writeDefaultTranslation(filePath string, sourceKey2Message map[string]*lang.Message, loc string) { + var trf lang.Translation + for _, msg := range sourceKey2Message { + msgCopy := *msg + msgCopy.Translation = msg.Source + trf.Messages = append(trf.Messages, &msgCopy) + } + + writeTranslation(filePath, trf) +} + +func main() { + flag.Parse() + + if *srcPath == "" || *locales == "" { + flag.Usage() + os.Exit(1) + } + + v := visitor{ + fileSet: token.NewFileSet(), + sourceKey2Message: make(map[string]*lang.Message), + } + + v.scanDir(*srcPath) + + locs := strings.Split(*locales, ",") + for _, loc := range locs { + loc = strings.TrimSpace(loc) + + filePath := filepath.Join(*outPath, fmt.Sprintf("%s.json", loc)) + + if loc == *defLocale { + writeDefaultTranslation(filePath, v.sourceKey2Message, loc) + } else { + writeUpdatedTranslation(filePath, v.sourceKey2Message, readTranslation(filePath), loc) + } + } +} diff --git a/cmd/minibox/main_nogui.go b/cmd/minibox/main_nogui.go new file mode 100644 index 0000000..5dd0053 --- /dev/null +++ b/cmd/minibox/main_nogui.go @@ -0,0 +1,29 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +//go:build !windows + +package main + +import ( + "flag" +) + +func main() { + flag.Parse() + cliMain() +} diff --git a/cmd/minibox/main_wingui.go b/cmd/minibox/main_wingui.go new file mode 100644 index 0000000..72da0f4 --- /dev/null +++ b/cmd/minibox/main_wingui.go @@ -0,0 +1,697 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +//go:build windows + +package main + +import ( + "bytes" + "context" + _ "embed" + "errors" + "flag" + "fmt" + "image/png" + "io/fs" + "os" + "path/filepath" + "strings" + "time" + + "github.com/lxn/walk" + . "github.com/lxn/walk/declarative" + "github.com/pangbox/server/cmd/minibox/lang/dict" + "github.com/pangbox/server/minibox" + "github.com/pangbox/server/res" + log "github.com/sirupsen/logrus" + "github.com/xo/dburl" + "golang.org/x/sys/windows" +) + +var logs bytes.Buffer +var mw *walk.MainWindow +var ni *walk.NotifyIcon +var lang *dict.Dict + +//go:embed status-healthy.png +var statusHealthyPNG []byte + +//go:embed status-unhealthy.png +var statusUnhealthyPNG []byte + +//go:embed status-offline.png +var statusOfflinePNG []byte + +func mustDecodeToBitmap(data []byte) *walk.Bitmap { + img, err := png.Decode(bytes.NewReader(data)) + if err != nil { + fatalErrorf("error decoding PNG icon resource: %v", err) + } + bmp, err := walk.NewBitmapFromImageForDPI(img, 96*2) + if err != nil { + fatalErrorf("error creating bitmap from PNG icon: %v", err) + } + return bmp +} + +var ( + statusHealthy = mustDecodeToBitmap(statusHealthyPNG) + statusUnhealthy = mustDecodeToBitmap(statusUnhealthyPNG) + statusOffline = mustDecodeToBitmap(statusOfflinePNG) + + pangboxIcon = mustDecodeToBitmap(res.PangboxPNG) +) + +var pangyaRegions = []string{ + "", + "us", + "jp", + "th", + "eu", + "id", + "kr", +} + +func tr(source string) string { + if translation := walk.TranslationFunc(); translation != nil { + return translation(source) + } + return source +} + +func regionIndex(opt string) int { + for i, n := range pangyaRegions { + if n == opt { + return i + } + } + return 0 +} + +type statusController struct { + image *walk.ImageView + button *walk.PushButton +} + +func (s *statusController) SetRunning(running bool) { + if s.image == nil || s.button == nil { + // UI has not loaded yet. + return + } + if running { + s.button.SetText(tr("Stop")) + s.image.SetImage(statusHealthy) + } else { + s.button.SetText(tr("Start")) + s.image.SetImage(statusOffline) + } +} + +type ServiceController interface { + Running() bool + Start() error + Stop() error +} + +func pollingStatusController(ctx context.Context, server ServiceController) func(status *statusController) { + return func(status *statusController) { + go func() { + t := time.NewTicker(time.Second / 5) + for { + select { + case <-t.C: + status.SetRunning(server.Running()) + case <-ctx.Done(): + return + } + } + }() + } +} + +func toggleServer(server ServiceController) func() { + return func() { + if server.Running() { + server.Stop() + } else { + server.Start() + } + } +} + +func serverStatus(name string, cb func(s *statusController), onClick func()) Widget { + statusController := &statusController{} + + cb(statusController) + + return Composite{ + Layout: HBox{MarginsZero: true}, + Children: []Widget{ + ImageView{ + Image: statusOffline, + AssignTo: &statusController.image, + }, + TextLabel{ + Text: name, + }, + HSpacer{}, + PushButton{ + Text: tr("Stop"), + AssignTo: &statusController.button, + OnClicked: onClick, + }, + }, + } +} + +func patchStatus(ctx context.Context, patcher *minibox.RugburnPatcher) Widget { + var unpatchButton *walk.PushButton + var patchButton *walk.PushButton + var label *walk.TextLabel + go func() { + t := time.NewTicker(time.Second) + for { + select { + case <-t.C: + // this may happen while still loading. + if unpatchButton == nil || patchButton == nil || label == nil { + continue + } + haveOrig := patcher.HaveOriginal() + ver, err := patcher.RugburnVersion() + + patchButton.SetEnabled(haveOrig) + unpatchButton.SetEnabled(haveOrig) + + if err != nil { + label.SetText(tr("Status: Error (Set pangya dir?)")) + } else if ver == "unpatched" { + label.SetText(tr("Status: Unpatched")) + } else if ver == "unknown" && !haveOrig { + label.SetText(tr("Status: Unknown ijl15 patch")) + } else if ver == "unknown" && haveOrig { + label.SetText(tr("Status: Unknown Rugburn version")) + } else { + label.SetText(fmt.Sprintf(tr("Status: Patched (ver: %s)"), strings.Trim(ver, "\000"))) + patchButton.SetText(tr("Re-&patch")) + } + case <-ctx.Done(): + return + } + } + }() + return Composite{ + Layout: HBox{MarginsZero: true}, + Children: []Widget{ + TextLabel{ + MinSize: Size{Width: 250}, + Text: tr("Checking..."), + AssignTo: &label, + }, + HSpacer{}, + PushButton{ + Text: tr("&Unpatch"), + Enabled: false, + AssignTo: &unpatchButton, + OnClicked: func() { + if err := patcher.Unpatch(); err != nil { + walk.MsgBox(mainForm(), tr("Error"), fmt.Sprintf(tr("Unpatching failed: %v"), err), walk.MsgBoxIconError) + } + }, + }, + PushButton{ + Text: tr("&Patch"), + Enabled: false, + AssignTo: &patchButton, + OnClicked: func() { + if err := patcher.Patch(); err != nil { + walk.MsgBox(mainForm(), tr("Error"), fmt.Sprintf(tr("Patching failed: %v"), err), walk.MsgBoxIconError) + } + }, + }, + }, + } +} + +func textOption(name string, option *string) Widget { + var te *walk.LineEdit + return Composite{ + Layout: HBox{MarginsZero: true}, + Children: []Widget{ + TextLabel{ + Text: name, + MinSize: Size{Width: 100}, + }, + LineEdit{ + AssignTo: &te, + Text: *option, + OnTextChanged: func() { + *option = te.Text() + }, + }, + }, + } +} + +func runMainWindow(ctx context.Context, minibox *minibox.Server) { + var dbTE, pyTE *walk.LineEdit + var curLang = language + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + pakFiles, err := fs.Glob(os.DirFS("."), "*.pak") + if err != nil { + log.Printf("Unexpected error globbing pak files: %v", err) + } + + mainGroup := GroupBox{ + Title: tr("Main Configuration"), + Layout: VBox{}, + Children: []Widget{}, + } + + dbGroup := GroupBox{ + Title: tr("Database Configuration"), + Layout: VBox{}, + Children: []Widget{}, + } + + netGroup := GroupBox{ + Title: tr("Listen Addresses"), + Layout: VBox{SpacingZero: true}, + Children: []Widget{ + textOption(tr("Web Server"), &opts.WebAddr), + textOption(tr("Admin Server"), &opts.AdminAddr), + textOption(tr("Login Server"), &opts.LoginAddr), + textOption(tr("Game Server"), &opts.GameAddr), + textOption(tr("Message Server"), &opts.MessageAddr), + textOption(tr("QA Auth Server"), &opts.QAAuthAddr), + VSpacer{}, + }, + } + + nameGroup := GroupBox{ + Title: tr("Server Names"), + Layout: VBox{SpacingZero: true}, + Children: []Widget{ + textOption(tr("Server Name"), &opts.GameServerName), + textOption(tr("Channel Name"), &opts.GameChannelName), + VSpacer{}, + }, + } + + dbGroup.Children = append(dbGroup.Children, TextLabel{ + Text: tr("The database will be created automatically if it does not exist."), + MinSize: Size{Width: 1, Height: 1}, + }) + + // Database URI control + dbGroup.Children = append(dbGroup.Children, Composite{ + Layout: HBox{}, + Children: []Widget{ + TextLabel{ + Text: tr("Database URI"), + }, + LineEdit{ + AssignTo: &dbTE, + Text: dbOpts.DatabaseURI, + OnTextChanged: func() { + dbOpts.DatabaseURI = dbTE.Text() + }, + }, + PushButton{ + Text: tr("Browse"), + OnClicked: func() { + dlg := new(walk.FileDialog) + dlg.Filter = tr("SQLite3 Database (*.sqlite3)|*.sqlite3") + dlg.Title = tr("Set SQLite3 Database") + + // Try to preload file dialog with current DSN. + if currentUrl, err := dburl.Parse(dbOpts.DatabaseURI); err == nil { + if absPath, err := filepath.Abs(currentUrl.DSN); err == nil { + dlg.FilePath = absPath + } + } + + if ok, err := dlg.ShowOpen(mainForm()); err != nil { + log.Printf("Unexpected error in folder dialog: %v", err) + } else if ok { + url := "sqlite://" + dlg.FilePath + dbTE.SetText(url) + dbOpts.DatabaseURI = url + } + }, + }, + }, + }) + + dbGroup.Children = append(dbGroup.Children, VSpacer{}) + + // Pak file hint + if len(pakFiles) == 0 { + mainGroup.Children = append(mainGroup.Children, TextLabel{ + Text: tr("Note: Consider moving Minibox to your PangYa install directory and running from there."), + TextColor: walk.RGB(255, 0, 0), + Font: Font{Bold: true}, + MinSize: Size{Width: 1, Height: 1}, + }) + } + + var displayLanguageOptions = []string{ + tr("English"), + tr("Japanese"), + } + + var displayLanguages = []string{ + "en", + "ja", + } + + languageIndex := 0 + for i, n := range displayLanguages { + if n == language { + languageIndex = i + } + } + + var languageCB *walk.ComboBox + mainGroup.Children = append(mainGroup.Children, Composite{ + Layout: HBox{MarginsZero: true}, + Children: []Widget{ + TextLabel{ + Text: tr("Language"), + MinSize: Size{Width: 100}, + }, + ComboBox{ + Model: displayLanguageOptions, + CurrentIndex: languageIndex, + OnCurrentIndexChanged: func() { + language = displayLanguages[languageCB.CurrentIndex()] + }, + AssignTo: &languageCB, + }, + HSpacer{}, + }, + }) + + mainGroup.Children = append(mainGroup.Children, Composite{ + Layout: HBox{MarginsZero: true}, + Children: []Widget{ + TextLabel{ + Text: tr("PangYa Path"), + MinSize: Size{Width: 100}, + }, + LineEdit{ + AssignTo: &pyTE, + Text: opts.PangyaDir, + OnTextChanged: func() { + opts.PangyaDir = pyTE.Text() + }, + }, + PushButton{ + Text: tr("Browse"), + OnClicked: func() { + dlg := new(walk.FileDialog) + dlg.Title = tr("Set PangYa Path") + if ok, err := dlg.ShowBrowseFolder(mainForm()); err != nil { + log.Printf("Unexpected error in folder dialog: %v", err) + } else if ok { + pyTE.SetText(dlg.FilePath) + opts.PangyaDir = dlg.FilePath + } + }, + }, + }, + }) + + var pangyaRegionOptions = []string{ + tr("Auto-detect (slower)"), + "US", + "JP", + "TH", + "EU", + "ID", + "KR", + } + + var regionCB *walk.ComboBox + mainGroup.Children = append(mainGroup.Children, Composite{ + Layout: HBox{MarginsZero: true}, + Children: []Widget{ + TextLabel{ + Text: tr("PangYa Region"), + MinSize: Size{Width: 100}, + }, + ComboBox{ + Model: pangyaRegionOptions, + CurrentIndex: regionIndex(opts.PangyaRegion), + OnCurrentIndexChanged: func() { + opts.PangyaRegion = pangyaRegions[regionCB.CurrentIndex()] + }, + AssignTo: ®ionCB, + }, + HSpacer{}, + }, + }) + + mainGroup.Children = append(mainGroup.Children, VSpacer{}) + + statusGroup := GroupBox{ + Title: "Server Status", + Layout: VBox{}, + Children: []Widget{ + serverStatus(tr("Topology Server"), pollingStatusController(ctx, minibox.Topology), toggleServer(minibox.Topology)), + serverStatus(tr("Web Server"), pollingStatusController(ctx, minibox.Web), toggleServer(minibox.Web)), + serverStatus(tr("Admin Server"), pollingStatusController(ctx, minibox.Admin), toggleServer(minibox.Admin)), + serverStatus(tr("QA Auth Server"), pollingStatusController(ctx, minibox.QAAuth), toggleServer(minibox.QAAuth)), + serverStatus(tr("Login Server"), pollingStatusController(ctx, minibox.Login), toggleServer(minibox.Login)), + serverStatus(tr("Game Server"), pollingStatusController(ctx, minibox.Game), toggleServer(minibox.Game)), + serverStatus(tr("Message Server"), pollingStatusController(ctx, minibox.Message), toggleServer(minibox.Message)), + VSpacer{}, + }, + } + + var logTE *walk.TextEdit + logView := TextEdit{ + ReadOnly: true, + Text: string(logs.Bytes()), + AssignTo: &logTE, + } + + t := time.NewTicker(time.Second / 5) + defer t.Stop() + go func() { + for range t.C { + if logTE != nil { + logTE.SetText(string(logs.Bytes())) + } + } + }() + + rugburnGroup := GroupBox{ + Title: tr("Rugburn"), + Layout: VBox{}, + Children: []Widget{ + TextLabel{ + MinSize: Size{Width: 200}, + Text: tr("Patch your PangYa installation with Rugburn to bypass GameGuard and redirect network requests. Visit rugburn.gg for more information."), + }, + LinkLabel{ + Alignment: AlignHNearVCenter, + MaxSize: Size{Width: 200}, + Text: `https://rugburn.gg`, + OnLinkActivated: func(link *walk.LinkLabelLink) { + windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(link.URL()), nil, nil, windows.SW_SHOWNORMAL) + }, + }, + patchStatus(ctx, minibox.Rugburn), + }, + } + + w := MainWindow{ + AssignTo: &mw, + Title: tr("Minibox - All-in-one PangYa Server"), + Size: Size{Width: 500, Height: 500}, + Layout: VBox{}, + Children: []Widget{ + TabWidget{ + Pages: []TabPage{ + {Title: tr("Main"), Layout: VBox{}, Children: []Widget{mainGroup, statusGroup, rugburnGroup}}, + {Title: tr("Database"), Layout: VBox{}, Children: []Widget{dbGroup}}, + {Title: tr("Network"), Layout: VBox{}, Children: []Widget{netGroup, nameGroup}}, + {Title: tr("Logs"), Layout: VBox{}, Children: []Widget{logView}}, + }, + }, + Composite{ + Layout: HBox{}, + Children: []Widget{ + HSpacer{}, + PushButton{ + Text: tr("&Quit"), + OnClicked: func() { + // TODO: + // - Should shut down gracefully + // - If games are active, should prompt + os.Exit(0) + }, + }, + PushButton{ + Text: tr("&Save"), + OnClicked: func() { + minibox.ConfigureDatabase(dbOpts) + minibox.ConfigureServices(opts) + if err := saveConfiguration("minibox.json"); err != nil { + msg := fmt.Sprintf(tr("Error saving to disk: %s; settings may not be retained next time you start Minibox."), err) + walk.MsgBox(mainForm(), tr("Warning"), msg, walk.MsgBoxIconWarning) + } + // Restart window if language changed. + if language != curLang { + mw.Close() + mw.Dispose() + updateLang() + return + } + }, + }, + }, + }, + }, + } + + w.Run() +} + +func mainForm() walk.Form { + if mw != nil { + return mw + } + return nil +} + +func fatalError(args ...any) { + msg := fmt.Sprint(args...) + walk.MsgBox(mainForm(), tr("Fatal Error"), msg, walk.MsgBoxIconError) + log.Fatal(msg) +} + +func fatalErrorf(msg string, args ...any) { + msg = fmt.Sprintf(msg, args...) + walk.MsgBox(mainForm(), tr("Fatal Error"), msg, walk.MsgBoxIconError) + log.Fatal(msg) +} + +func updateLang() { + var err error + lang, err = dict.NewDict(language) + if err != nil { + // Disable translations. + lang = nil + walk.SetTranslationFunc(nil) + return + } + walk.SetTranslationFunc(lang.Translation) + if err := ni.SetToolTip(tr("Minibox PangYa Server")); err != nil { + fatalError(err) + } +} + +func main() { + noGui := flag.Bool("nogui", false, "Disables the GUI.") + flag.Parse() + + if *noGui { + cliMain() + return + } + + log.SetOutput(&logs) + + ctx := context.Background() + log := log.WithContext(ctx) + + minibox := minibox.NewServer(ctx, log) + dummy, err := walk.NewMainWindow() + if err != nil { + fatalError(err) + } + + if err := loadConfiguration("minibox.json"); err != nil && !errors.Is(err, os.ErrNotExist) { + msg := fmt.Sprintf(tr("Error loading settings from disk: %s"), err) + walk.MsgBox(mainForm(), tr("Warning"), msg, walk.MsgBoxIconWarning) + } + + ni, err = walk.NewNotifyIcon(dummy) + if err != nil { + fatalError(err) + } + defer ni.Dispose() + + updateLang() + + // Configure concurrently to reduce startup delay. + go func() { + minibox.ConfigureDatabase(dbOpts) + if err := minibox.ConfigureServices(opts); err != nil { + msg := fmt.Sprintf(tr("Error configuring services: %s - Either move Minibox to your PangYa directory, OR set the PangYa directory to the location of your client."), err) + walk.MsgBox(mainForm(), tr("Warning"), msg, walk.MsgBoxIconWarning) + } + }() + + if err := ni.SetIcon(pangboxIcon); err != nil { + fatalError(err) + } + + ni.MouseDown().Attach(func(x, y int, button walk.MouseButton) { + if button != walk.LeftButton { + return + } + if mw != nil && !mw.IsDisposed() { + mw.Close() + mw.Dispose() + } else { + runMainWindow(ctx, minibox) + } + }) + + exitAction := walk.NewAction() + if err := exitAction.SetText(tr("E&xit")); err != nil { + fatalError(err) + } + + exitAction.Triggered().Attach(func() { + // Nuclear option, to exit fast. + // Should fix this later to shut down gracefully. + ni.SetVisible(false) + os.Exit(0) + }) + + if err := ni.ContextMenu().Actions().Add(exitAction); err != nil { + fatalError(err) + } + + if err := ni.SetVisible(true); err != nil { + fatalError(err) + } + + if err := ni.ShowInfo(tr("Minibox PangYa Server"), tr("Click the tray icon to configure. Right click the tray icon to exit.")); err != nil { + fatalError(err) + } + + dummy.Run() +} diff --git a/cmd/minibox/minibox.manifest b/cmd/minibox/minibox.manifest new file mode 100644 index 0000000..72c626b --- /dev/null +++ b/cmd/minibox/minibox.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + PerMonitorV2, PerMonitor + True + + + diff --git a/cmd/minibox/resource_windows_386.syso b/cmd/minibox/resource_windows_386.syso new file mode 100755 index 0000000000000000000000000000000000000000..9d327f50439f53023cc585b82295c00c8f24e7c6 GIT binary patch literal 105084 zcmeHw3w%`7wfCXgTKnt0dSBn|eUi&<@2|bBfY$c2{cN$1+g>Cy;se^MkW4~=0C|`M z1QnTN0trb-f>jVje4rvI2*^tm2oFI(1rY>MG2jCfRBY9v0{dJ4eP&Je?3{gOCUYjq z;0&Cs{aAZF_t|HkefA@}M-L0X;dk}0?9kagdY6@#<;zt2&(2vrtV=2QO~S$NT)Hlk z{}JQg+16PU>}y%UzeZ9ID|kNQ1iwWvmmmS<_ce)MO893}YAwO961PReYL|YWA$_qSy~U8e(~#ckkp2~t;#=+XwB8-**XuT^3zE)m%G?v~X{#G9|}l^4R0JGmMln>?kCvM*OkxWM!T=VCj zlo0fmFSltSJa_IXfwBmr49W%r1AaQU?iR89ff)&1bL(yv69ZTL-(c{vvqOP_?>9G{ z5Ywj%(cJI>)l1(-{Z>I5@))No$9HtWph5DEtIF|@s~vZb%&WWcq0^z$!Rk^;0rW8#S)dBx4oUTLY3moD-7x$i;k9a7@N3Dqa4>)>R z_Y%>$XSG;XXuW0C9JH(*f2KByvhj@ROP5WINJK^t-}zPm9}_tkqNnK15YX%nh5PtLpBbNZl(`iQHm=^uOd zZLw$DMseiL0|IR&!rWe(Y?z4r3;sA+%&8tLrkCf5=JEiA$H}(PL>uXe(EsDu0)aMm z(X45*Ek+n=9i@-<_Pw_cM#`h!!XB^_E`*VW@Xk$Zgwu1@P5ze09}?4whmel5qxh)u ze(>I5apx^X@;NuXB2PTuvMM4^Job6}jaQ|er;hr4yXPCq@87ju%&sbzG+%mly?l$yQ zan`P}!=R_sJ*i=k1m)U&wsm;F-L7^%wH_ zi}>xg3mFD2reB@o|M`;--z|`aXYkWc+xU29FI{?4#=+x}M@~szD^{Eo>(<#gNC(cs zh4wRk#tb2GBz>X@+LMR*|8Y)e?5S5?J|;G8_&DaZZR^Kk%QI%Y=eK_>_U}C=?y0Ug z1=?t%D+9jm&1Jdb-pS)Uxj-wF>)Q(2_|bFs)~djj^qfB}pY?c_A1HQ^*@Jr-%HoQV7~v` zdcG+h%pI6TWe!ju=;6jegOs;3=Mpv74)`1A9CNCvObfkn2cx@eW+H|hln>r&UoE$u zWwnU2tb?M)`a--R`(asB?lQXK>4ygM$WPXhZ&R z4=khiB&X+z+U|Q1#(Nyz$<%v=!tbcheJ}X9(ZG8!@q8sKX6ou9X+=oi&d>AmwrMxtiBe{%D)lEt)I!ge}mQ23_<8XWd%j zss1mle^R{t#{MXpukYR=-#JuW5XQR)@(|v&rA2I7w!rcIfXl_B|6R8hiz&md5=&+` zi?{S0|{m#7={2S@yP_-}i9 znSdR^7voun$;UcRHI8w?X(dDH9qDG#^7w=DJ!sLa2FEh-@W*?ao`v7EIZR>`C&^p{;a>MC&Fqv%j1>~9HznY0qXqeUFp5I z4(t-BwjKYK6I|H$#p$M|o&ZCo;S5qjeOr19g2nLpl#;g3$e za{T{~`PU8~bRO~kJd}5htbe*4n;Soovbnv}GA+E@LJySD=}d3_oOS#xcwK+_X);nS z*Y_mKHBLR$uH3ZH^Wk#-JjORc;y(;@P)6rJt$G*pU#^$04qWk5Dt8*=>3X@E(mIse zjdxkSoF13&ySctSH8<30bWV%Oqboq?bQOJP(90oC;|X^|_WD~EN6JnMZ5N?{|1ZJZ z%Q2)L*Do0GT|{M0pe*Zx_h`^2`^+*CnKgX)S1ErjjV*}!i7Q2YaJ{I?8${)@N1Do* z(nC4fqfxdYxPfS`kk4hJy8-WdrZPy!`!dP7M#@U^(il#I8@^i}-D8bRrz$7=DSF>m zzPOr8!iQ9L26(%TgP1AUIVc;J;f{FfPAqz<$QSerlAI~K&O6?t<-Fr(;cD%9;+)og zC(k)P>GXLgCY|mH-uPaGik(YWcOR!95k32%4G0B%H()HHJ6&|YlvWST{;^3>+(U%3 z9R1w8CY6YJO_km1f=t!<1JTwkbGqNDeOrNX+yi@d#@(k)=6T3k)w#Y) zC{66x*!Io8BlJ6?%#IDK!_S+xXC1zALw>xcbcPJc@PLevm7aZN>;ueCKw2_%JF_fg zh0GcG6Pt_Vm5(y^?;5+a|59at%;Q?~=>0S{az|8Kj5ZHx$O8_`(NUNR`7vhzW4EBq z7sl-7-!?fCM>=p?UwJ7h9a16x!kP6_PJDlV_Q}QkoRn#p=YcfL#{h5B*jK#%NQM0O zgycJ*IJEy|Xs1%V`~futB2FU-%y5?R%elCMn*kyr^sV zPJ#JQ_(pT+)%e<`j>x}hd3cIX~^#u4tikxk#S-{ ze7}GUi|(wCDzk8=91pYSU{M=jqLYmLOmqK>+i1?3oLeGg!JK8ziv{rw1TygVI>yJh zI;10?)0|*3@?$RFL(?XO$GvDgvUrHV{4#aEv=dJ?4c~Mj!>ozLk#9=)wuEnxNZ-A6 zgXi*N{+=%P`X%!s{8jm4h4KAvcBov+iu(!u49qEn?0B}RbK0F~b;SM~mOdis!`>a0 zTb1RVgRjXsijajb%)Q6=Ru#`F+!6UP$MeN!)^^C=PBL&BzHRK;wjrT!937FL`E>{A zCjX(<_r<+)PRj9dd>6u;NPMfpdIF4@&zxzmD?l3NKO!Az$m6hzgKuNJ7QsYEEo%xx zrZ-78*w0P=jWlNxG;l%wojX5^e2at}NaIWWZpZkLk@Eo=$BhopDFr_B00d1{XUG;y z{`EA!Rh1v*RoTD`GVukTIu7!f2Fo7{2l((O^kmtQ27X9K7_ucQKi40~2|8-s*tX5~ zg^mLmATO?XaUe6wfd_bTomA~(lAq?e-GVxWHb$*K;E(bu4fZe6V3T-ofXnp<_$cQ@ zhwTm=4y(K&8)Ss6khyLA$~OdQd!OGTaR1H;-@oAF#ih!SC_OFWPog#%S7^jqR(Hg%+0e=(Tr zmuq#{{(v+F?U#N*-RkC7WsZMX0zQMdKY35xmnwvgzr8#Z#?uei0d)WvldTwd*J9OC3pyeiiQ+>G2 zvGI4A=#u=EY?Fq{XIyF4O|bi;gnuMBYS1@LG)?K4CqZT313%DmlfUk!g%Mj}ZGw^J zkSqTW(fb%QF}Hqtv)vDBPy4i}9zNXmWx!u(q8BTj=H!i$yhe;5`J>lLFi+I2>Vlyx z|DUD|z4Gte=#mdAKoi&08}D_r3+6B&pT5CwPv-kO=0)&*P=Bkv=7HsASX*%i%TBQE zn~_O}@H~+gsRPCXZe1OvLu2h5B9t%3X+I4O8Sp(5op{r!K75~e{RK1|Yg?muh1Y!8 z^3(he(>e>C*W~N3`X{2AK=WRvlwcfOem|zMJ6~`r`p7lsv)(v9aHlEq3l%4rYKM*hQ3?kkm zyfMj_0J7d~u-{48qtzlvGwj;nq|=BWw+NEmj$33!f+u>=ysw^?b@D7BTKZYmX~0RA zCC(9|mFz5~ z4gue0^l8;Kgia<34(tD~r0-7XjQL#HhXwDa>3l(lrW@qkGwRF zOy$!({q;RNr2c5zbp5%lJCo|Y?l0&DAJg?wJN@nSGuHQPi?IIGQ;Pm^I^&(GkoAn# zp2qkx{+IX0nd*1ei~XGOf3yYIE^MbW6|$bu+Gp;6wo6LS+P){|AjrxFkevZ zXQ5w$G~_+f9FlusrD|^z*0VGHkA1zEFGH;Pd1l39wC@4!7i6#hiNe7;rBwKW`FEE7 zk##lne8pP-yMv_>eK7|Y7i7ZvEaU-)WlY7oA*S6~`s3XhJTWJf_sqe!cIeBpAdLM< z;cwtz?*`;yUbxqFfO&S7{#^I52cD|S{OQ#Z{TI+YJ2mf)8%M~zRNModtv~jy1y9T) zRCUC?&$6IxRP(Sd0C}*Hn(jrId3KinSInI$b*UXWI7%1H@y5CnT)eh`^#d;C#C|B8 z<`wL0{V_lFu{)-UXK4@Rcy)Mo^-@{p?bcW0E)y?puk^=0m6+Fy=NHD8;^p5FJj@}+ zT;$o6H^_NB*k5PKJ=3GsM8r$iEB&!g4%5Q;Vyf(b`h&e`n3oE%u7KCzbhiG_ty?Mg z>rv(L+zyzJO+IHHXhU%?@f>ZW^EGO)+p6V;_s}!$nIv#w> zG%*)l9h<{GQ`kES`B>kemo>$io|o-E?0|I+*vFRX1HumbUa<4DZV*`;uz9 zTbO0@Qh)dc>$I?kGSde{yTyCFxyfY8i+r=7^?FWw9pQZee6Uu-L_<$!`Ml5{Ywpp1 zQJ)2%kLMHGg;?uFblLRyz8za5_C%YS(S1R< zY_k4Gf9(F62`ST?^=F8+WmrRff9(W$76|HJN1m@<>CbYd8_ei$&o_)!|99^?F7CeD zUK4_Oo1cFAY1AB4o=2N3Och<0Yxe9@V$Vy*Bkgg#{#SKp{UIw~Oy88J>!rq-2164K z=D~P64bJECrZ{ZC_i4QPtF+wct1_v0sZMv}rOS?Zs`O_$bRTtw{>$Q$tj}wJhBZ;$~pd;qD>iP$BFZ(*p0XxbzKt14lAyHo42_HIOUwYP^ z_H=#Mw5KcOO$*l#t|w<44AR*ly6-to^MvJI#AmV%uqKQ3)uFzxb#*~bVr{arb~SvT z8P8PGd7I+Aj*opZJ=U65>KCYOrLxv6T$cy@%*0E!0lB_UnOBI;SyE}m3O7v+KVPVg zv1uf}53?>C(HX_^0D?y>IXIv90awa@V!HR4FWGy6f-jot4#&-}{?@0|_TppgQ_gfd z3d8%?e!?<=2kRFLVtpf*vtjnUSaRTeO|{I_@ejt$Pp%J)_`dzwTCsSW@^Rh~%LE>= z^yd7U(wZp!>Ac=wnRuw_QTJTlU^*}Ccubx+Qw(RTNjd2`>V@xFrf?lNCTRx-(iyME zV#?E4dw^uIzYn1t=C?DwSP*AzQkm0|AC;{ssgB97cI<4`)}vzaNHm|H)TbCQMl+{^j{*4@0s$g{ELRpMpOV|+PyDJ; zo~dnX_`^2HzQ;*Nx3sBw{=Xt#`)qz_&;4@kvTkpAmfmo?UrD$h(zyix=0-P}yz2bR zdlHY2AX7E*1aDoQhG2`7H6l0d-O!PJ3?OGPf4~oj&ha){SJF8RFGP4YHyq`&{$S@* zwAY9)8U5lZOMCoZ0bY;^dm%suw!yT}Hfn<+^$;?B7z|wT-|^5j;ZzOH{t3~MdoO@z zRp2T*A7K@(o!><3{jUWNdC_y@W)lss({bixgm3=u<+%^_ljvJH?a#sK1gRgB*gbEO zvobK?w?wba24S5QUU$%f3R=b6IY02ic@GMIRd1ZH{byRY|36+zL%e=O*Lylo^81`r zw0E&&mo~`t-Kon%@%M2yO04o7$;aLc)HisMc~bbkuF#!j2XB($fi`*kJ8%Xo=cfY7 zcLeN35b#a!O5Itva6PDn9cW*KZ*Y1BW-Jho1?>F-+WyM@8feFcR=e;yEjBzW1EnF7 zwnM9J&vO)rZ88v-kn9h5-f;^XK3M0Suwl;=7B+madeWv5fY>yGHh!@B(Iye}am$({ zte!%gpt3bo`+K_=% zR(_9>G-*xV(mA&|u2<_$^P;}YvS2P8uPs#S!Rb&S`kzS4FP(JH$V%sd(%uqSU(J4mPvdzvr00H-Qbs(r;NFby)9l{o`j55i zsxK)VbN(9PtW?Xq`r>mL|EH<{ab~CLOA0UNcHRUDC+hqP^eMSMWng;s0gk+hxw27X z7kf>9=X8CnC*$+<^11FW&HwH8hi1Ps)bKO+eww!ZTn}`AvY$;*$7ebjn4ax`?oX57 zIbFxk_&q)S&VJMV$$mCL9iQoBV0zXA-Jd4EbGnY7@q2puo&Bc!ll^RhIzH3M!1SyK zx<5^R=X4!EiA42 z1Jkn}=>9bMozr#vjNjAK@9a0-pX_H7)bW{42Bv2{(EVxhJE!aT8Na8e-`Q`vKiSVF zsN*x83{1~@p!?J0cTU&w)6wrZuVM?GON6t0Fdr5W@p#T1!nrg!<3KN)$X@*2$!5Ag z+0Q1Z<98=rV#$JYByeU-h|bl}^*}sCbJ!o6Hc1@V`(nHA+Bmlnd3^SaULG_T&a4-x zTe0NmPWieXCcksKj^CYhag(KW|8CJ-9&q%#uG5r)tLfabr4c{kexF=$Ra|9pHU-wa zxXIF;@^szV&nBqjcPCv;vS6+&&Zg02cZ;Jf*!J`?Sq~6)!`1U}E=sD;hBN7!Xu2-D z$?u%5<0s0mEAZDXnwyaSSVx>k8t;A2WfL-w-?G9JT3`E><8_BLmE z8BQ(yomg^rlRU=?a9Z*8?Qj@ZhtrCOM9MCvvY@5o=;yw6h2HV-)a5q$ozr#vczARr z&N~NRYu{(1_8fDcRk?7c*#lE=YUf8?-???Sh+~ASmc<#y?fQkpBOUsJx_<0u6V&ll zJ9Nh|*1qa8)js<9)+QOdGF_DyVfM2L>iAtLdpx}1GhJr4 z^W`zV)2=NKj}5qu4=uX0z8#-*=mYAuG5MX-b^LgI(3LnipIVoB{%z*_AF`tFrk6qc z?xu%&UMH7D+v8TIJLIvf4B5{n$aoC9gAQ&oC}$rV%Ok=VSh`IF><-S4~zw??$ zSJQjz^?hQ)(nrL+rb;op@&nK{_9H{~vk5XDL$7VA?@xwr zu}|E0yEqf-0sOvbR)cZ=0oxRP$u*DOFZby2+P*qH=E>0HcTQ(KhC01uiOIvl=VOw6bv@#V zWA2LSyHm3E*7>uaO;E>AmcLW^w+`%+dkDv4H(3wv?VkMu^kv)5-;VY@o^vbb-YIr% zUMKeL*ebTKSuQZ{Xqs!Z^1)dtX#;eBn*7e`I=++tyh=koK!0$`u-J3&P*>2m!uK1m z_$N!}nH7&Y-tU-aVA?lr-wV$=&P72zPemVC_b2<=1a*Seg^J|Mot= zrJeurEy^n!G9BHYCcksKj?Z+u0&+cwjJq59*DpQ0-gCRf(s#$kr`q{%PW8CBm-u+Mf=zXq^mx2&u9vc*&gpW*ns=b`N8Q%T*Nyr^ z#{V=wCtkn9e&}m|cY!@_A!$fjlSeHV6W*B+ZO^-Jl0^o20*Uaxyj zahz$ep4Q%UMW0Wv%a}8!%UC;dut3|O3!T?1?bxu|wVX;L-moqo-{&T%6)sIvU0|ekZw+hPDc85pZ4}<~v}$p$Qk?4?8M@TREK{``H9_d^g%1 z$&2?}S$Fq4>yB}t+bV_`{i@s3DULOUOwVhm^E3IK({+5W<$+&y9%`KZ-W{Ir@#>Cw zVX^d5byUOinyXUM?z8MFFNE38CaB{x9j_qP*6KVEPe;G2Ho;oZ`)bBJ>gJ>`#{cD< zbFGh{()1#%^EUaN({+3=WKd~f-jm7$&q%D{NvHc=)khES+_Z+qtj0Qi8-U(;XT%;g zsh;=4a;dgPnEh;mIzH3!65>5)+5LCOxwL1B-*x+7d>49RJYX%o?_rObm$z+7XiAyElcCqS^-1@q$?u%5<2(85;2ZCY#}}OtGiIC?)2EB5>(fs^6}a^D?ix=w zd38I&2TK;85bwNs%&||R`;+}_f;ztLfB1gp%=Ug~xzf#^=_W7RS%-JtdCIX4=>9bM zozr!E-T%1nb>3>6?P-E4t{OJsb2{VdaZYDw%I9HcIto1l)b({pO`RUKG<6I5~4unC{j8CQ>UIzv-Fr<RQ@j;KHE#zTaPndqEPpj?oal!3F`Pdy;$+!C}kHufl*WvHLF#?xUeJl$WqKiSVFsN?JObb0i6Ecs&P z$D$Xjyq>SeyP7`p*X7dVjHkm`c)GuIf13Qx={mknPnSoJvwS9~<8wS-d=nn0Gd&%0 zz7FHX<8nGa$2nhzoX?Q+Ij+NG^L2me{$xL!ppLK8)8*CUEWZir_#BTH--O5MOizcL zufurpxSWp9an9Ev=QHGdj_WYleBEEVKTUq;^r{@+pVcz@z7(;>e{_A)EgsAMU9FFs zeERs$(RU6-&;KCbvY$|1)kXY`4A=e9#V@iO&L)ecAq94R!h81Kjtz@3Ahj-I-_s{}_|sIXyVk_wVHY zlWHA^cTJ;O_cGzmo#&z38+*y9K7>!J16TYT``HZ=?!9VR%vImBZDUm1qVvndyVl>T z{r7C!&~6W%P_A#bo6pre)D!amCwd(~JJ1zt?N!|}VRJo$Z?G@9zPD5;C;Q{-q1peU z`Z!j&a;R^F>VFQSKY(*@@Lr&wla}#~**B@O4SaxpKGwuLolz$JHq;!jMg7l7H$5}*I@Z(E!Qf?Q$MUyRKH2j~#-@%o)%(`n zIsUejZ{ta$v7ikZ|EInGTjKGx6HZmYH_Ofc)de}ObAgi`D`{>z5$nF%PK=Ty0dD>$|g~_NdCq9_prBO-W@+b!)0S;;I|THol{}c~vfypQAzb^hf&-E^B$T$NJYrcHjy&Htw%Uq^NGs$6J@??1>! zr%iT(4$1o1Sib2mj{8rkvcx;)XR?R>Jy4}Rxv(U4y3x9_x|^4}`G2KJ=FUzho2*vt zqK2V^(SO?7xkT|AYuoQ3tMaCB%wHDvhS%lYyd{>R_UB$BSQE)}-)??uXHL zs{f58duQc`0={gMji5uk`X-83f6Eg_`#_#hZuX>PZ4exK`G1*oLwfzBb*a&>`LqZX z825_WOBCRqSLXOOdZl0G z(CjOn{J?3AQ}%hT-_-HWd!!Q2J-FYz(#pyv)eb{Zk-3mCy&0-+cvKGb5Tw0Cev3TJ@>EjN-wYJP+xhlDl3OW!-kN3 zKKHB+G&g=Es`3Uq+6O%8TknfrKOfQNvOZ3*A-KUa{pu&RKU+ca4EWZ1?K3aw!RFLo zUhia|`dgp&%s#ahGmXBtdHa9wrCy*V+W?n-Kxo*tqPgK%@;XvqX>wtalYS?IL;bzI z*2`;p)CPQ?t}paD0G+((2g-ht+jBqHw>4GI$EuMq&wRCB`zlrRDs%mHs$DqT5KQu2iQcs*k1TYwkK}=RmzUiqReFIgy$eideVkz3 zO^&@|xON9=dL6hup6Wc_E ziH-?3*1SUd3A=vh`Aab1yQnjCVjfkw*&|h*^lOm=MH0H#G@dUdYEXFUJtd?O_6W%$dNJY zKtt7bs@qpP>GyUp&;QRR-szGa9M=C|$wr6U*(q?9m=@zX+1&5}_4h_P*(Q=k{fq~? z$`*}-2Aykq-(`KPM~N znRJH@^m`t0yi3ROJ<(g&)%{}h2dNG|-%dBB9jG2YTr}0a9nllt3~S5VpMM*n5%3N3 z+6P@tuQK4ziRh=;25Gfr)1$tl!dBHI3Y_kXNE+TRg1H0F?`plke@dYLKad{#O!^@` zlz$z)7i_2NS$f9{MD)?ThD~6bX(4(Z=3i^HkF*!{^(PPW|6?j- z>?qCfJeXG9p9#ft{Ewo~)KM9_d%5s8qJ;_BL~ARk>>J%--jkE9CtrWnKaqSpJE-3p zjGH*oJ(JRcg9d#~Ut^zuy>_EN*`4iyXHuf`#;FgvH8^U}H@mZ5Udl#mtG+=#-;{vg zG5)<}%FrwS&PzE`MGNl*Re|g!@#u%XV=(`>f14^`OO#!}w1-K7tksD57HV_hEFKC*+UzjwMa;A=?dJ`wfjuO|IIYOf=W?a{lV z7j{f%8sYCLPS1kl-FdD{A~vUasNW&~tNp^q@eE9a#+i(Zww>M$=SS|5fbZqdkOAL2 zlXMR2I1BH<={%;NDxZYs4%K2jH*;^K%E}qK84F}Akg-6<0vQWrEC36LWPh!y9nTRW zsK$E;>er}0aaxU8)})is@qQ)%NOaEvehUQ%(J4-k4Ix_X{1yV|ahwW9 z;%Mn9-b$b+@?jY&h~!9|3L@SQ`LGPdfi?+oXx7tCj>P-f$&^b!8`FxT=8qQ5nI~g60y}5ACgGK!{wdY#a*;ZDT9eaduzH0UD8;*U)qH>pA zX2-t!OY(&k42EN0?_u#|{cFB@G1+ynb+wg4fB(n2ibDOZD=a_7`_tdwP#C{8XMYIG zQ;3&!+pwR^g&b!`a>7a0GLp6j(W|h^tU{~U8bQvnOm;V9A{l<^(Kk=0-sB) zZ&Df9wL;<-6Xh|A21QYJ>rFW0>4KgAi^^qFc!Vqgi}DtSq8kOcYQGo`gII+p^lVjHN80)Z`@se^?ha zd;-B6DKtT9z;fDFLq4Q|&7$Sdtz}ymcv*C3uokc}?%aHWV#4uV@iq(e%5P3 zL3yu!KmW$1f%5W#qP#H^vZ#D<`OkWdEi3+MdH%?PqCoitMTPlgCFLcGt~hz=ASgNpWFCN!j3wevs7` zBb5{;2L1`~?)#(myss%6ThPX*jmM>Z9M!+Hk7881JNoeL@$>N4$>tuo+t9nYFA`cD o3AIE*J0qdiNa%PZWQF&uILA6a66#%6UY2hYrxpSIF5KAvAMa7;-v9sr literal 0 HcmV?d00001 diff --git a/cmd/minibox/resource_windows_amd64.syso b/cmd/minibox/resource_windows_amd64.syso new file mode 100755 index 0000000000000000000000000000000000000000..1ecb01749108b5a276759041a11962710d5e82fd GIT binary patch literal 105084 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 literal 0 HcmV?d00001 diff --git a/cmd/minibox/resource_windows_arm.syso b/cmd/minibox/resource_windows_arm.syso new file mode 100644 index 0000000000000000000000000000000000000000..fe19798933034bf5b83d5c1448141c0089a03531 GIT binary patch literal 105084 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 Hu0mjfM;ol) literal 0 HcmV?d00001 diff --git a/cmd/minibox/status-offline.png b/cmd/minibox/status-offline.png new file mode 100644 index 0000000000000000000000000000000000000000..283ac2292df76ea41fca19dfc1c21d08f6772970 GIT binary patch literal 984 zcmV;}11J26P)EX>4Tx04R}tkv&MmKpe$iTT4YM4t5afkfAzR5Ut{LYkeQevU6Cm&mTxlJDtqIJ0lHTZO zu_It$8@RacX!0I#xdRM6>5?HiQh=tvSOnhB=$rDu;4RR%=JwX!$LRx*rLNL9z`-Ff zTB7WAk9YTW_xA6Zc7H$0PIAh%n=2Xs00Iz6L_t(o!^PLXYZOrw#qrOM+W2E21S=c2 zvrA)yMK%QNQdz34`2*6V6G@RmNMR}))2N^z*hxS{h(2||6iFmlSg4JH2HCJIL6Yd& zyp=IQca53Z0|NsCbG~!u-TTg~=uypjZ2+SjWEy8z-pzqb!% z@1`fXn1*Z(lH7yI63j)9$=@o_tk;I|qKDPKU)sXiG-QjN3XGR%E_#harCF~H;scH< zS1p`OFxCfi(Q_=pi2i9LDX`~z(NKbF9|5Wf2Kopvkm$1#G)nYq=eFx@d`|GbM1V>T zWD~Cwtdv~!O04_=EG2l>hhJZb0Iw3v$FW$pT3ATZkbTA7lFi$=orY|^(}r_#i!5^G z=U0CNH-80^G-Qi0iWLR0jg4sh5mRZ%R(B?%nHZOfYQ3!p@fdgVgGj8x8m0?q%L)$S z2tMIf8nX3W01;v#M)D#c-r!mqvgc(UO!7&1I(}dwN3*|$&sofLV)$MTr^7KoUiydV z){3)bE_T@7Pscfs!I&RMVv4HfvP2_pYAbQH&&L7RRX+iHwx#WQ)|Z$70000IP)EX>4Tx04R}tkv&MmKpe$iTT4YM4t5afkfAzR5Ut{LYkeQevU6Cm&mTxlJDtqIJ0lHTZO zu_It$8@RacX!0I#xdRM6>5?HiQh=tvSOnhB=$rDu;4RR%=JwX!$LRx*rLNL9z`-Ff zTB7WAk9YTW_xA6Zc7H$0PIAh%n=2Xs00FN_L_t(o!^PLXYZO5o1@O-d*l8ibPEdPc zDk~)jHgc7%*7pabxz6H>SjFQ0fPmelwX%zZpdbWrHKQ)~+7VDHr2=DfXm zuq^DrzBfO<-+3=+iK@1Rr+9?>xQ!e5g$cgm3qIjP3ck1bZ)idM0DZi`^@ZWO@oqjp zaDX={_`H&Us&)$l?BJT!aE=l7QZQ*PpsGE_OFXe29AY;GM@t1%wPzS2SuZLKQ}Fhm z0`km99gsIg>@-&*EbRHg8khaD?RY*r6R|Nc*Z{fAF_>F{TxuWx7*GFyTE z2FYcvK7`!izWt@ibd8_58__F3E_3!Gww1$n?6BtBAVu6OL|)@g#LYqi0=A;Uh?o}S zpGKS(6mS~z4OI$N#Bo6Z$K_jb3eGStF#B-|&Sqt}pRdSG;XHHxHY6!H$yRJr7-h~m zN3@siQr9@lT;>F1kG9)!SytJePyP^)MI2>IUUfu1T+9aR3MTcE@9*)dRWMmoG^-b) z+3PDsvnzzt%bLs>`?J%&aynj!=e=y+vj literal 0 HcmV?d00001 diff --git a/cmd/minibox/versioninfo.json b/cmd/minibox/versioninfo.json new file mode 100755 index 0000000..bae4abf --- /dev/null +++ b/cmd/minibox/versioninfo.json @@ -0,0 +1,43 @@ +{ + "FixedFileInfo": { + "FileVersion": { + "Major": 1, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "ProductVersion": { + "Major": 1, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "FileFlagsMask": "3f", + "FileFlags ": "00", + "FileOS": "040004", + "FileType": "01", + "FileSubType": "00" + }, + "StringFileInfo": { + "Comments": "Minibox, All-in-one Pangya Server Emulator", + "CompanyName": "Pangbox", + "FileDescription": "All-in-one PangYa server.", + "FileVersion": "v1.0.0.0", + "InternalName": "minibox.exe", + "LegalCopyright": "Copyright (c) 2018-2023 John Chadwick", + "LegalTrademarks": "PangYa is a registered trademark of Ntreev Soft Co., Ltd. Corporation. Pangbox is not endorsed or related to Ntreev Soft Co., Ltd. Corporation in any way. PangYa and related trademarks are used strictly for purposes of identification.", + "OriginalFilename": "main.go", + "PrivateBuild": "", + "ProductName": "Pangbox", + "ProductVersion": "v1.0.0.0", + "SpecialBuild": "" + }, + "VarFileInfo": { + "Translation": { + "LangID": "0409", + "CharsetID": "04B0" + } + }, + "IconPath": "../../res/pangbox.ico", + "ManifestPath": "" +} \ No newline at end of file diff --git a/cmd/packetparse/main.go b/cmd/packetparse/main.go new file mode 100644 index 0000000..d0c046f --- /dev/null +++ b/cmd/packetparse/main.go @@ -0,0 +1,140 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/go-restruct/restruct" + "github.com/pangbox/server/common" + "github.com/pangbox/server/game" + "github.com/pangbox/server/login" + "github.com/pangbox/server/message" +) + +func GetMessageTable(server string, origin string) (common.AnyMessageTable, error) { + switch server { + case "login": + switch origin { + case "server": + return login.ServerMessageTable.Any(), nil + case "client": + return login.ClientMessageTable.Any(), nil + default: + return nil, fmt.Errorf("unexpected origin %q; valid values are server, client", origin) + } + case "game": + switch origin { + case "server": + return game.ServerMessageTable.Any(), nil + case "client": + return game.ClientMessageTable.Any(), nil + default: + return nil, fmt.Errorf("unexpected origin %q; valid values are server, client", origin) + } + case "message": + switch origin { + case "server": + return message.ServerMessageTable.Any(), nil + case "client": + return message.ClientMessageTable.Any(), nil + default: + return nil, fmt.Errorf("unexpected origin %q; valid values are server, client", origin) + } + default: + return nil, fmt.Errorf("unexpected server %q; valid values are login, game, message", server) + } +} + +func ParseHex(input []byte) []byte { + output := []byte{} + + for i, n := range strings.Split(string(input), ",") { + n = strings.TrimSpace(n) + if n == "" { + continue + } + v, err := strconv.ParseUint(n, 0, 8) + if err != nil { + log.Fatalf("Unexpected sequence %d in hex string: %q", i, n) + } + output = append(output, byte(v)) + } + + return output +} + +func main() { + var err error + hex := flag.Bool("hex", false, "If set, parse hex input.") + flag.Parse() + + args := flag.Args() + + if len(args) < 2 || len(args) > 3 { + fmt.Fprintf(os.Stderr, "Usage: %v login|game|message server|client [FILE...] [-hex]", os.Args[0]) + flag.PrintDefaults() + return + } + + input := os.Stdin + if len(args) == 3 && args[2] != "-" { + input, err = os.Open(args[2]) + if err != nil { + log.Fatalf("Error opening input file: %v", err) + } + } + + messageTable, err := GetMessageTable(args[0], args[1]) + if err != nil { + log.Fatalf("Error getting message table: %v", err) + } + + var packet []byte + + packet, err = io.ReadAll(input) + if err != nil { + log.Fatalf("Error reading input: %v", err) + } + + if *hex { + packet = ParseHex(packet) + } + + msgid := binary.LittleEndian.Uint16(packet[:2]) + + message, err := messageTable.Build(msgid) + if err != nil { + log.Fatalf("Error building packet: %v", err) + } + + err = restruct.Unpack(packet[2:], binary.LittleEndian, message) + if err != nil { + log.Fatalf("Error parsing packet: %v; partial result: %#v; data: % 02x", err, message, packet) + } + + spew.Dump(message) +} diff --git a/cmd/shimclient/main.go b/cmd/shimclient/main.go new file mode 100755 index 0000000..7274645 --- /dev/null +++ b/cmd/shimclient/main.go @@ -0,0 +1,119 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "encoding/binary" + "flag" + "io" + "math/rand" + "net" + + "github.com/davecgh/go-spew/spew" + "github.com/manifoldco/promptui" + "github.com/pangbox/pangcrypt" + "github.com/pangbox/server/common" + log "github.com/sirupsen/logrus" +) + +func readServerPacket(k byte, r io.Reader) ([]byte, error) { + packetHeaderBytes := [3]byte{} + + read, err := r.Read(packetHeaderBytes[:]) + if err != nil { + return nil, err + } else if read != len(packetHeaderBytes) { + return nil, io.EOF + } + + remaining := binary.LittleEndian.Uint16(packetHeaderBytes[1:3]) + packet := make([]byte, len(packetHeaderBytes)+int(remaining)) + copy(packet[:3], packetHeaderBytes[:]) + _, err = io.ReadFull(r, packet[3:]) + if err != nil { + return nil, err + } + + return pangcrypt.ServerDecrypt(packet, k) +} + +func sendClientPacket(k byte, w io.Writer, data []byte) { + salt := rand.Intn(0x100) + outpkt, err := pangcrypt.ClientEncrypt(data, k, byte(salt)) + + if err != nil { + log.Fatal("While encrypting outgoing Login packet:", err) + } + + if n, err := w.Write(outpkt); err != nil { + log.Fatal("While sending client packet:", err) + } else if n < len(outpkt) { + log.Fatalf("Short write on out: %d of %d", n, len(outpkt)) + } +} + +func main() { + loginAddr := flag.String("login_addr", "127.0.0.1:10101", "address of login server") + flag.Parse() + + sock, err := net.Dial("tcp", *loginAddr) + if err != nil { + log.Fatal(err) + } + + // Read credentials. + prompt := promptui.Prompt{Label: "Username"} + username, err := prompt.Run() + if err != nil { + log.Fatal(err) + } + prompt = promptui.Prompt{Label: "Password", Mask: '*'} + password, err := prompt.Run() + if err != nil { + log.Fatal(err) + } + + // Read hello. + hello := [14]byte{} + if c, err := sock.Read(hello[:]); err != nil { + log.Fatal(err) + } else if c < len(hello) { + log.Fatal("short read on hello packet") + } + key := hello[6] + + log.Printf("Connected to %s with key %d.", sock.RemoteAddr(), key) + + sendClientPacket(key, sock, common.NewPacketBuilder(). + PutUint16(0x0001). + PutPString(username). + PutPString(password). + PutBytes([]byte{ + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, + }).MustBuild(), + ) + + for { + packet, err := readServerPacket(key, sock) + if err != nil { + log.Fatal(err) + } + spew.Dump(packet) + } +} diff --git a/cmd/topologyserver/main.go b/cmd/topologyserver/main.go new file mode 100755 index 0000000..7182141 --- /dev/null +++ b/cmd/topologyserver/main.go @@ -0,0 +1,81 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package main + +import ( + "flag" + "net/http" + "os" + + "github.com/pangbox/server/common/topology" + "github.com/pangbox/server/gen/proto/go/topologypb" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + log "github.com/sirupsen/logrus" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "google.golang.org/protobuf/encoding/protojson" +) + +//go:generate go run github.com/josephspurrier/goversioninfo/cmd/goversioninfo -platform-specific=true + +var ( + listenAddr = ":41141" + staticServerList = "" + useH2C = true +) + +func init() { + flag.StringVar(&listenAddr, "addr", listenAddr, "Address to listen on for topology server connections.") + flag.StringVar(&staticServerList, "static_server_list", staticServerList, "Filename of static server list in JSON format.") + flag.BoolVar(&useH2C, "use_h2c", useH2C, "Whether or not to enable H2C support.") + flag.Parse() +} + +func main() { + if staticServerList == "" { + log.Fatal("Currently, topology server requires a static server list.") + } + + jsonData, err := os.ReadFile(staticServerList) + if err != nil { + log.Fatalf("Error reading static server data: %v", err) + } + + config := &topologypb.Configuration{} + if err := protojson.Unmarshal(jsonData, config); err != nil { + log.Fatalf("Error parsing static server data: %v", err) + } + + serverList := []*topologypb.ServerEntry{} + for _, server := range config.Servers { + serverList = append(serverList, &topologypb.ServerEntry{ + Server: server, + }) + } + + storage := topology.NewMemoryStorage(serverList) + + server := topology.NewServer(storage) + + _, handler := topologypbconnect.NewTopologyServiceHandler(server) + httpserver := &http.Server{Handler: handler} + if useH2C { + httpserver.Handler = h2c.NewHandler(httpserver.Handler, &http2.Server{}) + } + log.Fatal(httpserver.ListenAndServe()) +} diff --git a/cmd/topologyserver/resource_windows_386.syso b/cmd/topologyserver/resource_windows_386.syso new file mode 100644 index 0000000000000000000000000000000000000000..5b038e3584669e9993c33be040b1184bdca2ad2f GIT binary patch literal 104138 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 literal 0 HcmV?d00001 diff --git a/cmd/topologyserver/resource_windows_amd64.syso b/cmd/topologyserver/resource_windows_amd64.syso new file mode 100644 index 0000000000000000000000000000000000000000..c555356f12f8f8fb927b924d7b9646e4b68d02f8 GIT binary patch literal 104138 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 q2mYefE=z 0 { + // Block until p is not full. + for { + if p.closed || p.writeClosed { + return 0, io.ErrClosedPipe + } + if !p.full() { + break + } + if p.wtimedout { + return 0, errTimeout + } + + p.wwait.Wait() + } + wasEmpty := p.empty() + + end := cap(p.buf) + if p.w < p.r { + end = p.r + } + x := copy(p.buf[p.w:end], b) + b = b[x:] + n += x + p.w += x + if p.w > len(p.buf) { + p.buf = p.buf[:p.w] + } + if p.w == cap(p.buf) { + p.w = 0 + } + + // Signal a blocked reader, if any. + if wasEmpty { + p.rwait.Signal() + } + } + return n, nil +} + +func (p *pipe) Close() error { + p.mu.Lock() + defer p.mu.Unlock() + p.closed = true + // Signal all blocked readers and writers to return an error. + p.rwait.Broadcast() + p.wwait.Broadcast() + return nil +} + +func (p *pipe) closeWrite() error { + p.mu.Lock() + defer p.mu.Unlock() + p.writeClosed = true + // Signal all blocked readers and writers to return an error. + p.rwait.Broadcast() + p.wwait.Broadcast() + return nil +} + +type conn struct { + io.Reader + io.Writer +} + +func (c *conn) Close() error { + err1 := c.Reader.(*pipe).Close() + err2 := c.Writer.(*pipe).closeWrite() + if err1 != nil { + return err1 + } + return err2 +} + +func (c *conn) SetDeadline(t time.Time) error { + c.SetReadDeadline(t) + c.SetWriteDeadline(t) + return nil +} + +func (c *conn) SetReadDeadline(t time.Time) error { + p := c.Reader.(*pipe) + p.mu.Lock() + defer p.mu.Unlock() + p.rtimer.Stop() + p.rtimedout = false + if !t.IsZero() { + p.rtimer = time.AfterFunc(time.Until(t), func() { + p.mu.Lock() + defer p.mu.Unlock() + p.rtimedout = true + p.rwait.Broadcast() + }) + } + return nil +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + p := c.Writer.(*pipe) + p.mu.Lock() + defer p.mu.Unlock() + p.wtimer.Stop() + p.wtimedout = false + if !t.IsZero() { + p.wtimer = time.AfterFunc(time.Until(t), func() { + p.mu.Lock() + defer p.mu.Unlock() + p.wtimedout = true + p.wwait.Broadcast() + }) + } + return nil +} + +func (*conn) LocalAddr() net.Addr { return addr{} } +func (*conn) RemoteAddr() net.Addr { return addr{} } + +type addr struct{} + +func (addr) Network() string { return "bufconn" } +func (addr) String() string { return "bufconn" } diff --git a/common/error.go b/common/error.go new file mode 100755 index 0000000..9be06da --- /dev/null +++ b/common/error.go @@ -0,0 +1,38 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package common + +import "fmt" + +// UnknownMessageError is returned when there is no known message ID in a given +// context. +type UnknownMessageError struct{ MessageID uint16 } + +// Error implements the error interface. +func (e UnknownMessageError) Error() string { + return fmt.Sprintf("unknown message %04x", e.MessageID) +} + +// UnexpectedMessageError is returned when there an unexpected message is +// received. +type UnexpectedMessageError struct{ MessageID uint16 } + +// Error implements the error interface. +func (e UnexpectedMessageError) Error() string { + return fmt.Sprintf("unexpected message %04x", e.MessageID) +} diff --git a/common/hash/bcrypt.go b/common/hash/bcrypt.go new file mode 100755 index 0000000..56dbd67 --- /dev/null +++ b/common/hash/bcrypt.go @@ -0,0 +1,38 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package hash + +import "golang.org/x/crypto/bcrypt" + +var ( + _ = Hasher(&Bcrypt{}) +) + +// Bcrypt implements a bcrypt-based password hashing scheme. +type Bcrypt struct{} + +// Hash implements Hasher. +func (Bcrypt) Hash(secret string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(secret), 0) + return string(hash), err +} + +// CheckHash implements Hasher. +func (Bcrypt) CheckHash(secret, hash string) bool { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(secret)) == nil +} diff --git a/common/hash/hasher.go b/common/hash/hasher.go new file mode 100755 index 0000000..2ff60e3 --- /dev/null +++ b/common/hash/hasher.go @@ -0,0 +1,27 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package hash + +// Hasher defines an interface for password hashers. +type Hasher interface { + // Hash creates a hash from a given input. + Hash(secret string) (string, error) + + // CheckHash checks if a hash matches a given input. + CheckHash(secret, hash string) bool +} diff --git a/common/hash/null.go b/common/hash/null.go new file mode 100755 index 0000000..48289dd --- /dev/null +++ b/common/hash/null.go @@ -0,0 +1,35 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package hash + +var ( + _ = Hasher(Null{}) +) + +// Null provides a hasher that returns the source input. +type Null struct{} + +// Hash implements Hasher. +func (Null) Hash(secret string) (string, error) { + return secret, nil +} + +// CheckHash implements Hasher. +func (Null) CheckHash(secret, hash string) bool { + return secret == hash +} diff --git a/common/packetbuilder.go b/common/packetbuilder.go new file mode 100644 index 0000000..a162d8d --- /dev/null +++ b/common/packetbuilder.go @@ -0,0 +1,104 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package common + +import ( + "encoding/binary" + "errors" + "fmt" + "strings" +) + +// PacketBuilder is used for ad-hoc building of packets, mainly intended for +// use in the shim client. +type PacketBuilder struct { + buf []byte + err error +} + +func NewPacketBuilder() PacketBuilder { + return PacketBuilder{make([]byte, 0, 256), nil} +} + +func (builder PacketBuilder) PutPString(s string) PacketBuilder { + c := len(s) + if c > 0xFFFF { + builder.err = errors.Join(builder.err, fmt.Errorf("string too big (%d)", c)) + return builder + } + builder.buf = binary.LittleEndian.AppendUint16(builder.buf, uint16(c)) + builder.buf = append(builder.buf, s...) + return builder +} + +func (builder PacketBuilder) PutString(s string, l int) PacketBuilder { + if l > len(s) { + s += strings.Repeat("\000", l-len(s)) + } else if l < len(s) { + s = s[:l] + } + builder.buf = append(builder.buf, s...) + return builder +} + +func (builder PacketBuilder) PutBytes(raw []byte) PacketBuilder { + builder.buf = append(builder.buf, raw...) + return builder +} + +func (builder PacketBuilder) PutUint8(v uint8) PacketBuilder { + builder.buf = append(builder.buf, v) + return builder +} + +func (builder PacketBuilder) PutUint16(v uint16) PacketBuilder { + builder.buf = binary.LittleEndian.AppendUint16(builder.buf, v) + return builder +} + +func (builder PacketBuilder) PutUint32(v uint32) PacketBuilder { + builder.buf = binary.LittleEndian.AppendUint32(builder.buf, v) + return builder +} + +func (builder PacketBuilder) PutInt8(v int8) PacketBuilder { + return builder.PutUint8(uint8(v)) +} + +func (builder PacketBuilder) PutInt16(v int16) PacketBuilder { + return builder.PutUint16(uint16(v)) +} + +func (builder PacketBuilder) PutInt32(v int32) PacketBuilder { + return builder.PutUint32(uint32(v)) +} + +func (builder PacketBuilder) Build() ([]byte, error) { + if builder.err != nil { + return nil, builder.err + } + return builder.buf, nil +} + +func (builder PacketBuilder) MustBuild() []byte { + b, err := builder.Build() + if err != nil { + panic(err) + } + return b +} diff --git a/common/pstring.go b/common/pstring.go new file mode 100755 index 0000000..2b1de43 --- /dev/null +++ b/common/pstring.go @@ -0,0 +1,31 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package common + +// PString represents a Pascal-style length prefix string. PString uses 2 +// bytes for the length. +type PString struct { + Length uint16 `struct:"sizeof=Value"` + Value string +} + +// ToPString is a helper function that constructs a PString from a normal +// string value. +func ToPString(value string) PString { + return PString{uint16(len(value)), value} +} diff --git a/common/pubsub/postgres.go b/common/pubsub/postgres.go new file mode 100755 index 0000000..84c20b9 --- /dev/null +++ b/common/pubsub/postgres.go @@ -0,0 +1,138 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pubsub + +import ( + "encoding/json" + "time" + + "github.com/lib/pq" + log "github.com/sirupsen/logrus" +) + +var ( + _ = PubSub(&PostgresPubSub{}) +) + +// GetPostgresListenerFunc is a type of function that returns a new +// pq.Listener ready to start listening to pubsub channels. You must provide +// such a function to PostgresPubSub. +type GetPostgresListenerFunc func() (*pq.Listener, error) + +// PostgresStream is a handler for a single PostgreSQL subscription. +type PostgresStream struct { + listener *pq.Listener + logger *log.Entry + stream chan Message +} + +func newPostgresStream(listener *pq.Listener, channel string, log *log.Entry) (*PostgresStream, error) { + stream := &PostgresStream{ + listener: listener, + logger: log.WithField("channel", channel), + stream: make(chan Message), + } + + err := stream.listen(channel) + if err != nil { + return nil, err + } + + return stream, nil +} + +// listen begins listening on a given channel. +func (s *PostgresStream) listen(channel string) error { + err := s.listener.Listen(channel) + if err != nil { + return err + } + + go func() { + defer close(s.stream) + + for { + select { + case data, ok := <-s.listener.Notify: + if !ok { + return + } + message := Message{} + err := json.Unmarshal([]byte(data.Extra), &message) + if err != nil { + s.logger.Error(err) + break + } + s.stream <- message + case <-time.After(30 * time.Second): + go s.listener.Ping() + } + + } + }() + + return nil +} + +// Channel returns a channel of streaming messages. +func (s *PostgresStream) Channel() <-chan Message { + return s.stream +} + +// Close ends the pubsub stream. +func (s *PostgresStream) Close() error { + return s.listener.Close() +} + +// PostgresPubSub is an implementation of pubsub using PostgreSQL +// notifications. +type PostgresPubSub struct { + getListener GetPostgresListenerFunc + chanPrefix string + logger *log.Entry +} + +// NewPostgresPubSub creates a new PostgreSQL publish/subscribe engine. +func NewPostgresPubSub(getListener GetPostgresListenerFunc, chanPrefix string, log *log.Entry) (*PostgresPubSub, error) { + return &PostgresPubSub{ + getListener: getListener, + chanPrefix: chanPrefix, + logger: log.WithField("pubsub", "postgres"), + }, nil +} + +// Publish publishes a message on a given channel. +func (p *PostgresPubSub) Publish(channel string, message Message) error { + return nil +} + +// Subscribe listens for messages on a given channel. +func (p *PostgresPubSub) Subscribe(channel string) (Stream, error) { + listener, err := p.getListener() + if err != nil { + return nil, err + } + + stream, err := newPostgresStream(listener, p.chanPrefix+channel, p.logger) + if err != nil { + listener.Close() + return nil, err + } + + return stream, nil +} diff --git a/common/pubsub/pubsub.go b/common/pubsub/pubsub.go new file mode 100755 index 0000000..cb68db4 --- /dev/null +++ b/common/pubsub/pubsub.go @@ -0,0 +1,43 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pubsub + +import ( + "time" + + "github.com/google/uuid" +) + +// Message is a pub/sub message. +type Message struct { + UUID uuid.UUID + Time time.Time + Payload []byte +} + +// Stream is a stream of incoming pub/sub messages. +type Stream interface { + Channel() <-chan Message + Close() error +} + +// PubSub is an interface for publish/subscribe messaging systems. +type PubSub interface { + Publish(channel string, message Message) error + Subscribe(channel string) (Stream, error) +} diff --git a/common/pycrypto/xtea.go b/common/pycrypto/xtea.go new file mode 100644 index 0000000..a6cf8df --- /dev/null +++ b/common/pycrypto/xtea.go @@ -0,0 +1,67 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pycrypto + +import ( + "fmt" + + "github.com/pangbox/pangfiles/crypto/pyxtea" +) + +var Keys = []pyxtea.Key{ + pyxtea.KeyUS, + pyxtea.KeyJP, + pyxtea.KeyTH, + pyxtea.KeyEU, + pyxtea.KeyID, + pyxtea.KeyKR, +} + +var regionToKey = map[string]pyxtea.Key{ + "us": pyxtea.KeyUS, + "jp": pyxtea.KeyJP, + "th": pyxtea.KeyTH, + "eu": pyxtea.KeyEU, + "id": pyxtea.KeyID, + "kr": pyxtea.KeyKR, +} + +var keyToRegion = map[pyxtea.Key]string{ + pyxtea.KeyUS: "us", + pyxtea.KeyJP: "jp", + pyxtea.KeyTH: "th", + pyxtea.KeyEU: "eu", + pyxtea.KeyID: "id", + pyxtea.KeyKR: "kr", +} + +func GetRegionKey(regionCode string) (pyxtea.Key, error) { + key, ok := regionToKey[regionCode] + if !ok { + return pyxtea.Key{}, fmt.Errorf("invalid region %q (valid regions: us, jp, th, eu, id, kr)", regionCode) + } + return key, nil +} + +func GetKeyRegion(key pyxtea.Key) string { + region, ok := keyToRegion[key] + if !ok { + panic("programming error: unexpected key") + } + return region +} diff --git a/common/restruct.go b/common/restruct.go new file mode 100644 index 0000000..1f9644e --- /dev/null +++ b/common/restruct.go @@ -0,0 +1,25 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package common + +import "github.com/go-restruct/restruct" + +func init() { + // We're using restruct expressions for some of the packets. + restruct.EnableExprBeta() +} diff --git a/common/server.go b/common/server.go new file mode 100755 index 0000000..2569280 --- /dev/null +++ b/common/server.go @@ -0,0 +1,87 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package common + +import ( + "net" + "sync" + + log "github.com/sirupsen/logrus" +) + +type BaseHandlerFunc func(*log.Entry, net.Conn) error + +type BaseServer struct { + mu sync.RWMutex + listener net.Listener +} + +func (b *BaseServer) setListener(l net.Listener) { + b.mu.Lock() + defer b.mu.Unlock() + + b.listener = l +} + +// Listen implements a basic TCP server connection loop, dispatching connections +// to the handle function. It has some basic provisions for logging and error +// handling as well. +func (b *BaseServer) Listen(logger *log.Entry, addr string, handler BaseHandlerFunc) error { + var err error + + listener, err := net.Listen("tcp", addr) + if err != nil { + return err + } + defer listener.Close() + b.setListener(listener) + + for { + socket, err := listener.Accept() + if err != nil { + return err + } + go func() { + logger := logger.WithFields(log.Fields{"addr": socket.RemoteAddr()}) + logger.Info("Entering connection thread.") + defer func() { + if r := recover(); r != nil { + logger.WithField("error", r).Error("PANIC in connection") + } + logger.Info("Exiting connection thread.") + }() + err := handler(logger, socket) + if err != nil { + logger.WithError(err).Error("ERROR in connection") + } + }() + } +} + +// Close stops accepting connections. +func (b *BaseServer) Close() error { + var err error + + b.mu.RLock() + defer b.mu.RUnlock() + + if b.listener != nil { + err = b.listener.Close() + } + return err +} diff --git a/common/serverconn.go b/common/serverconn.go new file mode 100755 index 0000000..efa2561 --- /dev/null +++ b/common/serverconn.go @@ -0,0 +1,159 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package common + +import ( + "encoding/binary" + "encoding/hex" + "io" + "net" + + "github.com/davecgh/go-spew/spew" + "github.com/go-restruct/restruct" + "github.com/pangbox/pangcrypt" + log "github.com/sirupsen/logrus" +) + +// ServerConn provides base functionality for PangYa-compatible servers. +type ServerConn[ClientMsg Message, ServerMsg Message] struct { + Socket net.Conn + Key uint8 + Log *log.Entry + + ClientMsg MessageTable[ClientMsg] + ServerMsg MessageTable[ServerMsg] +} + +// ReadPacket attempts to read a single packet from the socket. +func (c *ServerConn[_, _]) ReadPacket() ([]byte, error) { + packetHeaderBytes := [4]byte{} + + read, err := c.Socket.Read(packetHeaderBytes[:]) + if err != nil { + return nil, err + } else if read != len(packetHeaderBytes) { + return nil, io.EOF + } + + remaining := binary.LittleEndian.Uint16(packetHeaderBytes[1:3]) + packet := make([]byte, len(packetHeaderBytes)+int(remaining)) + copy(packet[:4], packetHeaderBytes[:]) + read, err = c.Socket.Read(packet[4:]) + if err != nil { + return nil, err + } else if read != len(packet[4:]) { + return nil, io.EOF + } + + return pangcrypt.ClientDecrypt(packet, c.Key) +} + +// ParsePacket attempts to construct a packet from packet data. +func (c *ServerConn[ClientMsg, _]) ParsePacket(packet []byte) (ClientMsg, error) { + msgid := binary.LittleEndian.Uint16(packet[:2]) + + c.Log.Debug(hex.Dump(packet)) + + message, err := c.ClientMsg.Build(msgid) + if err != nil { + return message, err + } + + err = restruct.Unpack(packet[2:], binary.LittleEndian, message) + if err != nil { + return message, err + } + + return message, nil +} + +// ReadMessage reads a single packet and parses it. +func (c *ServerConn[ClientMsg, _]) ReadMessage() (ClientMsg, error) { + var message ClientMsg + + data, err := c.ReadPacket() + if err != nil { + return message, err + } + + return c.ParsePacket(data) +} + +// SendMessage sends a message to the client. +func (c *ServerConn[_, ServerMsg]) SendMessage(msg ServerMsg) error { + data, err := restruct.Pack(binary.LittleEndian, msg) + if err != nil { + return err + } + + id, err := c.ServerMsg.ID(msg) + if err != nil { + return err + } + + msgid := [2]byte{} + binary.LittleEndian.PutUint16(msgid[:], id) + data = append(msgid[:], data...) + + data, err = pangcrypt.ServerEncrypt(data, c.Key, 0) + if err != nil { + return err + } + written, err := c.Socket.Write(data) + if err != nil { + return err + } else if written != len(data) { + return io.EOF + } + return nil +} + +// DebugMsg prints a message. +func (c *ServerConn[_, ServerMsg]) DebugMsg(msg ServerMsg) error { + data, err := restruct.Pack(binary.LittleEndian, msg) + if err != nil { + return err + } + + id, err := c.ServerMsg.ID(msg) + if err != nil { + return err + } + + msgid := [2]byte{} + binary.LittleEndian.PutUint16(msgid[:], id) + data = append(msgid[:], data...) + + spew.Dump(data) + return nil +} + +// SendRaw sends raw bytes into a PangYa packet. +func (c *ServerConn[_, ServerMsg]) SendRaw(data []byte) error { + data, err := pangcrypt.ServerEncrypt(data, c.Key, 0) + if err != nil { + return err + } + written, err := c.Socket.Write(data) + if err != nil { + return err + } else if written != len(data) { + return io.EOF + } + return nil +} diff --git a/common/table.go b/common/table.go new file mode 100644 index 0000000..d7725a1 --- /dev/null +++ b/common/table.go @@ -0,0 +1,78 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package common + +import ( + "log" + "reflect" +) + +type Message interface{} + +type MessageTable[T Message] struct { + IDToMessage map[uint16]T + MessageToID map[reflect.Type]uint16 +} + +type AnyMessageTable interface { + ID(msg Message) (uint16, error) + Build(id uint16) (Message, error) +} + +func NewMessageTable[T Message](index map[uint16]T) MessageTable[T] { + table := MessageTable[T]{ + IDToMessage: index, + MessageToID: make(map[reflect.Type]uint16), + } + for id, msg := range index { + typ := reflect.TypeOf(msg) + if otherId, ok := table.MessageToID[typ]; ok { + log.Fatalf("conflict: multiple IDs for message %T: 0x%04x and 0x%04x", msg, id, otherId) + } + table.MessageToID[typ] = id + } + return table +} + +func (table MessageTable[T]) Any() AnyMessageTable { + anytable := &MessageTable[Message]{ + IDToMessage: make(map[uint16]Message), + MessageToID: make(map[reflect.Type]uint16), + } + for id, msg := range table.IDToMessage { + anytable.IDToMessage[id] = msg + anytable.MessageToID[reflect.TypeOf(msg)] = id + } + return anytable +} + +func (table MessageTable[T]) ID(msg T) (uint16, error) { + id, ok := table.MessageToID[reflect.TypeOf(msg)] + if !ok { + return id, UnknownMessageError{MessageID: id} + } + return id, nil +} + +func (table MessageTable[T]) Build(id uint16) (T, error) { + message, ok := table.IDToMessage[id] + if !ok { + return message, UnknownMessageError{MessageID: id} + } + return reflect.New(reflect.TypeOf(message).Elem()).Interface().(T), nil +} diff --git a/common/topology/client.go b/common/topology/client.go new file mode 100644 index 0000000..7f3f994 --- /dev/null +++ b/common/topology/client.go @@ -0,0 +1,74 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package topology + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + "golang.org/x/net/http2" +) + +const DefaultDialTimeout = 10 * time.Second + +type ClientOptions struct { + // BaseURL specifies the base URL to send requests to. Use the h2c:// scheme to use H2C. + BaseURL string + + // DialTimeout specifies the maximum amount of time to dial the topology server in a request. + // If unspecified, this will be [DefaultDialTimeout]. + DialTimeout time.Duration +} + +func NewClient(options ClientOptions) (topologypbconnect.TopologyServiceClient, error) { + baseURL, err := url.Parse(options.BaseURL) + if err != nil { + return nil, fmt.Errorf("parsing topology server URL: %w", err) + } + + if options.DialTimeout == 0 { + options.DialTimeout = DefaultDialTimeout + } + + transport := &http2.Transport{} + netDialer := net.Dialer{Timeout: options.DialTimeout} + if baseURL.Scheme == "h2c" { + baseURL.Scheme = "https" + transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return netDialer.DialContext(ctx, network, addr) + } + } else { + transport.DialTLSContext = func(ctx context.Context, network, addr string, tlsConfig *tls.Config) (net.Conn, error) { + tlsDialer := tls.Dialer{ + NetDialer: &netDialer, + Config: tlsConfig, + } + return tlsDialer.DialContext(ctx, network, addr) + } + } + client := &http.Client{ + Transport: transport, + } + return topologypbconnect.NewTopologyServiceClient(client, baseURL.String()), nil +} diff --git a/common/topology/server.go b/common/topology/server.go new file mode 100755 index 0000000..14895b4 --- /dev/null +++ b/common/topology/server.go @@ -0,0 +1,79 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package topology + +import ( + "context" + + "github.com/bufbuild/connect-go" + "github.com/pangbox/server/gen/proto/go/topologypb" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" +) + +// Ensure that we are always implementing the full Topology service. +var _ = topologypbconnect.TopologyServiceHandler(&Server{}) + +// Server implements TopologyServiceServer. +type Server struct { + storage Storage +} + +// NewServer creates a new Topology server. +func NewServer(storage Storage) *Server { + return &Server{storage} +} + +// AddServer implements TopologyServiceServer. +func (s *Server) AddServer(ctx context.Context, request *connect.Request[topologypb.AddServerRequest]) (*connect.Response[topologypb.AddServerResponse], error) { + err := s.storage.Put(uint16(request.Msg.Server.Id), &topologypb.ServerEntry{ + Server: request.Msg.Server, + }) + if err != nil { + return nil, err + } + return connect.NewResponse(&topologypb.AddServerResponse{}), nil +} + +// ListServers implements TopologyServiceServer. +func (s *Server) ListServers(ctx context.Context, request *connect.Request[topologypb.ListServersRequest]) (*connect.Response[topologypb.ListServersResponse], error) { + // Get full server list. + entries, err := s.storage.List() + if err != nil { + return nil, err + } + + // Do filtering. + filteredServers := []*topologypb.Server{} + for _, entry := range entries { + if request.Msg.Type != topologypb.Server_TYPE_UNSPECIFIED && entry.Server.Type != request.Msg.Type { + continue + } + filteredServers = append(filteredServers, entry.Server) + } + + return connect.NewResponse(&topologypb.ListServersResponse{Server: filteredServers}), nil +} + +// GetServer implements TopologyServiceServer. +func (s *Server) GetServer(ctx context.Context, request *connect.Request[topologypb.GetServerRequest]) (*connect.Response[topologypb.GetServerResponse], error) { + entry, err := s.storage.Get(uint16(request.Msg.Id)) + if err != nil { + return nil, err + } + return connect.NewResponse(&topologypb.GetServerResponse{Server: entry.Server}), nil +} diff --git a/common/topology/storage.go b/common/topology/storage.go new file mode 100755 index 0000000..6d735c0 --- /dev/null +++ b/common/topology/storage.go @@ -0,0 +1,158 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package topology + +import ( + "encoding/binary" + "errors" + + "github.com/bufbuild/connect-go" + "github.com/pangbox/server/gen/proto/go/topologypb" + "github.com/syndtr/goleveldb/leveldb" + "google.golang.org/protobuf/proto" +) + +var ( + errServerNotFound = connect.NewError(connect.CodeNotFound, errors.New("server with ID not found")) +) + +// Storage implementations implement a storage layer for the topology. +type Storage interface { + Get(id uint16) (*topologypb.ServerEntry, error) + Put(id uint16, server *topologypb.ServerEntry) error + List() ([]*topologypb.ServerEntry, error) +} + +// MemoryStorage implements topology storage on top of memory. MemoryStorage +// defensively deep-copies all protobufs to prevent any pointers from being +// shared amongst users and the storage layers, similar to a database engine +// that would require marshalling and unmarshalling. +type MemoryStorage struct { + servers []*topologypb.ServerEntry + serverMap map[uint16]*topologypb.ServerEntry +} + +// NewMemoryStorage creates a new memory store with the provided servers. +func NewMemoryStorage(entries []*topologypb.ServerEntry) *MemoryStorage { + store := &MemoryStorage{ + servers: []*topologypb.ServerEntry{}, + serverMap: map[uint16]*topologypb.ServerEntry{}, + } + + for _, entry := range entries { + _ = store.Put(uint16(entry.Server.Id), entry) + } + + return store +} + +// Get implements topology.Storage. +func (s *MemoryStorage) Get(id uint16) (*topologypb.ServerEntry, error) { + if entry, ok := s.serverMap[id]; ok { + return proto.Clone(entry).(*topologypb.ServerEntry), nil + } + return nil, errServerNotFound +} + +// Put implements topology.Storage. +func (s *MemoryStorage) Put(id uint16, entry *topologypb.ServerEntry) error { + s.serverMap[id] = proto.Clone(entry).(*topologypb.ServerEntry) + for i := range s.servers { + if uint16(s.servers[i].Server.Id) == id { + s.servers[i] = proto.Clone(entry).(*topologypb.ServerEntry) + return nil + } + } + s.servers = append(s.servers, proto.Clone(entry).(*topologypb.ServerEntry)) + return nil +} + +// List implements topology.Storage. +func (s *MemoryStorage) List() ([]*topologypb.ServerEntry, error) { + result := make([]*topologypb.ServerEntry, len(s.servers)) + for i, server := range s.servers { + result[i] = proto.Clone(server).(*topologypb.ServerEntry) + } + return result, nil +} + +// LevelDBStorage implements topology storage on top of LevelDB. +type LevelDBStorage struct { + db *leveldb.DB +} + +// NewLevelDBStorage creates a new LevelDB-backed storage engine. +func NewLevelDBStorage(db *leveldb.DB) *LevelDBStorage { + return &LevelDBStorage{db} +} + +func (s *LevelDBStorage) toKey(id uint16) []byte { + k := [2]byte{} + binary.BigEndian.PutUint16(k[:], id) + return k[:] +} + +func (s *LevelDBStorage) translateErr(err error) error { + switch err { + case leveldb.ErrNotFound: + return errServerNotFound + default: + return err + } +} + +// Get implements topology.Storage. +func (s *LevelDBStorage) Get(id uint16) (*topologypb.ServerEntry, error) { + val, err := s.db.Get(s.toKey(id), nil) + if err != nil { + return nil, s.translateErr(err) + } + result := &topologypb.ServerEntry{} + if err := proto.Unmarshal(val, result); err != nil { + return nil, err + } + return result, nil +} + +// Put implements topology.Storage. +func (s *LevelDBStorage) Put(id uint16, server *topologypb.ServerEntry) error { + val, err := proto.Marshal(server) + if err != nil { + return err + } + + return s.db.Put(s.toKey(id), val, nil) +} + +// List implements topology.Storage. +func (s *LevelDBStorage) List() ([]*topologypb.ServerEntry, error) { + result := []*topologypb.ServerEntry{} + iter := s.db.NewIterator(nil, nil) + defer iter.Release() + for iter.Next() { + entry := &topologypb.ServerEntry{} + if err := proto.Unmarshal(iter.Value(), entry); err != nil { + return nil, err + } + result = append(result, entry) + } + if err := iter.Error(); err != nil { + return nil, err + } + return result, nil +} diff --git a/database/accounts/service.go b/database/accounts/service.go new file mode 100755 index 0000000..c663b7f --- /dev/null +++ b/database/accounts/service.go @@ -0,0 +1,169 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package accounts + +import ( + "context" + "database/sql" + "encoding/binary" + "errors" + "time" + + "github.com/go-restruct/restruct" + "github.com/google/uuid" + "github.com/pangbox/server/common/hash" + "github.com/pangbox/server/gen/dbmodels" + "github.com/pangbox/server/pangya" +) + +// Enumeration of possible errors that can be returned from authenticate. +var ( + ErrInvalidPassword = errors.New("invalid password") + ErrUnknownUsername = errors.New("unknown user") +) + +const sessionTimeout = 15 * time.Minute + +// Options specifies options for account services. +type Options struct { + Database dbmodels.DBTX + Hasher hash.Hasher +} + +// Service implements account services using the database. +type Service struct { + database *dbmodels.Queries + hasher hash.Hasher +} + +// NewService creates new account services using the database. +func NewService(opts Options) *Service { + return &Service{ + database: dbmodels.New(opts.Database), + hasher: opts.Hasher, + } +} + +func (s *Service) GetPlayer(ctx context.Context, playerID int64) (dbmodels.Player, error) { + return s.database.GetPlayer(ctx, playerID) +} + +func (s *Service) Register(ctx context.Context, username, password string) (dbmodels.Player, error) { + hash, err := s.hasher.Hash(password) + if err != nil { + return dbmodels.Player{}, err + } + return s.database.CreatePlayer(ctx, dbmodels.CreatePlayerParams{ + Username: username, + PasswordHash: hash, + }) +} + +// Authenticate authenticates a user using the database. +func (s *Service) Authenticate(ctx context.Context, username, password string) (dbmodels.Player, error) { + player, err := s.database.GetPlayerByUsername(ctx, username) + if errors.Is(err, sql.ErrNoRows) { + return dbmodels.Player{}, ErrUnknownUsername + } else if err != nil { + return dbmodels.Player{}, err + } + if !s.hasher.CheckHash(password, player.PasswordHash) { + return dbmodels.Player{}, ErrInvalidPassword + } + return player, nil +} + +// SetNickname sets the player's nickname. +func (s *Service) SetNickname(ctx context.Context, playerID int64, nickname string) (dbmodels.Player, error) { + return s.database.SetPlayerNickname(ctx, dbmodels.SetPlayerNicknameParams{ + PlayerID: playerID, + Nickname: sql.NullString{Valid: true, String: nickname}, + }) +} + +// HasCharacters returns whether or not the player has characters. +func (s *Service) HasCharacters(ctx context.Context, playerID int64) (bool, error) { + return s.database.PlayerHasCharacters(ctx, playerID) +} + +// GetCharacters returns the characters for a given player. +func (s *Service) GetCharacters(ctx context.Context, playerID int64) ([]pangya.PlayerCharacterData, error) { + characters, err := s.database.GetCharactersByPlayer(ctx, playerID) + if err != nil { + return nil, err + } + result := make([]pangya.PlayerCharacterData, len(characters)) + for i, character := range characters { + if err := restruct.Unpack(character.CharacterData, binary.LittleEndian, &result[i]); err != nil { + return nil, err + } + result[i].ID = uint32(character.CharacterID) + } + return result, nil +} + +// AddCharacter adds a character for a given player. +func (s *Service) AddCharacter(ctx context.Context, playerID int64, data pangya.PlayerCharacterData) error { + blob, err := restruct.Pack(binary.LittleEndian, &data) + if err != nil { + return err + } + _, err = s.database.CreateCharacter(ctx, dbmodels.CreateCharacterParams{ + PlayerID: playerID, + CharacterTypeID: int64(data.CharTypeID), + CharacterData: blob, + }) + return err +} + +// AddSession adds a new session for a player. +func (s *Service) AddSession(ctx context.Context, playerID int64, address string) (dbmodels.Session, error) { + sessionKey, err := uuid.NewRandom() + if err != nil { + return dbmodels.Session{}, err + } + return s.database.CreateSession(ctx, dbmodels.CreateSessionParams{ + PlayerID: playerID, + SessionKey: sessionKey.String(), + SessionAddress: address, + SessionExpiresAt: time.Now().Add(sessionTimeout).Unix(), + }) +} + +// GetSession gets a session by its ID. +func (s *Service) GetSession(ctx context.Context, sessionID int64) (dbmodels.Session, error) { + return s.database.GetSession(ctx, sessionID) +} + +// GetSessionByKey gets a session by its session key. +func (s *Service) GetSessionByKey(ctx context.Context, sessionKey string) (dbmodels.Session, error) { + return s.database.GetSessionByKey(ctx, sessionKey) +} + +// UpdateSessionExpiry bumps the session expiry value for a session. +func (s *Service) UpdateSessionExpiry(ctx context.Context, sessionID int64) (dbmodels.Session, error) { + return s.database.UpdateSessionExpiry(ctx, dbmodels.UpdateSessionExpiryParams{ + SessionID: sessionID, + SessionExpiresAt: time.Now().Add(sessionTimeout).Unix(), + }) +} + +// DeleteExpiredSessions deletes sessions that have expired. +func (s *Service) DeleteExpiredSessions(ctx context.Context) error { + return s.database.DeleteExpiredSessions(ctx, time.Now().Unix()) +} diff --git a/database/dialect.go b/database/dialect.go new file mode 100644 index 0000000..22153fa --- /dev/null +++ b/database/dialect.go @@ -0,0 +1,61 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package database + +import ( + "database/sql" + "errors" + + "github.com/pressly/goose/v3" +) + +type Dialect int + +const ( + DialectPostgreSQL Dialect = 1 + DialectSQLite Dialect = 2 +) + +var ( + dialect Dialect +) + +func AutoIncrementColumnType() string { + switch dialect { + case DialectPostgreSQL: + return " SERIAL PRIMARY KEY " + case DialectSQLite: + return " INTEGER PRIMARY KEY " + } + panic("no dialect set") +} + +func OpenDBWithDriver(driver string, dsn string) (*sql.DB, error) { + switch driver { + case "pgx": + dialect = DialectPostgreSQL + goose.SetDialect("postgres") + case "sqlite", "sqlite3": + dialect = DialectSQLite + goose.SetDialect("sqlite3") + driver = "sqlite" + default: + return nil, errors.New("unsupported SQL dialect") + } + return sql.Open(driver, dsn) +} diff --git a/database/postgres_test.go b/database/postgres_test.go new file mode 100644 index 0000000..bc7d66c --- /dev/null +++ b/database/postgres_test.go @@ -0,0 +1,67 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package database + +import ( + "context" + "testing" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/pangbox/server/gen/dbmodels" + _ "github.com/pangbox/server/migrations" + "github.com/pressly/goose/v3" + "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +func RunPostgreSQLTest(t *testing.T, cb func(*testing.T, dbmodels.DBTX)) { + t.Helper() + + ctx := context.Background() + + container, err := postgres.RunContainer(ctx) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + if err := container.Terminate(ctx); err != nil { + t.Fatalf("Could not terminate container: %v", err) + } + }) + + dsn, err := container.ConnectionString(ctx) + if err != nil { + t.Fatalf("Could not determine dsn: %v", err) + } + + db, err := OpenDBWithDriver("pgx", dsn) + if err != nil { + t.Fatalf("Failed to open DB: %v", err) + } + + if err = goose.Up(db, "."); err != nil { + t.Fatalf("Failed to run migrations forward: %v", err) + } + + cb(t, db) + + err = goose.Down(db, ".") + if err != nil { + t.Fatalf("Failed to run migrations backward: %v", err) + } +} diff --git a/database/query_test.go b/database/query_test.go new file mode 100644 index 0000000..b8179ad --- /dev/null +++ b/database/query_test.go @@ -0,0 +1,75 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package database + +import ( + "context" + "database/sql" + "testing" + + "github.com/pangbox/server/gen/dbmodels" + "github.com/stretchr/testify/assert" +) + +func TestPostgreSQL(t *testing.T) { + t.Skip("postgresql support is currently broken") + RunPostgreSQLTest(t, testCreateUser) + RunPostgreSQLTest(t, testCreateUserUsernameUnique) +} + +func TestSQLite(t *testing.T) { + RunSQLiteTest(t, testCreateUser) + RunSQLiteTest(t, testCreateUserUsernameUnique) +} + +func testCreateUser(t *testing.T, db dbmodels.DBTX) { + ctx := context.Background() + + queries := dbmodels.New(db) + user1, err := queries.CreatePlayer(ctx, dbmodels.CreatePlayerParams{ + Username: "test", + Nickname: sql.NullString{String: "testnick", Valid: true}, + PasswordHash: "xxx", + }) + assert.NoError(t, err) + user2, err := queries.CreatePlayer(ctx, dbmodels.CreatePlayerParams{ + Username: "test2", + Nickname: sql.NullString{String: "testnick2", Valid: true}, + PasswordHash: "xxx", + }) + assert.NoError(t, err) + assert.NotEqual(t, user1.PlayerID, user2.PlayerID) +} + +func testCreateUserUsernameUnique(t *testing.T, db dbmodels.DBTX) { + ctx := context.Background() + + queries := dbmodels.New(db) + _, err := queries.CreatePlayer(ctx, dbmodels.CreatePlayerParams{ + Username: "test", + Nickname: sql.NullString{String: "testnick", Valid: true}, + PasswordHash: "xxx", + }) + assert.NoError(t, err) + _, err = queries.CreatePlayer(ctx, dbmodels.CreatePlayerParams{ + Username: "test", + Nickname: sql.NullString{String: "testnick", Valid: true}, + PasswordHash: "xxx", + }) + assert.ErrorContains(t, err, "UNIQUE") +} diff --git a/database/sqlite_test.go b/database/sqlite_test.go new file mode 100644 index 0000000..867423a --- /dev/null +++ b/database/sqlite_test.go @@ -0,0 +1,48 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package database + +import ( + "testing" + + "github.com/pangbox/server/gen/dbmodels" + _ "github.com/pangbox/server/migrations" + "github.com/pressly/goose/v3" + _ "modernc.org/sqlite" +) + +func RunSQLiteTest(t *testing.T, cb func(*testing.T, dbmodels.DBTX)) { + t.Helper() + + db, err := OpenDBWithDriver("sqlite", ":memory:") + if err != nil { + t.Fatalf("Failed to open DB: %v", err) + } + + err = goose.Up(db, ".") + if err != nil { + t.Fatalf("Failed to run migrations forward: %v", err) + } + + cb(t, db) + + err = goose.Down(db, ".") + if err != nil { + t.Fatalf("Failed to run migrations backward: %v", err) + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..05361aa --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1684647100, + "narHash": "sha256-8Oar1AW/s1EAXuM0b2n1VR0c33666ZCnEroPCMUst9k=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2dee1073d978512e21155a664c9bd15d54792c9e", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d57148c --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + description = "Pangbox server for PangYa."; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in rec { + packages = rec { + default = pkgs.buildGoModule { + name = "pangbox"; + src = self; + vendorHash = "sha256-FuW0wTQFA1l2HJHut/MSQlbcGb4A9mc+PamofREdIAM="; + }; + }; + devShell = pkgs.mkShell { + inputsFrom = with packages; [ default ]; + }; + } + ); +} diff --git a/game/conn.go b/game/conn.go new file mode 100755 index 0000000..3d30e2f --- /dev/null +++ b/game/conn.go @@ -0,0 +1,496 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package game + +import ( + "context" + "encoding/binary" + "fmt" + "math/rand" + + "github.com/bufbuild/connect-go" + "github.com/go-restruct/restruct" + "github.com/pangbox/server/common" + "github.com/pangbox/server/gen/dbmodels" + "github.com/pangbox/server/gen/proto/go/topologypb" + "github.com/pangbox/server/pangya" +) + +// Conn holds the state for a connection to the server. +type Conn struct { + common.ServerConn[ClientMessage, ServerMessage] + s *Server +} + +// SendHello sends the initial handshake bytes to the client. +func (c *Conn) SendHello() error { + data, err := restruct.Pack(binary.LittleEndian, &ConnectMessage{ + Unknown: [8]byte{0x00, 0x06, 0x00, 0x00, 0x3f, 0x00, 0x01, 0x01}, + Key: c.Key, + }) + if err != nil { + return err + } + + _, err = c.Socket.Write(data) + if err != nil { + return err + } + + return nil +} + +// Handle runs the main connection loop. +func (c *Conn) Handle(ctx context.Context) error { + log := c.Log + c.Key = uint8(rand.Intn(16)) + + err := c.SendHello() + if err != nil { + return fmt.Errorf("sending hello message: %w", err) + } + + msg, err := c.ReadMessage() + if err != nil { + return fmt.Errorf("reading handshake: %w", err) + } + + var session dbmodels.Session + var player dbmodels.Player + switch t := msg.(type) { + case *ClientAuth: + session, err = c.s.accountsService.GetSessionByKey(ctx, t.LoginKey.Value) + if err != nil { + // TODO: error handling + return nil + } + player, err = c.s.accountsService.GetPlayer(ctx, session.PlayerID) + if err != nil { + // TODO: error handling + return nil + } + log.Debugf("Client auth: %#v", msg) + + default: + return fmt.Errorf("expected client auth, got %T", t) + } + + playerCharacters, err := c.s.accountsService.GetCharacters(ctx, session.PlayerID) + if err != nil { + // TODO: handle error for client + return fmt.Errorf("database error: %w", err) + } + + playerGameData := ServerUserData{ + ClientVersion: common.ToPString("824.00"), + ServerVersion: common.ToPString("Pangbox"), + Game: 0xFFFF, + UserInfo: pangya.UserInfo{ + Username: player.Username, + Nickname: player.Nickname.String, + PlayerID: uint32(player.PlayerID), + ConnnectionID: uint32(session.SessionID), + }, + PlayerStats: pangya.PlayerStats{ + Pangs: uint64(player.Pang), + }, + Items: pangya.PlayerEquipment{ + CaddieID: 0, + CharacterID: playerCharacters[0].ID, + ClubSetID: 0x1754, + AztecIffID: 0x14000000, + Items: pangya.PlayerEquippedItems{ + ItemIDs: [10]uint32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, + }, + EquippedCharacter: playerCharacters[0], + EquippedClub: pangya.PlayerClubData{ + Item: pangya.PlayerItem{ + ID: 0x1754, + IFFID: 0x10000000, + }, + Stats: pangya.ClubStats{ + UpgradeStats: [5]uint16{8, 9, 8, 3, 3}, + }, + }, + } + + c.SendMessage(&playerGameData) + + c.SendMessage(&ServerCharData{ + Count1: uint16(len(playerCharacters)), + Count2: uint16(len(playerCharacters)), + Characters: playerCharacters, + }) + + c.SendMessage(&ServerAchievementProgress{ + Remaining: 0, + Count: 0, + }) + + c.SendMessage(&ServerMessageConnect{}) + + message := &ServerChannelList{} + response, err := c.s.topologyClient.ListServers(ctx, connect.NewRequest(&topologypb.ListServersRequest{ + Type: topologypb.Server_TYPE_GAME_SERVER, + })) + if err != nil { + // TODO: error handling to client + return err + } + for _, server := range response.Msg.Server { + entry := pangya.ServerEntry{ + ServerName: server.Name, + ServerID: server.Id, + NumUsers: server.NumUsers, + MaxUsers: server.MaxUsers, + IPAddress: server.Address, + Port: uint16(server.Port), + Flags: uint16(server.Flags), + } + if server.Id == c.s.serverID { + // TODO: support multiple channels? + entry.Channels = append(entry.Channels, pangya.ChannelEntry{ + ChannelName: c.s.channelName, + MaxUsers: 200, // TODO + NumUsers: 0, // TODO + Unknown2: 0x0008, // TODO + }) + } + message.Servers = append(message.Servers, entry) + } + message.Count = uint8(len(response.Msg.Server)) + c.SendMessage(message) + + status := &ServerRoomStatus{} + + for { + msg, err = c.ReadMessage() + if err != nil { + return fmt.Errorf("reading next message: %w", err) + } + + switch t := msg.(type) { + case *ClientException: + log.WithField("exception", t.Message).Debug("Client exception") + return fmt.Errorf("client reported exception: %v", t.Message) + case *ClientMessageSend: + event := &ServerGlobalEvent{Type: ChatMessageData} + event.Data.Message = t.Message + event.Data.Nickname = t.Nickname + c.SendMessage(event) + log.Debug(t.Message.Value) + case *ClientRequestMessengerList: + // TODO + log.Debug("TODO: messenger list") + case *ClientGetUserOnlineStatus: + // TODO + log.Debug("TODO: online status") + case *ClientGetUserData: + // TODO + log.Debug("TODO: user data") + case *ClientRoomLoungeAction: + c.SendMessage(&ServerRoomLoungeAction{ + ConnID: uint32(session.SessionID), + LoungeAction: t.LoungeAction, + }) + case *ClientRoomCreate: + c.SendMessage(&ServerRoomJoin{ + RoomName: t.RoomName.Value, + RoomNumber: 1, + EventNumber: 0, + }) + status = &ServerRoomStatus{ + RoomType: t.RoomType, + Course: t.Course, + NumHoles: t.NumHoles, + HoleProgression: 1, + NaturalWind: 0, + MaxUsers: t.MaxUsers, + ShotTimerMS: t.ShotTimerMS, + GameTimerMS: t.GameTimerMS, + Flags: 0, + Owner: true, + RoomName: t.RoomName, + } + c.SendMessage(status) + self := RoomListUser{ + ConnID: uint32(session.SessionID), + Nickname: player.Nickname.String, + GuildName: "", + Slot: 1, + CharTypeID: playerGameData.EquippedCharacter.CharTypeID, + Flag2: 520, // ? + GuildEmblemImage: "guildmark", + UserID: uint32(player.PlayerID), + CharacterData: playerGameData.EquippedCharacter, + } + other := RoomListUser{ + ConnID: 0xFEEE, + Nickname: "other", + GuildName: "", + Slot: 2, + CharTypeID: 0x04000005, + Flag2: 520, + GuildEmblemImage: "guildmark", + UserID: 0x2000, + CharacterData: pangya.PlayerCharacterData{ + CharTypeID: 0x04000007, + ID: 0x50000, + HairColor: 0, + }, + } + c.SendMessage(&ServerRoomCensus{ + Type: byte(ListSet), + Unknown: 0xFFFF, + ListSet: &RoomCensusListSet{ + UserCount: 2, + UserList: []RoomListUser{self, other}, + }, + }) + c.SendMessage(&ServerPlayerReady{ + ConnID: 0xFEEE, + State: 0, + }) + case *ClientAssistModeToggle: + c.SendMessage(&ServerAssistModeToggled{}) + // TODO: Should send user status update; need to look at packet dumps. + case *ClientPlayerReady: + c.SendMessage(&Server0230{}) + c.SendMessage(&Server0231{}) + c.SendMessage(&ServerGameInit{ + NumPlayers: 2, + Players: []GamePlayer{ + { + Number: 1, + Info: PlayerInfo{ + Username: player.Username, + Nickname: player.Nickname.String, + }, + Game: PlayerGameInfo{}, + Character: playerGameData.EquippedCharacter, + Caddie: playerGameData.EquippedCaddie, + ClubSet: playerGameData.EquippedClub, + Mascot: playerGameData.EquippedMascot, + StartTime: pangya.SystemTime{}, + NumCards: 0, + }, + { + Number: 2, + Info: PlayerInfo{ + Username: "otheru", + Nickname: "other", + }, + Game: PlayerGameInfo{}, + Character: playerGameData.EquippedCharacter, + Caddie: playerGameData.EquippedCaddie, + ClubSet: playerGameData.EquippedClub, + Mascot: playerGameData.EquippedMascot, + StartTime: pangya.SystemTime{}, + NumCards: 0, + }, + }, + }) + gameData := &ServerRoomGameData{ + Course: status.Course, + Unknown: 0x0, + HoleProgression: status.HoleProgression, + NumHoles: status.NumHoles, + Unknown2: 0x0, + ShotTimerMS: status.ShotTimerMS, + GameTimerMS: status.GameTimerMS, + RandomSeed: rand.Uint32(), + } + for i := byte(0); i < gameData.NumHoles; i++ { + gameData.Holes = append(gameData.Holes, HoleInfo{ + HoleID: rand.Uint32(), + Pin: 0x0, + Course: status.Course, + Num: i, + }) + } + c.SendMessage(gameData) + // (currently crashes...) + case *ClientRequestInboxList: + // TODO: need new sql message table + msg := &ServerInboxList{ + PageNum: t.PageNum, + NumPages: 1, + NumMessages: 1, + Messages: []InboxMessage{ + {ID: 0x1, SenderNickname: "@Pangbox"}, + }, + } + c.DebugMsg(msg) + c.SendMessage(msg) + case *ClientRequestInboxMessage: + c.SendMessage(&ServerMailMessage{ + Message: MailMessage{ + ID: 0x1, + SenderNickname: common.ToPString("@Pangbox"), + DateTime: common.ToPString("2023-06-03 01:21:00"), + Message: common.ToPString("Welcome to the first Pangbox server release! Not much works yet..."), + }, + }) + case *ClientUnknownCounter: + // Do nothing. + case *Client001A: + // Do nothing. + case *ClientJoinChannel: + c.SendMessage(&Server004E{Unknown: []byte{0x01}}) + c.SendMessage(&Server01F6{Unknown: []byte{0x00, 0x00, 0x00, 0x00}}) + c.SendMessage(&ServerLoginBonusStatus{Unknown: []byte{0x0, 0x0, 0x0, 0x0, 0x1, 0x4, 0x0, 0x0, 0x18, 0x3, 0x0, 0x0, 0x0, 0x27, 0x0, 0x0, 0x18, 0x3, 0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0}}) + case *ClientRequestDailyReward: + c.SendMessage(&ServerMoneyUpdate{ + Type: uint16(MoneyUpdateRewardUnknown), + RewardUnknown: &UpdateRewardUnknownData{ + Unknown: 1, + }, + }) + case *Client009C: + c.SendMessage(&Server010E{Unknown: make([]byte, 0x104)}) + case *ClientMultiplayerJoin: + c.SendMessage(&ServerRoomList{ + Count: 0, + Type: ListSet, + RoomList: []RoomListRoom{}, + }) + // TODO: lobby room new sql impl + c.SendMessage(&ServerUserCensus{ + Count: 1, + Type: UserListSet, + UserList: []CensusUser{ + { + UserID: uint32(player.PlayerID), + ConnID: uint32(session.SessionID), + RoomNumber: -1, + Nickname: player.Nickname.String, + Rank: byte(player.Rank), + GuildEmblemID: "guildmark", // TODO + GlobalID: player.Username, // TODO + }, + }, + }) + c.SendMessage(&ServerMultiplayerJoined{}) + case *ClientMultiplayerLeave: + c.SendMessage(&ServerMultiplayerLeft{}) + case *ClientEventLobbyJoin: + c.SendMessage(&ServerEventLobbyJoined{}) + case *ClientEventLobbyLeave: + c.SendMessage(&ServerEventLobbyLeft{}) + case *ClientRoomLeave: + log.Println("Client leave room") + c.SendMessage(&ServerRoomLeave{RoomNumber: t.RoomNumber}) + case *ClientRoomEdit: + log.Printf("%#v\n", t) + for _, change := range t.Changes { + if change.RoomName != nil { + status.RoomName = *change.RoomName + } + if change.RoomType != nil { + status.RoomType = *change.RoomType + } + if change.Course != nil { + status.Course = *change.Course + } + if change.NumHoles != nil { + status.NumHoles = *change.NumHoles + } + if change.HoleProgression != nil { + status.HoleProgression = *change.HoleProgression + } + if change.ShotTimerSeconds != nil { + status.ShotTimerMS = uint32(*change.ShotTimerSeconds) * 1000 + } + if change.MaxUsers != nil { + status.MaxUsers = *change.MaxUsers + } + if change.GameTimerMinutes != nil { + status.GameTimerMS = uint32(*change.GameTimerMinutes) * 60 * 1000 + } + if change.NaturalWind != nil { + status.NaturalWind = *change.NaturalWind + } + } + c.SendMessage(status) + case *Client0088: + // Unknown tutorial-related message. + case *ClientRoomUserEquipmentChange: + // TODO + case *ClientTutorialStart: + // TODO + c.SendMessage(&ServerRoomEquipmentData{ + Unknown: []byte{ + 0x00, 0x00, 0x00, 0x01, 0x04, 0x04, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x04, 0xdd, + 0x77, 0x94, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x04, 0x14, 0x08, 0x00, 0x24, 0x14, 0x08, 0x00, + 0x44, 0x14, 0x08, 0x00, 0x64, 0x14, 0x08, 0x00, 0x84, 0x14, 0x08, 0x00, 0xa4, 0x14, 0x08, 0x00, + 0xc4, 0x14, 0x08, 0x00, 0xe4, 0x14, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + }) + case *ClientTutorialClear: + // After clearing first tutorial + // TODO + c.SendMessage(&ServerTutorialStatus{ + Unknown: [6]byte{0x00, 0x01, 0x03, 0x00, 0x00, 0x00}, + }) + case *ClientUserMacrosSet: + // TODO: server-side macro storage + log.Debugf("Set macros: %+v", t.MacroList) + case *ClientEquipmentUpdate: + // TODO + log.Debug("TODO: 0020") + case *Client00FE: + // TODO + log.Debug("TODO: 00FE") + case *ClientShopJoin: + // Enter shop, not sure what responses need to go here? + log.Debug("TODO: 0140") + default: + return fmt.Errorf("unexpected message: %T", t) + } + } +} diff --git a/game/message.go b/game/message.go new file mode 100644 index 0000000..5558640 --- /dev/null +++ b/game/message.go @@ -0,0 +1,38 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package game + +import "github.com/pangbox/server/common" + +type ServerMessage interface { + common.Message + isGameServerMessage() +} + +type ServerMessage_ struct{} + +func (msg *ServerMessage_) isGameServerMessage() {} + +type ClientMessage interface { + common.Message + isGameClientMessage() +} + +type ClientMessage_ struct{} + +func (msg *ClientMessage_) isGameClientMessage() {} diff --git a/game/msgclient.go b/game/msgclient.go new file mode 100755 index 0000000..fe6a918 --- /dev/null +++ b/game/msgclient.go @@ -0,0 +1,314 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package game + +import ( + "github.com/pangbox/server/common" + "github.com/pangbox/server/pangya" +) + +var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{ + 0x0002: &ClientAuth{}, + 0x0003: &ClientMessageSend{}, + 0x0004: &ClientJoinChannel{}, + 0x0007: &ClientGetUserOnlineStatus{}, + 0x0008: &ClientRoomCreate{}, + 0x000A: &ClientRoomEdit{}, + 0x000B: &ClientTutorialStart{}, + 0x000C: &ClientRoomUserEquipmentChange{}, + 0x000D: &ClientPlayerReady{}, + 0x000F: &ClientRoomLeave{}, + 0x001A: &Client001A{}, + 0x001D: &ClientBuyItem{}, + 0x0020: &ClientEquipmentUpdate{}, + 0x002F: &ClientGetUserData{}, + 0x0033: &ClientException{}, + 0x0043: &ClientRequestServerList{}, + 0x0048: &ClientUnknownCounter{}, + 0x0063: &ClientRoomLoungeAction{}, + 0x0069: &ClientUserMacrosSet{}, + 0x0081: &ClientMultiplayerJoin{}, + 0x0082: &ClientMultiplayerLeave{}, + 0x0088: &Client0088{}, + 0x008B: &ClientRequestMessengerList{}, + 0x009C: &Client009C{}, + 0x00AE: &ClientTutorialClear{}, + 0x00FE: &Client00FE{}, + 0x0140: &ClientShopJoin{}, + 0x0143: &ClientRequestInboxList{}, + 0x0144: &ClientRequestInboxMessage{}, + 0x016E: &ClientRequestDailyReward{}, + 0x0176: &ClientEventLobbyJoin{}, + 0x0177: &ClientEventLobbyLeave{}, + 0x0184: &ClientAssistModeToggle{}, +}) + +// ClientAuth is a message sent to authenticate a session. +type ClientAuth struct { + ClientMessage_ + Username common.PString + Unknown1 uint32 + Unknown2 uint32 + Unknown3 uint16 + LoginKey common.PString + Version common.PString +} + +// ClientMessageSend is sent when the client sends a public chat message. +type ClientMessageSend struct { + ClientMessage_ + Nickname common.PString + Message common.PString +} + +// ClientGetUserOnlineStatus is sent to get information of a user. +type ClientGetUserOnlineStatus struct { + ClientMessage_ + Unknown uint8 + Username common.PString +} + +type SettingsChange struct { + Type byte + RoomName *common.PString `struct-if:"Type == 0"` + Password *common.PString `struct-if:"Type == 1"` + RoomType *byte `struct-if:"Type == 2"` + Course *byte `struct-if:"Type == 3"` + NumHoles *uint8 `struct-if:"Type == 4"` + HoleProgression *uint8 `struct-if:"Type == 5"` + ShotTimerSeconds *uint8 `struct-if:"Type == 6"` + MaxUsers *uint8 `struct-if:"Type == 7"` + GameTimerMinutes *uint8 `struct-if:"Type == 8"` + ArtifactID *uint32 `struct-if:"Type == 13"` + NaturalWind *uint32 `struct-if:"Type == 14"` +} + +// ClientRoomEdit is sent when the client changes room settings. +type ClientRoomEdit struct { + ClientMessage_ + Unknown uint16 + NumChanges uint8 `struct:"sizeof=Changes"` + Changes []SettingsChange +} + +// ClientRoomCreate is sent by the client when creating a room. +type ClientRoomCreate struct { + ClientMessage_ + Unknown byte + ShotTimerMS uint32 + GameTimerMS uint32 + MaxUsers uint8 + RoomType byte + NumHoles byte + Course byte + Unknown2 [5]byte + RoomName common.PString + Password common.PString + Unknown3 [4]byte +} + +// ClientJoinChannel is a message sent when the client joins a channel. +type ClientJoinChannel struct { + ClientMessage_ + ChannelID byte +} + +// ClientTutorialStart is sent when starting a tutorial. +type ClientTutorialStart struct { + ClientMessage_ + // TODO +} + +// ClientRoomUserEquipmentChange is sent when a user's equipment changes in a room. +type ClientRoomUserEquipmentChange struct { + ClientMessage_ + // TODO +} + +// ClientPlayerReady is sent by the client when they are ready/to start the game. +type ClientPlayerReady struct { + ClientMessage_ + State byte +} + +// ClientRoomLeave is sent by the client when leaving a room back to lobby +type ClientRoomLeave struct { + ClientMessage_ + Unknown byte + RoomNumber uint16 + Unknown2 uint32 + Unknown3 uint32 + Unknown4 uint32 + Unknown5 uint32 +} + +type Client001A struct { + ClientMessage_ +} + +type PurchaseItem struct { + Unknown uint32 + ItemID uint32 + Unknown2 uint16 + Unknown3 uint16 + Quantity uint32 + ItemCostPang uint32 + ItemCostCookie uint32 +} + +// ClientBuyItem is sent by the client to buy an item from the shop. +type ClientBuyItem struct { + ClientMessage_ + Unknown1 byte + NumItems uint16 `struct:"sizeof=Items"` + Items []PurchaseItem +} + +// ClientEquipmentUpdate +type ClientEquipmentUpdate struct { + ClientMessage_ +} + +// ClientRequestServerList is a message sent to request the current +// list of game servers. +type ClientRequestServerList struct { + ClientMessage_ +} + +type ClientUnknownCounter struct { + ClientMessage_ + Unknown uint8 +} + +type LoungeActionRotation struct { + Z float32 +} + +type LoungeActionPosition struct { + X, Y, Z float32 +} + +type LoungeAction struct { + ActionType byte + Rotation *LoungeActionRotation `struct-if:"ActionType == 0"` + PositionAbs *LoungeActionRotation `struct-if:"ActionType == 4"` + PositionRel *LoungeActionRotation `struct-if:"ActionType == 6"` + Emote *common.PString `struct-if:"ActionType == 7"` + Departure *uint32 `struct-if:"ActionType == 8"` +} + +// ClientRoomLoungeAction +type ClientRoomLoungeAction struct { + ClientMessage_ + LoungeAction +} + +// ClientRequestMessengerList is a message sent to request the current +// list of message servers. +type ClientRequestMessengerList struct { + ClientMessage_ +} + +// ClientGetUserData is a message sent by the client to request +// the client state. +type ClientGetUserData struct { + ClientMessage_ + UserID uint32 + Request byte +} + +// ClientException is a message sent when the client encounters an +// error. +type ClientException struct { + ClientMessage_ + Empty byte + Message common.PString +} + +// Client009C is an unknown message. +type Client009C struct { + ClientMessage_ +} + +// ClientTutorialClear is an unknown message. +type ClientTutorialClear struct { + ClientMessage_ +} + +// ClientShopJoin is an unknown message. +type ClientShopJoin struct { + ClientMessage_ +} + +// ClientUserMacrosSet is a message sent to set the user's macros. +type ClientUserMacrosSet struct { + ClientMessage_ + MacroList pangya.MacroList +} + +// ClientMultiplayerJoin is the message sent when joining multiplayer. +type ClientMultiplayerJoin struct { + ClientMessage_ +} + +// ClientMultiplayerLeave is sent when the client exits multiplayer mode. +type ClientMultiplayerLeave struct { + ClientMessage_ +} + +// ClientEventLobbyJoin is the message sent when joining the event lobby. +type ClientEventLobbyJoin struct { + ClientMessage_ +} + +// ClientEventLobbyLeave is sent when the client exits the event lobby. +type ClientEventLobbyLeave struct { + ClientMessage_ +} + +// ClientAssistModeToggle is sent when assist mode is toggled. +type ClientAssistModeToggle struct { + ClientMessage_ +} + +// Client0088 is an unknown message. +type Client0088 struct { + ClientMessage_ +} + +// ClientRequestInboxList is the message sent to request the inbox. +type ClientRequestInboxList struct { + ClientMessage_ + PageNum uint32 +} + +// ClientRequestInboxMessage is sent by the client to retrieve a message from the inbox. +type ClientRequestInboxMessage struct { + ClientMessage_ + MessageID uint32 +} + +// ClientRequestDailyReward is the message sent to request the daily +// reward. +type ClientRequestDailyReward struct { + ClientMessage_ +} + +type Client00FE struct { + ClientMessage_ +} diff --git a/game/msgserver.go b/game/msgserver.go new file mode 100755 index 0000000..24dc8b3 --- /dev/null +++ b/game/msgserver.go @@ -0,0 +1,666 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package game + +import ( + "github.com/pangbox/server/common" + "github.com/pangbox/server/pangya" +) + +var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ + 0x0040: &ServerGlobalEvent{}, + 0x0044: &ServerUserData{}, + 0x0046: &ServerUserCensus{}, + 0x0047: &ServerRoomList{}, + 0x0048: &ServerRoomCensus{}, + 0x0049: &ServerRoomJoin{}, + 0x004A: &ServerRoomStatus{}, + 0x004B: &ServerRoomEquipmentData{}, + 0x004C: &ServerRoomLeave{}, + 0x004E: &Server004E{}, + 0x0052: &ServerRoomGameData{}, + 0x0070: &ServerCharData{}, + 0x0076: &ServerGameInit{}, + 0x0078: &ServerPlayerReady{}, + 0x0095: &ServerMoneyUpdate{}, + 0x009F: &ServerChannelList{}, + 0x00A1: &ServerUserInfo{}, + 0x00C4: &ServerRoomLoungeAction{}, + 0x00C8: &ServerPangPurchaseData{}, + 0x00F1: &ServerMessageConnect{}, + 0x00F5: &ServerMultiplayerJoined{}, + 0x00F6: &ServerMultiplayerLeft{}, + 0x010E: &Server010E{}, + 0x011F: &ServerTutorialStatus{}, + 0x01F6: &Server01F6{}, + 0x0210: &ServerInboxNotify{}, + 0x0211: &ServerInboxList{}, + 0x0212: &ServerMailMessage{}, + 0x0216: &ServerUserStatusUpdate{}, + 0x021D: &ServerAchievementProgress{}, + 0x0230: &Server0230{}, + 0x0231: &Server0231{}, + 0x0248: &ServerLoginBonusStatus{}, + 0x0250: &ServerEventLobbyJoined{}, + 0x0251: &ServerEventLobbyLeft{}, + 0x026A: &ServerAssistModeToggled{}, +}) + +// ConnectMessage is the message sent upon connecting. +type ConnectMessage struct { + Unknown [8]byte + Key byte +} + +// MessageDataType enumerates message data event types. +type MessageDataType byte + +const ( + ChatMessageData = 0 +) + +// GlobalChatMessage contains a global chat message +type GlobalChatMessage struct { + Nickname common.PString + Message common.PString +} + +// ServerGlobalEvent is a message that contains global chat events. +type ServerGlobalEvent struct { + ServerMessage_ + Type MessageDataType + Data GlobalChatMessage +} + +// ServerChannelList is a message that contains a list of all of the +// channels for a given server. Channels are isolated game zones within a region. +type ServerChannelList struct { + ServerMessage_ + Count byte `struct:"sizeof=Servers"` + Servers []pangya.ServerEntry +} + +// ServerUserData contains important state information. +type ServerUserData struct { + ServerMessage_ + Empty byte + ClientVersion common.PString + ServerVersion common.PString + Game uint16 + UserInfo pangya.UserInfo + PlayerStats pangya.PlayerStats + Unknown [78]byte + Items pangya.PlayerEquipment + JunkData [252 * 43]byte + EquippedCharacter pangya.PlayerCharacterData + EquippedCaddie pangya.PlayerCaddieData + EquippedClub pangya.PlayerClubData + EquippedMascot pangya.PlayerMascotData + Unknown2 [321]byte +} + +// ServerCharData contains the user's characters. +type ServerCharData struct { + ServerMessage_ + Count1 uint16 `struct:"sizeof=Characters"` + Count2 uint16 `struct:"sizeof=Characters"` + Characters []pangya.PlayerCharacterData +} + +type PlayerInfo struct { + Username string `struct:"[22]byte"` + Nickname string `struct:"[22]byte"` + GuildName string `struct:"[17]byte"` + GuildEmblemImage string `struct:"[12]byte"` + Unknown [71]byte + Flag byte + Unknown2 uint16 + Unknown3 uint16 + Unknown4 uint16 + Unknown5 uint16 + Unknown6 [16]byte + GlobalID string `struct:"[128]byte"` +} + +type PlayerGameInfo struct { + Stroke uint32 + Putt uint32 + Time uint32 + StrokeTime uint32 + Unknown float32 + Unknown2 uint32 + Unknown3 uint32 + Unknown4 uint32 + Unknown5 uint32 + Unknown6 uint32 + Unknown7 uint32 + Unknown8 uint16 + Unknown9 uint32 + Unknown10 uint32 + Unknown11 uint32 + Unknown12 uint32 + Unknown13 float32 + Unknown14 float32 + Unknown15 uint32 + Level byte + Pang uint64 + Unknown16 uint32 + Unknown17 [6]byte + Unknown18 [5]uint64 + Unknown19 uint64 + Unknown20 uint32 + Unknown21 uint32 + Unknown22 uint32 + Unknown23 uint32 + Unknown24 uint32 + Unknown25 uint32 + Unknown26 uint32 + Unknown27 uint32 + Unknown28 uint32 + Unknown29 uint32 + Unknown30 uint32 + Unknown31 uint32 + Unknown32 uint64 + Unknown33 uint32 + Unknown34 uint32 + Unknown35 uint32 + Unknown36 uint32 + Unknown37 uint32 + Unknown38 uint32 + Unknown39 uint16 + Unknown40 uint32 + Unknown41 uint32 + Unknown42 uint32 + Unknown43 uint32 + Unknown44 uint32 + Unknown45 uint32 + Unknown46 uint32 + Unknown47 uint32 + Unknown48 uint32 + Unknown49 uint16 +} + +type GamePlayer struct { + Number uint16 + Info PlayerInfo + Game PlayerGameInfo + Unknown [11430]byte + Character pangya.PlayerCharacterData + Caddie pangya.PlayerCaddieData + ClubSet pangya.PlayerClubData + Mascot pangya.PlayerMascotData + StartTime pangya.SystemTime + NumCards uint8 +} + +type ServerGameInit struct { + ServerMessage_ + Unknown byte + NumPlayers byte `struct:"sizeof=Players"` + Players []GamePlayer +} + +// ServerUserInfo contains requested user information. +type ServerUserInfo struct { + ServerMessage_ + ResponseCode uint8 + PlayerID uint32 + UserInfo pangya.UserInfo +} + +type ServerRoomLoungeAction struct { + ServerMessage_ + ConnID uint32 + LoungeAction +} + +// ServerPangPurchaseData is sent after a pang purchase succeeds. +type ServerPangPurchaseData struct { + ServerMessage_ + PangsRemaining uint64 + PangsSpent uint64 +} + +// ServerPlayerID is a message that contains the PlayerID and some +// other unknown data. +type ServerPlayerID struct { + ServerMessage_ + Empty byte + PlayerID uint32 + Unknown [239]byte +} + +// UserCensusType enumerates the types of census messages. +type UserCensusType byte + +const ( + UserAdd UserCensusType = 1 + UserRemove UserCensusType = 3 + UserListSet UserCensusType = 4 + UserListAppend UserCensusType = 5 +) + +const CensusMaxUsers = 36 + +type CensusUser struct { + UserID uint32 + ConnID uint32 + RoomNumber int16 + Nickname string `struct:"[22]byte"` + Rank byte + Unknown uint32 + Badge uint32 + Unknown2 uint32 + Unknown3 uint32 + Unknown4 byte + GuildEmblemID string `struct:"[19]byte"` + GlobalID string `struct:"[128]byte"` +} + +// ServerUserCensus contains information about users currently online in +// multiplayer +type ServerUserCensus struct { + ServerMessage_ + Type UserCensusType + Count uint8 `struct:"sizeof=UserList"` + UserList []CensusUser +} + +// ListType enumerates the types of room list messages. +type ListType byte + +const ( + ListSet ListType = 0 + ListAdd ListType = 1 + ListRemove ListType = 2 + ListChange ListType = 3 + + // Only valid for lounge mode rooms + ListLounge ListType = 7 +) + +type RoomListRoom struct { + Name string `struct:"[64]byte"` + Public bool `struct:"byte"` + Unknown uint16 + UserMax uint8 + UserCount uint8 + Unknown2 [18]byte + NumHoles uint8 + Number uint16 + HoleProgression uint8 + Course uint8 + ShotTimerMS uint32 + GameTimerMS uint32 + Flags uint32 + Unknown3 [76]byte + Unknown4 uint32 + Unknown5 uint32 + OwnerID uint32 + Class byte + ArtifactID uint32 + Unknown6 uint32 + EventNum uint32 + EventNumTop uint32 + EventShotTimerMS uint32 + Unknown7 uint32 +} + +// ServerRoomList contains information about rooms currently open in +// multiplayer. +type ServerRoomList struct { + ServerMessage_ + Count uint8 `struct:"sizeof=RoomList"` + Type ListType + Unknown uint16 + RoomList []RoomListRoom +} + +type RoomListUser struct { + ConnID uint32 + Nickname string `struct:"[22]byte"` + GuildName string `struct:"[20]byte"` + Slot uint8 + Flag uint32 + TitleID uint32 + CharTypeID uint32 + PortraitBGID uint32 + PortraitFrameID uint32 + PortraitStickerID uint32 + PortraitSlotID uint32 + SkinUnknown1 uint32 + SkinUnknown2 uint32 + Flag2 uint16 + Rank uint8 + Unknown uint8 + Unknown2 uint16 + GuildID uint32 + GuildEmblemImage string `struct:"[12]byte"` + GuildEmblemID uint8 + UserID uint32 + LoungeState uint32 + Unknown3 uint16 + Unknown4 uint32 + X float32 + Y float32 + Z float32 + Angle float32 + ShopUnknown uint32 + ShopName string `struct:"[64]byte"` + MascotTypeID uint32 + GlobalID string `struct:"[22]byte"` + Unknown5 [106]byte + Guest bool `struct:"byte"` + AverageScore float32 + Unknown6 [3]byte + UnknownMisalign byte // TODO: something either before or after here is misaligned + CharacterData pangya.PlayerCharacterData +} + +type RoomCensusListSet struct { + UserCount uint8 `struct:"sizeof=UserList"` + UserList []RoomListUser +} + +type RoomCensusListAdd struct { + User RoomListUser +} + +type RoomCensusListRemove struct { + ConnID uint32 +} + +type RoomCensusListChange struct { + ConnID uint32 + User RoomListUser +} + +// ServerRoomCensus reports on the users in a game room. +type ServerRoomCensus struct { + ServerMessage_ + Type byte + Unknown uint16 + ListSet *RoomCensusListSet `struct-if:"Type == 0"` + ListAdd *RoomCensusListAdd `struct-if:"Type == 1"` + ListRemove *RoomCensusListRemove `struct-if:"Type == 2"` + ListChange *RoomCensusListChange `struct-if:"Type == 3"` +} + +// ServerRoomStatus is sent when a room's settings or status changes. +type ServerRoomStatus struct { + ServerMessage_ + Unknown uint16 + RoomType byte + Course byte + NumHoles byte + HoleProgression byte + NaturalWind uint32 + MaxUsers byte + Unknown2 uint16 + ShotTimerMS uint32 + GameTimerMS uint32 + Flags uint32 + Owner bool `struct:"byte"` + RoomName common.PString +} + +type ServerRoomEquipmentData struct { + ServerMessage_ + Unknown []byte +} + +type ServerRoomLeave struct { + ServerMessage_ + RoomNumber uint16 +} + +type Server004E struct { + ServerMessage_ + Unknown []byte +} + +type HoleInfo struct { + HoleID uint32 + Pin uint8 + Course uint8 + Num uint8 +} + +type ServerRoomGameData struct { + ServerMessage_ + Course byte + Unknown byte + HoleProgression byte + NumHoles uint8 + Unknown2 uint32 + ShotTimerMS uint32 + GameTimerMS uint32 + Holes []HoleInfo `struct:"sizefrom=NumHoles"` + RandomSeed uint32 +} + +// ServerRoomJoin is sent when a room is joined. +type ServerRoomJoin struct { + ServerMessage_ + Status byte + Unknown byte + RoomName string `struct:"[64]byte"` + Unknown2 [25]byte + RoomNumber uint16 + Unknown3 [111]byte + EventNumber uint32 + Unknown4 [12]byte +} + +type ServerPlayerReady struct { + ServerMessage_ + ConnID uint32 + State byte +} + +type MoneyUpdateType uint16 + +const ( + MoneyUpdateRewardUnknown MoneyUpdateType = 2 + MoneyUpdatePangBalance MoneyUpdateType = 273 +) + +type UpdateRewardUnknownData struct { + Unknown uint16 +} + +type UpdatePangBalanceData struct { + Unknown uint32 + PangAmount uint32 + Unknown2 uint32 +} + +type ServerMoneyUpdate struct { + ServerMessage_ + Type uint16 + + RewardUnknown *UpdateRewardUnknownData `struct-if:"Type == 2"` + PangBalance *UpdatePangBalanceData `struct-if:"Type == 273"` +} + +// ServerMessageConnect seems to make the client connect to the message server. +// TODO: need to do more reverse engineering effort +type ServerMessageConnect struct { + ServerMessage_ + Unknown byte +} + +type ServerMultiplayerJoined struct { + ServerMessage_ +} + +type ServerMultiplayerLeft struct { + ServerMessage_ +} + +type Server010E struct { + ServerMessage_ + Unknown []byte +} + +type ServerTutorialStatus struct { + ServerMessage_ + Unknown [6]byte +} + +type Server01F6 struct { + ServerMessage_ + Unknown []byte +} + +// ServerInboxNotify is unimplemented. +type ServerInboxNotify struct { + ServerMessage_ + Unknown []byte +} + +type MessageAttachment struct { + ID uint32 + ItemID uint32 + Unknown byte + ItemQuantity uint32 + Unknown2 uint32 + Unknown3 uint64 + Unknown5 uint64 + Unknown6 uint32 + Unknown7 uint32 + Unknown8 [12]byte + Unknown10 uint16 +} + +type InboxMessage struct { + ID uint32 + SenderNickname string `struct:"[30]byte"` + Message string `struct:"[80]byte"` + Unknown [18]byte + Unknown2 uint32 + Unknown3 byte + AttachmentCount uint32 `struct:"sizeof=Attachments"` + Attachments []MessageAttachment +} + +type ServerInboxList struct { + ServerMessage_ + Status uint32 + PageNum uint32 + NumPages uint32 + NumMessages uint32 `struct:"sizeof=Messages"` + Messages []InboxMessage +} + +type MailMessage struct { + ID uint32 + SenderNickname common.PString + DateTime common.PString + Message common.PString + Unknown byte + AttachmentCount uint32 `struct:"sizeof=Attachments"` + Attachments []MessageAttachment +} + +type ServerMailMessage struct { + ServerMessage_ + Status uint32 + Message MailMessage +} + +type UserStatusChangeValue struct { + StatusID uint32 + StatusSlot uint32 + Unknown uint32 + StatusAmountOld uint32 + StatusAmountNew uint32 + StatusAmountDelta int32 + Unknown2 [25]byte +} + +type UserStatusChangeMastery struct { + CharacterID uint32 + StatusSlot uint32 + Unknown [16]byte + MasteryPowerUpCount uint16 + MasteryControlUpCount uint16 + MasteryImpactUpCount uint16 + MasterySpinUpCount uint16 + MasteryCurveUpCount uint16 + Unknown2 [16]byte +} + +type UserStatusChange204 struct { + Unknown [72]byte +} + +type UserStatusChange struct { + StatusChangeType byte + Value *UserStatusChangeValue `struct-if:"StatusChangeType == 2"` + Mastery *UserStatusChangeMastery `struct-if:"StatusChangeType == 201"` + Unknown204 *UserStatusChange204 `struct-if:"StatusChangeType == 204"` +} + +type ServerUserStatusUpdate struct { + ServerMessage_ + DateTimeUnix uint32 + Count uint32 + Changes []UserStatusChange +} + +type AchievementProgress struct { + Unknown byte + StatusID uint32 + StatusSlot uint32 + Value uint32 +} + +type ServerAchievementProgress struct { + ServerMessage_ + Status uint32 + Remaining uint32 + Count uint32 `struct:"sizeof=Achievements"` + Achievements []AchievementProgress +} + +type Server0230 struct { + ServerMessage_ +} + +type Server0231 struct { + ServerMessage_ +} + +type ServerLoginBonusStatus struct { + ServerMessage_ + Unknown []byte +} + +type ServerEventLobbyJoined struct { + ServerMessage_ +} + +type ServerEventLobbyLeft struct { + ServerMessage_ +} + +type ServerAssistModeToggled struct { + ServerMessage_ + Unknown uint32 +} diff --git a/game/server.go b/game/server.go new file mode 100755 index 0000000..779f488 --- /dev/null +++ b/game/server.go @@ -0,0 +1,82 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package game + +import ( + "context" + "net" + + "github.com/pangbox/server/common" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + "github.com/pangbox/server/pangya/iff" + log "github.com/sirupsen/logrus" +) + +// Options specify the options used to construct the game server. +type Options struct { + TopologyClient topologypbconnect.TopologyServiceClient + AccountsService *accounts.Service + PangyaIFF *iff.Archive + ServerID uint32 + ChannelName string +} + +// Server provides an implementation of the PangYa game server. +type Server struct { + baseServer *common.BaseServer + topologyClient topologypbconnect.TopologyServiceClient + accountsService *accounts.Service + pangyaIFF *iff.Archive + serverID uint32 + channelName string +} + +// New creates a new instance of the game server. +func New(opts Options) *Server { + return &Server{ + baseServer: &common.BaseServer{}, + topologyClient: opts.TopologyClient, + accountsService: opts.AccountsService, + pangyaIFF: opts.PangyaIFF, + serverID: opts.ServerID, + channelName: opts.ChannelName, + } +} + +// Listen listens for connections on a given address and blocks indefinitely. +func (s *Server) Listen(ctx context.Context, addr string) error { + logger := log.WithField("server", "GameServer") + return s.baseServer.Listen(logger, addr, func(logger *log.Entry, socket net.Conn) error { + conn := Conn{ + ServerConn: common.ServerConn[ClientMessage, ServerMessage]{ + Socket: socket, + Log: logger, + ClientMsg: ClientMessageTable, + ServerMsg: ServerMessageTable, + }, + s: s, + } + return conn.Handle(ctx) + }) +} + +func (s *Server) Shutdown(shutdownCtx context.Context) error { + // TODO: Need to shut down connection threads. + return s.baseServer.Close() +} diff --git a/gen.go b/gen.go new file mode 100755 index 0000000..dcd513b --- /dev/null +++ b/gen.go @@ -0,0 +1,22 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package server + +//go:generate go run github.com/bufbuild/buf/cmd/buf generate +//go:generate go run github.com/kyleconroy/sqlc/cmd/sqlc generate --no-remote +//go:generate go run github.com/pangbox/server/cmd/minibox/lang/update -src cmd/minibox -out cmd/minibox/lang -locales en,jp diff --git a/gen/dbmodels/character.sql.go b/gen/dbmodels/character.sql.go new file mode 100644 index 0000000..f9f455c --- /dev/null +++ b/gen/dbmodels/character.sql.go @@ -0,0 +1,101 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 +// source: character.sql + +package dbmodels + +import ( + "context" +) + +const createCharacter = `-- name: CreateCharacter :one +INSERT INTO character ( + player_id, + character_type_id, + character_data +) VALUES ( + ?, ?, ? +) +RETURNING character_id, player_id, character_type_id, character_data +` + +type CreateCharacterParams struct { + PlayerID int64 + CharacterTypeID int64 + CharacterData []byte +} + +func (q *Queries) CreateCharacter(ctx context.Context, arg CreateCharacterParams) (Character, error) { + row := q.db.QueryRowContext(ctx, createCharacter, arg.PlayerID, arg.CharacterTypeID, arg.CharacterData) + var i Character + err := row.Scan( + &i.CharacterID, + &i.PlayerID, + &i.CharacterTypeID, + &i.CharacterData, + ) + return i, err +} + +const getCharacter = `-- name: GetCharacter :one +SELECT character_id, player_id, character_type_id, character_data FROM character +WHERE character_id = ? LIMIT 1 +` + +func (q *Queries) GetCharacter(ctx context.Context, characterID int64) (Character, error) { + row := q.db.QueryRowContext(ctx, getCharacter, characterID) + var i Character + err := row.Scan( + &i.CharacterID, + &i.PlayerID, + &i.CharacterTypeID, + &i.CharacterData, + ) + return i, err +} + +const getCharactersByPlayer = `-- name: GetCharactersByPlayer :many +SELECT character_id, player_id, character_type_id, character_data FROM character +WHERE player_id = ? +` + +func (q *Queries) GetCharactersByPlayer(ctx context.Context, playerID int64) ([]Character, error) { + rows, err := q.db.QueryContext(ctx, getCharactersByPlayer, playerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Character + for rows.Next() { + var i Character + if err := rows.Scan( + &i.CharacterID, + &i.PlayerID, + &i.CharacterTypeID, + &i.CharacterData, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const playerHasCharacters = `-- name: PlayerHasCharacters :one +SELECT count(*) > 0 FROM character +WHERE player_id = ? +` + +func (q *Queries) PlayerHasCharacters(ctx context.Context, playerID int64) (bool, error) { + row := q.db.QueryRowContext(ctx, playerHasCharacters, playerID) + var column_1 bool + err := row.Scan(&column_1) + return column_1, err +} diff --git a/gen/dbmodels/db.go b/gen/dbmodels/db.go new file mode 100644 index 0000000..8f066c1 --- /dev/null +++ b/gen/dbmodels/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 + +package dbmodels + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/gen/dbmodels/models.go b/gen/dbmodels/models.go new file mode 100644 index 0000000..dcb3b4e --- /dev/null +++ b/gen/dbmodels/models.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 + +package dbmodels + +import ( + "database/sql" +) + +type Character struct { + CharacterID int64 + PlayerID int64 + CharacterTypeID int64 + CharacterData []byte +} + +type Player struct { + PlayerID int64 + Username string + Nickname sql.NullString + PasswordHash string + Pang int64 + Rank int64 +} + +type Session struct { + SessionID int64 + PlayerID int64 + SessionKey string + SessionAddress string + SessionExpiresAt int64 +} diff --git a/gen/dbmodels/player.sql.go b/gen/dbmodels/player.sql.go new file mode 100644 index 0000000..bdb142f --- /dev/null +++ b/gen/dbmodels/player.sql.go @@ -0,0 +1,103 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 +// source: player.sql + +package dbmodels + +import ( + "context" + "database/sql" +) + +const createPlayer = `-- name: CreatePlayer :one +INSERT INTO player ( + username, + nickname, + password_hash +) VALUES ( + ?, ?, ? +) +RETURNING player_id, username, nickname, password_hash, pang, rank +` + +type CreatePlayerParams struct { + Username string + Nickname sql.NullString + PasswordHash string +} + +func (q *Queries) CreatePlayer(ctx context.Context, arg CreatePlayerParams) (Player, error) { + row := q.db.QueryRowContext(ctx, createPlayer, arg.Username, arg.Nickname, arg.PasswordHash) + var i Player + err := row.Scan( + &i.PlayerID, + &i.Username, + &i.Nickname, + &i.PasswordHash, + &i.Pang, + &i.Rank, + ) + return i, err +} + +const getPlayer = `-- name: GetPlayer :one +SELECT player_id, username, nickname, password_hash, pang, rank FROM player +WHERE player_id = ? LIMIT 1 +` + +func (q *Queries) GetPlayer(ctx context.Context, playerID int64) (Player, error) { + row := q.db.QueryRowContext(ctx, getPlayer, playerID) + var i Player + err := row.Scan( + &i.PlayerID, + &i.Username, + &i.Nickname, + &i.PasswordHash, + &i.Pang, + &i.Rank, + ) + return i, err +} + +const getPlayerByUsername = `-- name: GetPlayerByUsername :one +SELECT player_id, username, nickname, password_hash, pang, rank FROM player +WHERE username = ? LIMIT 1 +` + +func (q *Queries) GetPlayerByUsername(ctx context.Context, username string) (Player, error) { + row := q.db.QueryRowContext(ctx, getPlayerByUsername, username) + var i Player + err := row.Scan( + &i.PlayerID, + &i.Username, + &i.Nickname, + &i.PasswordHash, + &i.Pang, + &i.Rank, + ) + return i, err +} + +const setPlayerNickname = `-- name: SetPlayerNickname :one +UPDATE player SET nickname = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, rank +` + +type SetPlayerNicknameParams struct { + Nickname sql.NullString + PlayerID int64 +} + +func (q *Queries) SetPlayerNickname(ctx context.Context, arg SetPlayerNicknameParams) (Player, error) { + row := q.db.QueryRowContext(ctx, setPlayerNickname, arg.Nickname, arg.PlayerID) + var i Player + err := row.Scan( + &i.PlayerID, + &i.Username, + &i.Nickname, + &i.PasswordHash, + &i.Pang, + &i.Rank, + ) + return i, err +} diff --git a/gen/dbmodels/session.sql.go b/gen/dbmodels/session.sql.go new file mode 100644 index 0000000..4946977 --- /dev/null +++ b/gen/dbmodels/session.sql.go @@ -0,0 +1,148 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.18.0 +// source: session.sql + +package dbmodels + +import ( + "context" +) + +const createSession = `-- name: CreateSession :one +INSERT INTO session ( + player_id, + session_key, + session_address, + session_expires_at +) VALUES ( + ?, ?, ?, ? +) +RETURNING session_id, player_id, session_key, session_address, session_expires_at +` + +type CreateSessionParams struct { + PlayerID int64 + SessionKey string + SessionAddress string + SessionExpiresAt int64 +} + +func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { + row := q.db.QueryRowContext(ctx, createSession, + arg.PlayerID, + arg.SessionKey, + arg.SessionAddress, + arg.SessionExpiresAt, + ) + var i Session + err := row.Scan( + &i.SessionID, + &i.PlayerID, + &i.SessionKey, + &i.SessionAddress, + &i.SessionExpiresAt, + ) + return i, err +} + +const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec +DELETE FROM session WHERE session_expires_at < ? +` + +func (q *Queries) DeleteExpiredSessions(ctx context.Context, sessionExpiresAt int64) error { + _, err := q.db.ExecContext(ctx, deleteExpiredSessions, sessionExpiresAt) + return err +} + +const getSession = `-- name: GetSession :one +SELECT session_id, player_id, session_key, session_address, session_expires_at FROM session +WHERE session_id = ? LIMIT 1 +` + +func (q *Queries) GetSession(ctx context.Context, sessionID int64) (Session, error) { + row := q.db.QueryRowContext(ctx, getSession, sessionID) + var i Session + err := row.Scan( + &i.SessionID, + &i.PlayerID, + &i.SessionKey, + &i.SessionAddress, + &i.SessionExpiresAt, + ) + return i, err +} + +const getSessionByKey = `-- name: GetSessionByKey :one +SELECT session_id, player_id, session_key, session_address, session_expires_at FROM session +WHERE session_key = ? LIMIT 1 +` + +func (q *Queries) GetSessionByKey(ctx context.Context, sessionKey string) (Session, error) { + row := q.db.QueryRowContext(ctx, getSessionByKey, sessionKey) + var i Session + err := row.Scan( + &i.SessionID, + &i.PlayerID, + &i.SessionKey, + &i.SessionAddress, + &i.SessionExpiresAt, + ) + return i, err +} + +const getSessionsByPlayer = `-- name: GetSessionsByPlayer :many +SELECT session_id, player_id, session_key, session_address, session_expires_at FROM session +WHERE player_id = ? +` + +func (q *Queries) GetSessionsByPlayer(ctx context.Context, playerID int64) ([]Session, error) { + rows, err := q.db.QueryContext(ctx, getSessionsByPlayer, playerID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Session + for rows.Next() { + var i Session + if err := rows.Scan( + &i.SessionID, + &i.PlayerID, + &i.SessionKey, + &i.SessionAddress, + &i.SessionExpiresAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateSessionExpiry = `-- name: UpdateSessionExpiry :one +UPDATE session SET session_expires_at = ? WHERE session_id = ? RETURNING session_id, player_id, session_key, session_address, session_expires_at +` + +type UpdateSessionExpiryParams struct { + SessionExpiresAt int64 + SessionID int64 +} + +func (q *Queries) UpdateSessionExpiry(ctx context.Context, arg UpdateSessionExpiryParams) (Session, error) { + row := q.db.QueryRowContext(ctx, updateSessionExpiry, arg.SessionExpiresAt, arg.SessionID) + var i Session + err := row.Scan( + &i.SessionID, + &i.PlayerID, + &i.SessionKey, + &i.SessionAddress, + &i.SessionExpiresAt, + ) + return i, err +} diff --git a/gen/proto/go/topologypb/topology.pb.go b/gen/proto/go/topologypb/topology.pb.go new file mode 100644 index 0000000..8de67f6 --- /dev/null +++ b/gen/proto/go/topologypb/topology.pb.go @@ -0,0 +1,841 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc (unknown) +// source: topologypb/topology.proto + +package topologypb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Server_Type int32 + +const ( + Server_TYPE_UNSPECIFIED Server_Type = 0 + Server_TYPE_LOGIN_SERVER Server_Type = 1 + Server_TYPE_GAME_SERVER Server_Type = 2 + Server_TYPE_MESSAGE_SERVER Server_Type = 3 +) + +// Enum value maps for Server_Type. +var ( + Server_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "TYPE_LOGIN_SERVER", + 2: "TYPE_GAME_SERVER", + 3: "TYPE_MESSAGE_SERVER", + } + Server_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "TYPE_LOGIN_SERVER": 1, + "TYPE_GAME_SERVER": 2, + "TYPE_MESSAGE_SERVER": 3, + } +) + +func (x Server_Type) Enum() *Server_Type { + p := new(Server_Type) + *p = x + return p +} + +func (x Server_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Server_Type) Descriptor() protoreflect.EnumDescriptor { + return file_topologypb_topology_proto_enumTypes[0].Descriptor() +} + +func (Server_Type) Type() protoreflect.EnumType { + return &file_topologypb_topology_proto_enumTypes[0] +} + +func (x Server_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Server_Type.Descriptor instead. +func (Server_Type) EnumDescriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{2, 0} +} + +// ServerEntry is the internal server entry used for storage. +type ServerEntry struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server *Server `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` + LastPing *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_ping,json=lastPing,proto3" json:"last_ping,omitempty"` + LastHealthy *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_healthy,json=lastHealthy,proto3" json:"last_healthy,omitempty"` +} + +func (x *ServerEntry) Reset() { + *x = ServerEntry{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerEntry) ProtoMessage() {} + +func (x *ServerEntry) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerEntry.ProtoReflect.Descriptor instead. +func (*ServerEntry) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{0} +} + +func (x *ServerEntry) GetServer() *Server { + if x != nil { + return x.Server + } + return nil +} + +func (x *ServerEntry) GetLastPing() *timestamppb.Timestamp { + if x != nil { + return x.LastPing + } + return nil +} + +func (x *ServerEntry) GetLastHealthy() *timestamppb.Timestamp { + if x != nil { + return x.LastHealthy + } + return nil +} + +// Configuration stores static server configuration. +type Configuration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Servers []*Server `protobuf:"bytes,1,rep,name=servers,proto3" json:"servers,omitempty"` +} + +func (x *Configuration) Reset() { + *x = Configuration{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Configuration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Configuration) ProtoMessage() {} + +func (x *Configuration) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Configuration.ProtoReflect.Descriptor instead. +func (*Configuration) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{1} +} + +func (x *Configuration) GetServers() []*Server { + if x != nil { + return x.Servers + } + return nil +} + +// Server is the server data provided by a node. +type Server struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type Server_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Server_Type" json:"type,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Id uint32 `protobuf:"varint,3,opt,name=id,proto3" json:"id,omitempty"` + NumUsers uint32 `protobuf:"varint,4,opt,name=num_users,json=numUsers,proto3" json:"num_users,omitempty"` + MaxUsers uint32 `protobuf:"varint,5,opt,name=max_users,json=maxUsers,proto3" json:"max_users,omitempty"` + Address string `protobuf:"bytes,6,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,7,opt,name=port,proto3" json:"port,omitempty"` + Flags uint32 `protobuf:"varint,8,opt,name=flags,proto3" json:"flags,omitempty"` +} + +func (x *Server) Reset() { + *x = Server{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Server) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Server) ProtoMessage() {} + +func (x *Server) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Server.ProtoReflect.Descriptor instead. +func (*Server) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{2} +} + +func (x *Server) GetType() Server_Type { + if x != nil { + return x.Type + } + return Server_TYPE_UNSPECIFIED +} + +func (x *Server) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Server) GetId() uint32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Server) GetNumUsers() uint32 { + if x != nil { + return x.NumUsers + } + return 0 +} + +func (x *Server) GetMaxUsers() uint32 { + if x != nil { + return x.MaxUsers + } + return 0 +} + +func (x *Server) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *Server) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *Server) GetFlags() uint32 { + if x != nil { + return x.Flags + } + return 0 +} + +type AddServerRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server *Server `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` +} + +func (x *AddServerRequest) Reset() { + *x = AddServerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddServerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddServerRequest) ProtoMessage() {} + +func (x *AddServerRequest) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddServerRequest.ProtoReflect.Descriptor instead. +func (*AddServerRequest) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{3} +} + +func (x *AddServerRequest) GetServer() *Server { + if x != nil { + return x.Server + } + return nil +} + +type AddServerResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AddServerResponse) Reset() { + *x = AddServerResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddServerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddServerResponse) ProtoMessage() {} + +func (x *AddServerResponse) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddServerResponse.ProtoReflect.Descriptor instead. +func (*AddServerResponse) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{4} +} + +type ListServersRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type Server_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Server_Type" json:"type,omitempty"` +} + +func (x *ListServersRequest) Reset() { + *x = ListServersRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListServersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListServersRequest) ProtoMessage() {} + +func (x *ListServersRequest) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListServersRequest.ProtoReflect.Descriptor instead. +func (*ListServersRequest) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{5} +} + +func (x *ListServersRequest) GetType() Server_Type { + if x != nil { + return x.Type + } + return Server_TYPE_UNSPECIFIED +} + +type ListServersResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server []*Server `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` +} + +func (x *ListServersResponse) Reset() { + *x = ListServersResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListServersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListServersResponse) ProtoMessage() {} + +func (x *ListServersResponse) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListServersResponse.ProtoReflect.Descriptor instead. +func (*ListServersResponse) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{6} +} + +func (x *ListServersResponse) GetServer() []*Server { + if x != nil { + return x.Server + } + return nil +} + +type GetServerRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *GetServerRequest) Reset() { + *x = GetServerRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetServerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServerRequest) ProtoMessage() {} + +func (x *GetServerRequest) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetServerRequest.ProtoReflect.Descriptor instead. +func (*GetServerRequest) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{7} +} + +func (x *GetServerRequest) GetId() uint32 { + if x != nil { + return x.Id + } + return 0 +} + +type GetServerResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server *Server `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"` +} + +func (x *GetServerResponse) Reset() { + *x = GetServerResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_topologypb_topology_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetServerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetServerResponse) ProtoMessage() {} + +func (x *GetServerResponse) ProtoReflect() protoreflect.Message { + mi := &file_topologypb_topology_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetServerResponse.ProtoReflect.Descriptor instead. +func (*GetServerResponse) Descriptor() ([]byte, []int) { + return file_topologypb_topology_proto_rawDescGZIP(), []int{8} +} + +func (x *GetServerResponse) GetServer() *Server { + if x != nil { + return x.Server + } + return nil +} + +var File_topologypb_topology_proto protoreflect.FileDescriptor + +var file_topologypb_topology_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x74, 0x6f, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x70, 0x62, 0x2f, 0x74, 0x6f, 0x70, + 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa6, 0x01, 0x0a, + 0x0b, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1f, 0x0a, 0x06, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x37, 0x0a, + 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, 0x61, + 0x73, 0x74, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x3d, 0x0a, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x68, + 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x48, 0x65, + 0x61, 0x6c, 0x74, 0x68, 0x79, 0x22, 0x32, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x22, 0xb0, 0x02, 0x0a, 0x06, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x75, + 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6e, + 0x75, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x75, + 0x73, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x55, + 0x73, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, + 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x22, 0x62, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x14, 0x0a, 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4c, + 0x4f, 0x47, 0x49, 0x4e, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x01, 0x12, 0x14, 0x0a, + 0x10, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x41, 0x4d, 0x45, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, + 0x52, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x45, 0x53, 0x53, + 0x41, 0x47, 0x45, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 0x10, 0x03, 0x22, 0x33, 0x0a, 0x10, + 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1f, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x07, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x22, 0x13, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x36, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x36, + 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x22, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x34, 0x0a, 0x11, 0x47, 0x65, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1f, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x07, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x32, 0xb3, 0x01, 0x0a, 0x0f, 0x54, 0x6f, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x32, 0x0a, 0x09, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x12, 0x11, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x41, 0x64, 0x64, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x13, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x32, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, + 0x11, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x61, 0x6e, 0x67, 0x62, 0x6f, 0x78, 0x2f, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, + 0x2f, 0x74, 0x6f, 0x70, 0x6f, 0x6c, 0x6f, 0x67, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_topologypb_topology_proto_rawDescOnce sync.Once + file_topologypb_topology_proto_rawDescData = file_topologypb_topology_proto_rawDesc +) + +func file_topologypb_topology_proto_rawDescGZIP() []byte { + file_topologypb_topology_proto_rawDescOnce.Do(func() { + file_topologypb_topology_proto_rawDescData = protoimpl.X.CompressGZIP(file_topologypb_topology_proto_rawDescData) + }) + return file_topologypb_topology_proto_rawDescData +} + +var file_topologypb_topology_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_topologypb_topology_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_topologypb_topology_proto_goTypes = []interface{}{ + (Server_Type)(0), // 0: Server.Type + (*ServerEntry)(nil), // 1: ServerEntry + (*Configuration)(nil), // 2: Configuration + (*Server)(nil), // 3: Server + (*AddServerRequest)(nil), // 4: AddServerRequest + (*AddServerResponse)(nil), // 5: AddServerResponse + (*ListServersRequest)(nil), // 6: ListServersRequest + (*ListServersResponse)(nil), // 7: ListServersResponse + (*GetServerRequest)(nil), // 8: GetServerRequest + (*GetServerResponse)(nil), // 9: GetServerResponse + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp +} +var file_topologypb_topology_proto_depIdxs = []int32{ + 3, // 0: ServerEntry.server:type_name -> Server + 10, // 1: ServerEntry.last_ping:type_name -> google.protobuf.Timestamp + 10, // 2: ServerEntry.last_healthy:type_name -> google.protobuf.Timestamp + 3, // 3: Configuration.servers:type_name -> Server + 0, // 4: Server.type:type_name -> Server.Type + 3, // 5: AddServerRequest.server:type_name -> Server + 0, // 6: ListServersRequest.type:type_name -> Server.Type + 3, // 7: ListServersResponse.server:type_name -> Server + 3, // 8: GetServerResponse.server:type_name -> Server + 4, // 9: TopologyService.AddServer:input_type -> AddServerRequest + 6, // 10: TopologyService.ListServers:input_type -> ListServersRequest + 8, // 11: TopologyService.GetServer:input_type -> GetServerRequest + 5, // 12: TopologyService.AddServer:output_type -> AddServerResponse + 7, // 13: TopologyService.ListServers:output_type -> ListServersResponse + 9, // 14: TopologyService.GetServer:output_type -> GetServerResponse + 12, // [12:15] is the sub-list for method output_type + 9, // [9:12] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_topologypb_topology_proto_init() } +func file_topologypb_topology_proto_init() { + if File_topologypb_topology_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_topologypb_topology_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ServerEntry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Configuration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Server); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddServerRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddServerResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListServersRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListServersResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetServerRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_topologypb_topology_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetServerResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_topologypb_topology_proto_rawDesc, + NumEnums: 1, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_topologypb_topology_proto_goTypes, + DependencyIndexes: file_topologypb_topology_proto_depIdxs, + EnumInfos: file_topologypb_topology_proto_enumTypes, + MessageInfos: file_topologypb_topology_proto_msgTypes, + }.Build() + File_topologypb_topology_proto = out.File + file_topologypb_topology_proto_rawDesc = nil + file_topologypb_topology_proto_goTypes = nil + file_topologypb_topology_proto_depIdxs = nil +} diff --git a/gen/proto/go/topologypb/topologypbconnect/topology.connect.go b/gen/proto/go/topologypb/topologypbconnect/topology.connect.go new file mode 100644 index 0000000..d469bd7 --- /dev/null +++ b/gen/proto/go/topologypb/topologypbconnect/topology.connect.go @@ -0,0 +1,166 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: topologypb/topology.proto + +package topologypbconnect + +import ( + context "context" + errors "errors" + connect_go "github.com/bufbuild/connect-go" + topologypb "github.com/pangbox/server/gen/proto/go/topologypb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect_go.IsAtLeastVersion0_1_0 + +const ( + // TopologyServiceName is the fully-qualified name of the TopologyService service. + TopologyServiceName = "TopologyService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // TopologyServiceAddServerProcedure is the fully-qualified name of the TopologyService's AddServer + // RPC. + TopologyServiceAddServerProcedure = "/TopologyService/AddServer" + // TopologyServiceListServersProcedure is the fully-qualified name of the TopologyService's + // ListServers RPC. + TopologyServiceListServersProcedure = "/TopologyService/ListServers" + // TopologyServiceGetServerProcedure is the fully-qualified name of the TopologyService's GetServer + // RPC. + TopologyServiceGetServerProcedure = "/TopologyService/GetServer" +) + +// TopologyServiceClient is a client for the TopologyService service. +type TopologyServiceClient interface { + AddServer(context.Context, *connect_go.Request[topologypb.AddServerRequest]) (*connect_go.Response[topologypb.AddServerResponse], error) + ListServers(context.Context, *connect_go.Request[topologypb.ListServersRequest]) (*connect_go.Response[topologypb.ListServersResponse], error) + GetServer(context.Context, *connect_go.Request[topologypb.GetServerRequest]) (*connect_go.Response[topologypb.GetServerResponse], error) +} + +// NewTopologyServiceClient constructs a client for the TopologyService service. By default, it uses +// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewTopologyServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) TopologyServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + return &topologyServiceClient{ + addServer: connect_go.NewClient[topologypb.AddServerRequest, topologypb.AddServerResponse]( + httpClient, + baseURL+TopologyServiceAddServerProcedure, + opts..., + ), + listServers: connect_go.NewClient[topologypb.ListServersRequest, topologypb.ListServersResponse]( + httpClient, + baseURL+TopologyServiceListServersProcedure, + opts..., + ), + getServer: connect_go.NewClient[topologypb.GetServerRequest, topologypb.GetServerResponse]( + httpClient, + baseURL+TopologyServiceGetServerProcedure, + opts..., + ), + } +} + +// topologyServiceClient implements TopologyServiceClient. +type topologyServiceClient struct { + addServer *connect_go.Client[topologypb.AddServerRequest, topologypb.AddServerResponse] + listServers *connect_go.Client[topologypb.ListServersRequest, topologypb.ListServersResponse] + getServer *connect_go.Client[topologypb.GetServerRequest, topologypb.GetServerResponse] +} + +// AddServer calls TopologyService.AddServer. +func (c *topologyServiceClient) AddServer(ctx context.Context, req *connect_go.Request[topologypb.AddServerRequest]) (*connect_go.Response[topologypb.AddServerResponse], error) { + return c.addServer.CallUnary(ctx, req) +} + +// ListServers calls TopologyService.ListServers. +func (c *topologyServiceClient) ListServers(ctx context.Context, req *connect_go.Request[topologypb.ListServersRequest]) (*connect_go.Response[topologypb.ListServersResponse], error) { + return c.listServers.CallUnary(ctx, req) +} + +// GetServer calls TopologyService.GetServer. +func (c *topologyServiceClient) GetServer(ctx context.Context, req *connect_go.Request[topologypb.GetServerRequest]) (*connect_go.Response[topologypb.GetServerResponse], error) { + return c.getServer.CallUnary(ctx, req) +} + +// TopologyServiceHandler is an implementation of the TopologyService service. +type TopologyServiceHandler interface { + AddServer(context.Context, *connect_go.Request[topologypb.AddServerRequest]) (*connect_go.Response[topologypb.AddServerResponse], error) + ListServers(context.Context, *connect_go.Request[topologypb.ListServersRequest]) (*connect_go.Response[topologypb.ListServersResponse], error) + GetServer(context.Context, *connect_go.Request[topologypb.GetServerRequest]) (*connect_go.Response[topologypb.GetServerResponse], error) +} + +// NewTopologyServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewTopologyServiceHandler(svc TopologyServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) { + mux := http.NewServeMux() + mux.Handle(TopologyServiceAddServerProcedure, connect_go.NewUnaryHandler( + TopologyServiceAddServerProcedure, + svc.AddServer, + opts..., + )) + mux.Handle(TopologyServiceListServersProcedure, connect_go.NewUnaryHandler( + TopologyServiceListServersProcedure, + svc.ListServers, + opts..., + )) + mux.Handle(TopologyServiceGetServerProcedure, connect_go.NewUnaryHandler( + TopologyServiceGetServerProcedure, + svc.GetServer, + opts..., + )) + return "/.TopologyService/", mux +} + +// UnimplementedTopologyServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedTopologyServiceHandler struct{} + +func (UnimplementedTopologyServiceHandler) AddServer(context.Context, *connect_go.Request[topologypb.AddServerRequest]) (*connect_go.Response[topologypb.AddServerResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("TopologyService.AddServer is not implemented")) +} + +func (UnimplementedTopologyServiceHandler) ListServers(context.Context, *connect_go.Request[topologypb.ListServersRequest]) (*connect_go.Response[topologypb.ListServersResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("TopologyService.ListServers is not implemented")) +} + +func (UnimplementedTopologyServiceHandler) GetServer(context.Context, *connect_go.Request[topologypb.GetServerRequest]) (*connect_go.Response[topologypb.GetServerResponse], error) { + return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("TopologyService.GetServer is not implemented")) +} diff --git a/go.mod b/go.mod new file mode 100755 index 0000000..909be38 --- /dev/null +++ b/go.mod @@ -0,0 +1,135 @@ +module github.com/pangbox/server + +go 1.20 + +require ( + github.com/bufbuild/buf v1.19.0 + github.com/bufbuild/connect-go v1.7.0 + github.com/davecgh/go-spew v1.1.1 + github.com/go-restruct/restruct v1.2.0-alpha + github.com/google/uuid v1.3.0 + github.com/jackc/pgx/v5 v5.3.1 + github.com/kyleconroy/sqlc v1.18.0 + github.com/lib/pq v1.10.9 + github.com/pangbox/pangcrypt v0.0.0-20181124232112-60117463a15d + github.com/pangbox/pangfiles v0.0.2-alpha.0.20230603175117-d8085775e1b5 + github.com/pkg/errors v0.9.1 + github.com/pressly/goose/v3 v3.11.2 + github.com/sirupsen/logrus v1.9.0 + github.com/stretchr/testify v1.8.2 + github.com/syndtr/goleveldb v1.0.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.20.1 + github.com/xo/dburl v0.14.2 + golang.org/x/crypto v0.9.0 + golang.org/x/net v0.10.0 + google.golang.org/protobuf v1.30.0 + modernc.org/sqlite v1.22.1 +) + +require ( + bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/akavel/rsrc v0.10.2 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 // indirect + github.com/benbjohnson/clock v1.3.3 // indirect + github.com/billziss-gh/cgofuse v1.5.0 // indirect + github.com/bufbuild/protocompile v0.5.1 // indirect + github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/containerd/containerd v1.6.19 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cubicdaiya/gonp v1.0.4 // indirect + github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect + github.com/docker/cli v23.0.6+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v23.0.6+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/fgprof v0.9.3 // indirect + github.com/go-chi/chi/v5 v5.0.8 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gofrs/uuid/v5 v5.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/google/go-containerregistry v0.15.1 // indirect + github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/josephspurrier/goversioninfo v1.4.0 // indirect + github.com/julienschmidt/httprouter v1.3.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.16.5 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/lxn/polyglot v0.0.0-20120605161256-3427b7be6a34 // indirect + github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 // indirect + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/patternmatcher v0.5.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/opencontainers/runc v1.1.7 // indirect + github.com/pangbox/rugburn v0.0.0-20230528190157-e19211ef1659 // indirect + github.com/pganalyze/pg_query_go/v4 v4.2.0 // indirect + github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect + github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 // indirect + github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rasky/go-lzo v0.0.0-20151023001055-affec0788321 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/riza-io/grpc-go v0.1.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rs/cors v1.9.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/testcontainers/testcontainers-go v0.20.1 // indirect + github.com/tetratelabs/wazero v1.1.0 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect + go.opentelemetry.io/otel v1.15.1 // indirect + go.opentelemetry.io/otel/sdk v1.15.1 // indirect + go.opentelemetry.io/otel/trace v1.15.1 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.9.1 // indirect + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + google.golang.org/grpc v1.54.0 // indirect + gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + lukechampine.com/uint128 v1.3.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100755 index 0000000..a3597dc --- /dev/null +++ b/go.sum @@ -0,0 +1,752 @@ +bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05 h1:UrYe9YkT4Wpm6D+zByEyCJQzDqTPXqTDUI7bZ41i9VE= +bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05/go.mod h1:h0h5FBYpXThbvSfTqthw+0I4nmHnhTHkO5BoOHsBWqg= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.9.7 h1:mKNHW/Xvv1aFH87Jb6ERDzXTJTLPlmzfZ28VBFD/bfg= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1 h1:X8MJ0fnN5FPdcGF5Ij2/OW+HgiJrRg3AfHAx1PJtIzM= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230321174746-8dcc6526cfb1/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.3 h1:g+rSsSaAzhHJYcIQE78hJ3AhyjjtQvleKDjlhdBnIhc= +github.com/benbjohnson/clock v1.3.3/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/billziss-gh/cgofuse v1.3.0 h1:mFj8XQg/vvxMFywNy1F7IqFYcMeBqceYTh1+iUhpsk8= +github.com/billziss-gh/cgofuse v1.3.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM= +github.com/billziss-gh/cgofuse v1.5.0 h1:kH516I/s+Ab4diL/Y/ayFeUjjA8ey+JK12xDfBf4HEs= +github.com/billziss-gh/cgofuse v1.5.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM= +github.com/bufbuild/buf v1.19.0 h1:8c/XL39hO2hGgKWgUnRT3bCc8KvMa+V1jpoWWdN2bsw= +github.com/bufbuild/buf v1.19.0/go.mod h1:KiNA2H6TdwkCYS7ZUQIpn4vKq/d7IjUprkgCK9Ik4hU= +github.com/bufbuild/connect-go v1.7.0 h1:MGp82v7SCza+3RhsVhV7aMikwxvI3ZfD72YiGt8FYJo= +github.com/bufbuild/connect-go v1.7.0/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk= +github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg= +github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40= +github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 h1:jP4sqm2PHgm3+eQ50zCoCdIyQFkIL/Rtkw6TT8OYPFI= +github.com/bytecodealliance/wasmtime-go/v8 v8.0.0/go.mod h1:tgazNLU7xSC2gfRAM8L4WyE+dgs5yp9FF5/tGebEQyM= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/containerd v1.6.19 h1:F0qgQPrG0P2JPgwpxWxYavrVeXAG0ezUIB9Z/4FTUAU= +github.com/containerd/containerd v1.6.19/go.mod h1:HZCDMn4v/Xl2579/MvtOC2M206i+JJ6VxFWU/NetrGY= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= +github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/cli v23.0.6+incompatible h1:CScadyCJ2ZKUDpAMZta6vK8I+6/m60VIjGIV7Wg/Eu4= +github.com/docker/cli v23.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v23.0.6+incompatible h1:aBD4np894vatVX99UTx/GyOUOK4uEcROwA3+bQhEcoU= +github.com/docker/docker v23.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= +github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= +github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M= +github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-containerregistry v0.15.1 h1:RsJ9NbfxYWF8Wl4VmvkpN3zYATwuvlPq2j20zmcs63E= +github.com/google/go-containerregistry v0.15.1/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de h1:6bMcLOeKoNo0+mTOb1ee3McF6CCKGixjLR3EDQY1Jik= +github.com/google/pprof v0.0.0-20230502171905-255e3b9b56de/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84 h1:2uT3aivO7NVpUPGcQX7RbHijHMyWix/yCnIrCWc+5co= +github.com/jdxcode/netrc v0.0.0-20221124155335-4616370d1a84/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= +github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kyleconroy/sqlc v1.18.0 h1:+a0Fc0sRCGCFyeSLiA9iKbHXufRGOWOqZjCdtfSgvL8= +github.com/kyleconroy/sqlc v1.18.0/go.mod h1:FfVkspjWAR6NHR9BwfwqRK0UvrrAq9vT1WGn8q+v9Ug= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lxn/polyglot v0.0.0-20120605161256-3427b7be6a34 h1:HoxaEYFKd1Vw/lvZp6WFt8M/ArvepjowSQeFOi238TE= +github.com/lxn/polyglot v0.0.0-20120605161256-3427b7be6a34/go.mod h1:9aD+HECjP98XuvEqwLeehq0tXwnzKcAksbcpd6ILZKg= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo= +github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk= +github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pangbox/pangcrypt v0.0.0-20181124232112-60117463a15d h1:WZa3yBhrPN1+6aFHkQhD5xbTttYwxVw0RsXG5lsJOj0= +github.com/pangbox/pangcrypt v0.0.0-20181124232112-60117463a15d/go.mod h1:5Bd6YEPYgUrM2LygkXw8oMERGUItIxlxdpnmuE+5pkM= +github.com/pangbox/pangfiles v0.0.1 h1:Zj9prIhbbMdgqHh7s8DBiM8nRUojpJlpt2dCKbmKkdI= +github.com/pangbox/pangfiles v0.0.1/go.mod h1:3AiPs/VZB0+xPxB2C5HVGPyUMVh2XoSybq9Bu3Z2ngE= +github.com/pangbox/pangfiles v0.0.2-alpha.0.20230603175117-d8085775e1b5 h1:afO/IX2XRr9hUaXoFm0LkEj99xM4KgSci7I0eaEK1Fc= +github.com/pangbox/pangfiles v0.0.2-alpha.0.20230603175117-d8085775e1b5/go.mod h1:OOPiXQJWNAvkaBIKWeUgG4t3hcqYeQxIJKJspS8BAJI= +github.com/pangbox/rugburn v0.0.0-20230528190157-e19211ef1659 h1:JqL1lkshTqE7jmA14REvJt8MNFYFXfXwCQiaunPF3Yk= +github.com/pangbox/rugburn v0.0.0-20230528190157-e19211ef1659/go.mod h1:UyrTSVBSBI+LC7gLCFTC38cqihsi0tl9ODNIRxrXVuU= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pganalyze/pg_query_go/v4 v4.2.0 h1:67hSBZXYfABNYisEu/Xfu6R2gupnQwaoRhQicy0HSnQ= +github.com/pganalyze/pg_query_go/v4 v4.2.0/go.mod h1:aEkDNOXNM5j0YGzaAapwJ7LB3dLNj+bvbWcLv1hOVqA= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM= +github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pingcap/log v0.0.0-20210906054005-afc726e70354 h1:SvWCbCPh1YeHd9yQLksvJYAgft6wLTY1aNG81tpyscQ= +github.com/pingcap/log v0.0.0-20210906054005-afc726e70354/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= +github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00 h1:aDC/yAGx/jPEyrX+UPKV3GWg+4A4yG8ifuP6jBEhDi0= +github.com/pingcap/tidb/parser v0.0.0-20220725134311-c80026e61f00/go.mod h1:wjvp+T3/T9XYt0nKqGX3Kc1AKuyUcfno6LTc6b2A6ew= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pressly/goose/v3 v3.11.2 h1:QgTP45FhBBHdmf7hWKlbWFHtwPtxo0phSDkwDKGUrYs= +github.com/pressly/goose/v3 v3.11.2/go.mod h1:LWQzSc4vwfHA/3B8getTp8g3J5Z8tFBxgxinmGlMlJk= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rasky/go-lzo v0.0.0-20151023001055-affec0788321 h1:lDAd4GuNYYOZE4wD9sET4eKTiCPS/Ginh3iwnYAqwxY= +github.com/rasky/go-lzo v0.0.0-20151023001055-affec0788321/go.mod h1:9leZcVcItj6m9/CfHY5Em/iBrCz7js8LcRQGTKEEv2M= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riza-io/grpc-go v0.1.0 h1:qm0j1YT0mqvtaGQ4A+sFe5odQSEE2OGnXGjTJAzQdUM= +github.com/riza-io/grpc-go v0.1.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= +github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/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= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/testcontainers/testcontainers-go v0.20.1 h1:mK15UPJ8c5P+NsQKmkqzs/jMdJt6JMs5vlw2y4j92c0= +github.com/testcontainers/testcontainers-go v0.20.1/go.mod h1:zb+NOlCQBkZ7RQp4QI+YMIHyO2CQ/qsXzNF5eLJ24SY= +github.com/testcontainers/testcontainers-go/modules/postgres v0.20.1 h1:PkAq2/sxchYxLiepcshIUnMzmhlecakGOCTtKEuZCA0= +github.com/testcontainers/testcontainers-go/modules/postgres v0.20.1/go.mod h1:c9mDiyvz7se25wEvvkx/8ok1YIIsQE9ACItnim7C0xw= +github.com/tetratelabs/wazero v1.1.0 h1:EByoAhC+QcYpwSZJSs/aV0uokxPwBgKxfiokSUwAknQ= +github.com/tetratelabs/wazero v1.1.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xo/dburl v0.14.2 h1:tqiXv1glyxFph3LA39RXE4TYidr/yp7kG2YDrgJVjiA= +github.com/xo/dburl v0.14.2/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel v1.15.1 h1:3Iwq3lfRByPaws0f6bU3naAqOR1n5IeDWd9390kWHa8= +go.opentelemetry.io/otel v1.15.1/go.mod h1:mHHGEHVDLal6YrKMmk9LqC4a3sF5g+fHfrttQIB1NTc= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= +go.opentelemetry.io/otel/sdk v1.15.1 h1:5FKR+skgpzvhPQHIEfcwMYjCBr14LWzs3uSqKiQzETI= +go.opentelemetry.io/otel/sdk v1.15.1/go.mod h1:8rVtxQfrbmbHKfqzpQkT5EzZMcbMBwTzNAggbEAM0KA= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.opentelemetry.io/otel/trace v1.15.1 h1:uXLo6iHJEzDfrNC0L0mNjItIp06SyaBQxu5t3xMlngY= +go.opentelemetry.io/otel/trace v1.15.1/go.mod h1:IWdQG/5N1x7f6YUlmdLeJvH9yxtuJAfc4VW5Agv9r/8= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/exp v0.0.0-20200513190911-00229845015e/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20210715201039-d37aa40e8013/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200423201157-2723c5de0d66/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= +lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= +modernc.org/sqlite v1.22.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/login/conn.go b/login/conn.go new file mode 100755 index 0000000..cbae9c9 --- /dev/null +++ b/login/conn.go @@ -0,0 +1,247 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package login + +import ( + "context" + "encoding/binary" + "fmt" + "math/rand" + + "github.com/bufbuild/connect-go" + "github.com/go-restruct/restruct" + "github.com/pangbox/server/common" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/gen/dbmodels" + "github.com/pangbox/server/gen/proto/go/topologypb" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + "github.com/pangbox/server/pangya" +) + +// Conn holds the state for a connection to the server. +type Conn struct { + common.ServerConn[ClientMessage, ServerMessage] + topologyClient topologypbconnect.TopologyServiceClient + accountsService *accounts.Service +} + +// SendHello sends the initial handshake bytes to the client. +func (c *Conn) SendHello() error { + data, err := restruct.Pack(binary.LittleEndian, &ConnectMessage{ + Unknown1: 0x0b00, + Unknown2: 0x0000, + Unknown3: 0x0000, + Key: uint16(c.Key), + Unknown4: 0x0000, + ServerID: 0x2775, + Unknown6: 0x0000, + }) + if err != nil { + return err + } + + _, err = c.Socket.Write(data) + if err != nil { + return err + } + + return nil +} + +// GetServerList returns a server list using the topology store. +func (c *Conn) GetServerList(ctx context.Context, typ topologypb.Server_Type) (*ServerList, error) { + message := &ServerList{} + response, err := c.topologyClient.ListServers(ctx, connect.NewRequest(&topologypb.ListServersRequest{Type: typ})) + if err != nil { + return nil, fmt.Errorf("getting server list: %w", err) + } + + for _, server := range response.Msg.Server { + message.Servers = append(message.Servers, ServerEntry{ + ServerName: server.Name, + ServerID: server.Id, + NumUsers: server.NumUsers, + MaxUsers: server.MaxUsers, + IPAddress: server.Address, + Port: uint16(server.Port), + Flags: uint16(server.Flags), + }) + } + + message.Count = uint8(len(message.Servers)) + + return message, nil +} + +// Handle runs the main connection loop. +func (c *Conn) Handle(ctx context.Context) error { + log := c.Log + c.Key = uint8(rand.Intn(16)) + + err := c.SendHello() + if err != nil { + return fmt.Errorf("sending hello: %w", err) + } + + msg, err := c.ReadMessage() + if err != nil { + return fmt.Errorf("reading handshake: %w", err) + } + + var player dbmodels.Player + switch t := msg.(type) { + case *ClientLogin: + player, err = c.accountsService.Authenticate(ctx, t.Username.Value, t.Password.Value) + default: + return fmt.Errorf("expected ClientLogin, got %T", t) + } + + if err == accounts.ErrUnknownUsername || err == accounts.ErrInvalidPassword { + log.Infof("Bad credentials.") + c.SendMessage(&ServerLogin{ + Status: LoginStatusError, + Error: &LoginError{ + Error: LoginErrorInvalidCredentials, + }, + }) + c.Socket.Close() + return nil + } else if err != nil { + return fmt.Errorf("database error during authentication: %w", err) + } + + if !player.Nickname.Valid { + c.SendMessage(&ServerLogin{ + Status: LoginStatusSetNickname, + SetNickname: &LoginSetNickname{ + Unknown: 0xFFFFFFFF, + }, + }) + + NickSetup: + for { + msg, err := c.ReadMessage() + if err != nil { + return fmt.Errorf("reading handshake: %w", err) + } + + switch t := msg.(type) { + case *ClientCheckNickname: + // TODO + log.Infof("TODO: check nickname %s", t.Nickname.Value) + c.SendMessage(&ServerNicknameCheckResponse{ + Nickname: t.Nickname, + }) + case *ClientSetNickname: + player, err = c.accountsService.SetNickname(ctx, player.PlayerID, t.Nickname.Value) + if err != nil { + // TODO: need to handle error + log.Errorf("Database error setting nickname: %v", err) + return nil + } + break NickSetup + default: + return fmt.Errorf("expected ClientCheckNickname, ClientSetNickname, got %T", t) + } + } + } + + haveCharacters, err := c.accountsService.HasCharacters(ctx, player.PlayerID) + if err != nil { + log.Errorf("Database error getting characters: %v", err) + return nil + } + + if !haveCharacters { + c.SendMessage(&ServerLogin{ + Status: LoginStatusSetCharacter, + SetCharacter: &LoginSetCharacter{}, + }) + + CharacterSetup: + for { + msg, err := c.ReadMessage() + if err != nil { + return fmt.Errorf("reading handshake: %w", err) + } + + switch t := msg.(type) { + case *ClientSelectCharacter: + c.accountsService.AddCharacter(ctx, player.PlayerID, pangya.PlayerCharacterData{ + CharTypeID: t.CharacterID, + HairColor: t.HairColor, + }) + c.SendMessage(&Server0011{}) + break CharacterSetup + default: + return fmt.Errorf("expected ClientSelectCharacter, got %T", t) + } + } + } + + session, err := c.accountsService.AddSession(ctx, player.PlayerID, c.Socket.RemoteAddr().String()) + if err != nil { + log.Errorf("Error creating session in DB: %v", err) + } + + // TODO: make token + c.SendMessage(&ServerLoginSessionKey{ + SessionKey: common.ToPString(session.SessionKey), + }) + + c.SendMessage(&ServerLogin{ + Success: &LoginSuccess{ + Username: common.ToPString(player.Username), + Nickname: common.ToPString(player.Nickname.String), + UserID: uint32(player.PlayerID), + }, + }) + + log.Info("sending message server list") + messageServers, err := c.GetServerList(ctx, topologypb.Server_TYPE_MESSAGE_SERVER) + if err != nil { + return fmt.Errorf("listing message servers: %w", err) + } + c.SendMessage(&ServerMessageServerList{ServerList: *messageServers}) + + log.Info("sending game server list") + gameServers, err := c.GetServerList(ctx, topologypb.Server_TYPE_GAME_SERVER) + if err != nil { + return fmt.Errorf("listing game servers: %w", err) + } + c.SendMessage(&ServerGameServerList{ServerList: *gameServers}) + + log.Info("waiting for response.") + msg, err = c.ReadMessage() + if err != nil { + return fmt.Errorf("reading next message: %w", err) + } + + switch t := msg.(type) { + case *ClientSelectServer: + log.Debugf("Select server: %+v", t) + default: + return fmt.Errorf("expected ClientSelectServer, got %T", t) + } + + c.SendMessage(&ServerGameSessionKey{ + SessionKey: common.ToPString(session.SessionKey), + }) + + return nil +} diff --git a/login/message.go b/login/message.go new file mode 100644 index 0000000..46773cd --- /dev/null +++ b/login/message.go @@ -0,0 +1,38 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package login + +import "github.com/pangbox/server/common" + +type ServerMessage interface { + common.Message + isLoginServerMessage() +} + +type ServerMessage_ struct{} + +func (msg *ServerMessage_) isLoginServerMessage() {} + +type ClientMessage interface { + common.Message + isLoginClientMessage() +} + +type ClientMessage_ struct{} + +func (msg *ClientMessage_) isLoginClientMessage() {} diff --git a/login/model_test.go b/login/model_test.go new file mode 100755 index 0000000..b292fd1 --- /dev/null +++ b/login/model_test.go @@ -0,0 +1,80 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package login + +import ( + "encoding/binary" + "reflect" + "testing" + + "github.com/go-restruct/restruct" + "github.com/stretchr/testify/assert" +) + +func TestMessageStructure(t *testing.T) { + tests := []struct { + data []byte + value interface{} + }{ + { + data: []byte{ + /* 0x00 */ 0x01, 0x50, 0x61, 0x6e, 0x67, 0x62, 0x6f, 0x78, + /* 0x08 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0x10 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0x18 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0x20 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0x28 */ 0x00, 0xea, 0x4e, 0x00, 0x00, 0xd0, 0x07, 0x00, + /* 0x30 */ 0x00, 0x01, 0x00, 0x00, 0x00, 0x31, 0x32, 0x37, + /* 0x38 */ 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x00, 0x00, + /* 0x40 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xea, + /* 0x48 */ 0x4e, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, + /* 0x50 */ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + /* 0x58 */ 0x00, 0x00, 0x00, 0x00, 0x00, + }, + value: &ServerList{ + Count: 1, + Servers: []ServerEntry{ + { + ServerName: "Pangbox", + ServerID: 20202, + NumUsers: 1, + MaxUsers: 2000, + IPAddress: "127.0.0.1", + Port: 20202, + Flags: 0x800, + }, + }, + }, + }, + } + + for _, test := range tests { + v := reflect.New(reflect.TypeOf(test.value).Elem()) + err := restruct.Unpack(test.data, binary.LittleEndian, v.Interface()) + assert.Nil(t, err, "unpack") + assert.Equal(t, test.value, v.Interface(), "unpack") + + data, err := restruct.Pack(binary.LittleEndian, test.value) + assert.Nil(t, err, "pack") + assert.Equal(t, test.data, data, "pack") + + size, err := restruct.SizeOf(test.value) + assert.Nil(t, err, "sizing") + assert.Equal(t, len(test.data), size, "sizing") + } +} diff --git a/login/msgclient.go b/login/msgclient.go new file mode 100755 index 0000000..f8b9e09 --- /dev/null +++ b/login/msgclient.go @@ -0,0 +1,69 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package login + +import "github.com/pangbox/server/common" + +// ClientMessageID is the type used to identify client messages. +type ClientMessageID uint16 + +var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{ + 0x0001: &ClientLogin{}, + 0x0003: &ClientSelectServer{}, + 0x0006: &ClientSetNickname{}, + 0x0007: &ClientCheckNickname{}, + 0x0008: &ClientSelectCharacter{}, + 0x000B: &ClientReconnect{}, +}) + +type ClientLoginMessage struct { +} + +// ClientLogin is the payload associated with the ClientLogin packet. +// It is sent by the client when the client logs in. +type ClientLogin struct { + ClientMessage_ + Username common.PString + Password common.PString +} + +type ClientSelectServer struct { + ClientMessage_ + Unknown1 uint32 +} + +type ClientSetNickname struct { + ClientMessage_ + Nickname common.PString +} + +type ClientCheckNickname struct { + ClientMessage_ + Nickname common.PString +} + +type ClientSelectCharacter struct { + ClientMessage_ + CharacterID uint32 + HairColor uint8 + Unknown uint8 +} + +type ClientReconnect struct { + ClientMessage_ +} diff --git a/login/msgserver.go b/login/msgserver.go new file mode 100755 index 0000000..a649050 --- /dev/null +++ b/login/msgserver.go @@ -0,0 +1,141 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package login + +import "github.com/pangbox/server/common" + +// These are the known possible server message IDs. +var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ + 0x0001: &ServerLogin{}, + 0x0002: &ServerGameServerList{}, + 0x0003: &ServerGameSessionKey{}, + 0x0006: &ServerMacros{}, + 0x0009: &ServerMessageServerList{}, + 0x000E: &ServerNicknameCheckResponse{}, + 0x0010: &ServerLoginSessionKey{}, + 0x0011: &Server0011{}, + 0x0040: &ServerGameGuardCheck{}, + 0x004D: &ServerLobbiesList{}, +}) + +// ConnectMessage is the message sent by the server when connecting. +type ConnectMessage struct { + Unknown1 uint16 + Unknown2 uint16 + Unknown3 uint16 + Key uint16 + Unknown4 uint16 + ServerID uint16 + Unknown6 uint16 +} + +// ServerEntry represents a server in a ServerListMessage. +type ServerEntry struct { + ServerName string `struct:"[40]byte"` + ServerID uint32 + MaxUsers uint32 + NumUsers uint32 + IPAddress string `struct:"[18]byte"` + Port uint16 + Unknown1 uint16 + Flags uint16 + Unknown2 [16]byte +} + +type ServerList struct { + Count uint8 `struct:"sizeof=Servers"` + Servers []ServerEntry +} + +const ( + LoginStatusSuccess = 0 + LoginStatusSetNickname = 216 + LoginStatusSetCharacter = 217 + LoginStatusError = 227 +) + +type LoginSuccess struct { + Username common.PString + UserID uint32 + Unknown [14]byte + Nickname common.PString +} + +type LoginSetNickname struct { + Unknown uint32 +} + +type LoginSetCharacter struct { +} + +const ( + LoginErrorAlreadyLoggedIn = 5100019 + LoginErrorDuplicateConn = 5100107 + LoginErrorInvalidCredentials = 5100143 + LoginErrorInvalidReconnectToken = 5157002 +) + +type LoginError struct { + Error uint32 +} + +type ServerLogin struct { + ServerMessage_ + Status byte + + Success *LoginSuccess `struct-if:"Status == 0"` + SetNickname *LoginSetNickname `struct-if:"Status == 216"` + SetCharacter *LoginSetCharacter `struct-if:"Status == 217"` + Error *LoginError `struct-if:"Status == 227"` +} + +type ServerGameServerList struct { + ServerMessage_ + ServerList +} +type ServerGameSessionKey struct { + ServerMessage_ + Unknown uint32 + SessionKey common.PString +} +type ServerMacros struct { + ServerMessage_ +} +type ServerMessageServerList struct { + ServerMessage_ + ServerList +} +type ServerNicknameCheckResponse struct { + ServerMessage_ + Unknown uint32 + Nickname common.PString +} +type ServerLoginSessionKey struct { + ServerMessage_ + SessionKey common.PString +} +type Server0011 struct { + ServerMessage_ + Unknown byte +} +type ServerGameGuardCheck struct { + ServerMessage_ +} +type ServerLobbiesList struct { + ServerMessage_ +} diff --git a/login/server.go b/login/server.go new file mode 100755 index 0000000..843c9ba --- /dev/null +++ b/login/server.go @@ -0,0 +1,73 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package login + +import ( + "context" + "net" + + "github.com/pangbox/server/common" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + log "github.com/sirupsen/logrus" +) + +// Options specify the options to use to instantiate the login server. +type Options struct { + TopologyClient topologypbconnect.TopologyServiceClient + AccountsService *accounts.Service +} + +// Server provides an implementation of the PangYa login server. +type Server struct { + topologyClient topologypbconnect.TopologyServiceClient + accountsService *accounts.Service + baseServer *common.BaseServer +} + +// New creates a new instance of the login server. +func New(opts Options) *Server { + return &Server{ + topologyClient: opts.TopologyClient, + accountsService: opts.AccountsService, + baseServer: &common.BaseServer{}, + } +} + +// Listen listens for connections on the given port and blocks indefinitely. +func (s *Server) Listen(ctx context.Context, addr string) error { + logger := log.WithField("server", "LoginServer") + return s.baseServer.Listen(logger, addr, func(logger *log.Entry, socket net.Conn) error { + conn := Conn{ + ServerConn: common.ServerConn[ClientMessage, ServerMessage]{ + Socket: socket, + Log: logger, + ClientMsg: ClientMessageTable, + ServerMsg: ServerMessageTable, + }, + topologyClient: s.topologyClient, + accountsService: s.accountsService, + } + return conn.Handle(ctx) + }) +} + +func (s *Server) Shutdown(shutdownCtx context.Context) error { + // TODO: Need to shut down connection threads. + return s.baseServer.Close() +} diff --git a/message/conn.go b/message/conn.go new file mode 100755 index 0000000..4f784ca --- /dev/null +++ b/message/conn.go @@ -0,0 +1,76 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package message + +import ( + "encoding/binary" + "fmt" + "math/rand" + + "github.com/go-restruct/restruct" + "github.com/pangbox/server/common" +) + +// Conn holds the state for a connection to the server. +type Conn struct { + common.ServerConn[ClientMessage, ServerMessage] +} + +// SendHello sends the initial handshake bytes to the client. +func (c *Conn) SendHello() error { + data, err := restruct.Pack(binary.LittleEndian, &ConnectMessage{ + Unknown1: 0x0900, + Unknown2: 0x0000, + Unknown3: 0x002E, + Unknown4: 0x0101, + Key: uint16(c.Key), + Unknown5: 0x0000, + }) + if err != nil { + return err + } + + _, err = c.Socket.Write(data) + if err != nil { + return err + } + + return nil +} + +// Handle runs the main connection loop. +func (c *Conn) Handle() error { + log := c.Log + c.Key = uint8(rand.Intn(16)) + + err := c.SendHello() + if err != nil { + return fmt.Errorf("sending hello: %w", err) + } + + for { + msg, err := c.ReadMessage() + if err != nil { + log.WithError(err).Error("Error receiving packet") + return err + } + + // TODO: messageng needs impl; should probably use old message server for now? + log.Printf("%#v\n", msg) + } +} diff --git a/message/message.go b/message/message.go new file mode 100644 index 0000000..6962cad --- /dev/null +++ b/message/message.go @@ -0,0 +1,38 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package message + +import "github.com/pangbox/server/common" + +type ServerMessage interface { + common.Message + isMessageServerMessage() +} + +type ServerMessage_ struct{} + +func (msg *ServerMessage_) isMessageServerMessage() {} + +type ClientMessage interface { + common.Message + isMessageClientMessage() +} + +type ClientMessage_ struct{} + +func (msg *ClientMessage_) isMessageClientMessage() {} diff --git a/message/msgclient.go b/message/msgclient.go new file mode 100755 index 0000000..bd90a97 --- /dev/null +++ b/message/msgclient.go @@ -0,0 +1,31 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package message + +import "github.com/pangbox/server/common" + +var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{ + 0x0012: &ClientAuth{}, +}) + +// ClientAuthMessage is sent at connection start to authenticate a session. +type ClientAuth struct { + ClientMessage_ + Cookie uint32 + Nickname common.PString +} diff --git a/message/msgserver.go b/message/msgserver.go new file mode 100755 index 0000000..445843c --- /dev/null +++ b/message/msgserver.go @@ -0,0 +1,39 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package message + +import "github.com/pangbox/server/common" + +var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ + 0x0001: &Server0001{}, +}) + +// ConnectMessage is the message sent by the server when connecting. +type ConnectMessage struct { + Unknown1 uint16 + Unknown2 uint16 + Unknown3 uint16 + Unknown4 uint16 + Key uint16 + Unknown5 uint16 +} + +// Server0001 is an unknown message. +type Server0001 struct { + ServerMessage_ +} diff --git a/message/server.go b/message/server.go new file mode 100755 index 0000000..4be6413 --- /dev/null +++ b/message/server.go @@ -0,0 +1,71 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package message + +import ( + "context" + "net" + + "github.com/pangbox/server/common" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + log "github.com/sirupsen/logrus" +) + +// Options specify the options to use to instantiate the message server. +type Options struct { + TopologyClient topologypbconnect.TopologyServiceClient + AccountsService *accounts.Service +} + +// Server provides an implementation of the PangYa message server. +type Server struct { + topologyClient topologypbconnect.TopologyServiceClient + accountsService *accounts.Service + baseServer *common.BaseServer +} + +// New creates a new instance of the Message server. +func New(opts Options) *Server { + return &Server{ + topologyClient: opts.TopologyClient, + accountsService: opts.AccountsService, + baseServer: &common.BaseServer{}, + } +} + +// Listen listens for new connections on the provided address and blocks. +func (s *Server) Listen(ctx context.Context, addr string) error { + logger := log.WithField("server", "MessageServer") + return s.baseServer.Listen(logger, addr, func(logger *log.Entry, socket net.Conn) error { + conn := Conn{ + ServerConn: common.ServerConn[ClientMessage, ServerMessage]{ + Socket: socket, + Log: logger, + ClientMsg: ClientMessageTable, + ServerMsg: ServerMessageTable, + }, + } + return conn.Handle() + }) +} + +func (s *Server) Shutdown(shutdownCtx context.Context) error { + // TODO: Need to shut down connection threads. + return s.baseServer.Close() +} diff --git a/migrations/0001_create_player_table.sql b/migrations/0001_create_player_table.sql new file mode 100644 index 0000000..1f2479f --- /dev/null +++ b/migrations/0001_create_player_table.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE player ( + player_id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + nickname TEXT UNIQUE, + password_hash TEXT NOT NULL, + pang INTEGER NOT NULL DEFAULT 10000, + rank INTEGER NOT NULL DEFAULT 0 +); + +-- +goose Down +DROP TABLE player; diff --git a/migrations/0002_create_character_table.sql b/migrations/0002_create_character_table.sql new file mode 100644 index 0000000..92fde23 --- /dev/null +++ b/migrations/0002_create_character_table.sql @@ -0,0 +1,10 @@ +-- +goose Up +CREATE TABLE character ( + character_id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id INTEGER REFERENCES player(player_id) ON DELETE CASCADE NOT NULL, + character_type_id INTEGER NOT NULL, + character_data BLOB NOT NULL +); + +-- +goose Down +DROP TABLE character; diff --git a/migrations/0003_create_sessions_table.sql b/migrations/0003_create_sessions_table.sql new file mode 100644 index 0000000..ff51c46 --- /dev/null +++ b/migrations/0003_create_sessions_table.sql @@ -0,0 +1,15 @@ +-- +goose Up +CREATE TABLE session ( + session_id INTEGER PRIMARY KEY AUTOINCREMENT, + player_id INTEGER REFERENCES player(player_id) ON DELETE CASCADE NOT NULL, + session_key TEXT NOT NULL UNIQUE, + session_address TEXT NOT NULL, + session_expires_at INTEGER NOT NULL +); + +CREATE UNIQUE INDEX session_key_idx ON session (session_key); + +-- +goose Down +DROP INDEX session_key_idx; + +DROP TABLE session; diff --git a/migrations/embed.go b/migrations/embed.go new file mode 100644 index 0000000..6dc175b --- /dev/null +++ b/migrations/embed.go @@ -0,0 +1,31 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package migrations + +import ( + "embed" + + "github.com/pressly/goose/v3" +) + +//go:embed *.sql +var migrations embed.FS + +func init() { + goose.SetBaseFS(migrations) +} diff --git a/minibox/admin.go b/minibox/admin.go new file mode 100644 index 0000000..249a6c0 --- /dev/null +++ b/minibox/admin.go @@ -0,0 +1,74 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + "net/http" + + "github.com/pangbox/server/admin" + log "github.com/sirupsen/logrus" +) + +type AdminOptions struct { + Addr string +} + +type AdminServer struct { + service *Service +} + +func NewAdmin(ctx context.Context) *AdminServer { + web := new(AdminServer) + web.service = NewService(ctx) + return web +} + +func (w *AdminServer) Configure(opts AdminOptions) error { + spawn := func(ctx context.Context, service *Service) { + AdminServer := http.Server{Addr: opts.Addr, Handler: admin.New(admin.Options{})} + + service.SetShutdownFunc(func(shutdownCtx context.Context) error { + return AdminServer.Shutdown(shutdownCtx) + }) + + if ctx.Err() != nil { + log.Errorf("Admin server cancelled before server could start: %v", ctx.Err()) + return + } + + err := AdminServer.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + log.Errorf("Error serving admin server: %v", err) + } + } + + return w.service.Configure(spawn) +} + +func (w *AdminServer) Running() bool { + return w.service.Running() +} + +func (w *AdminServer) Start() error { + return w.service.Start() +} + +func (w *AdminServer) Stop() error { + return w.service.Stop() +} diff --git a/minibox/gameserver.go b/minibox/gameserver.go new file mode 100644 index 0000000..1da63da --- /dev/null +++ b/minibox/gameserver.go @@ -0,0 +1,87 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/game" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + "github.com/pangbox/server/pangya/iff" + log "github.com/sirupsen/logrus" +) + +type GameOptions struct { + Addr string + TopologyClient topologypbconnect.TopologyServiceClient + AccountsService *accounts.Service + PangyaIFF *iff.Archive + ServerID uint32 + ChannelName string +} + +type GameServer struct { + service *Service +} + +func NewGameServer(ctx context.Context) *GameServer { + game := new(GameServer) + game.service = NewService(ctx) + return game +} + +func (g *GameServer) Configure(opts GameOptions) error { + spawn := func(ctx context.Context, service *Service) { + gameServer := game.New(game.Options{ + TopologyClient: opts.TopologyClient, + AccountsService: opts.AccountsService, + PangyaIFF: opts.PangyaIFF, + ServerID: opts.ServerID, + ChannelName: opts.ChannelName, + }) + + service.SetShutdownFunc(func(shutdownCtx context.Context) error { + return gameServer.Shutdown(shutdownCtx) + }) + + if ctx.Err() != nil { + log.Errorf("GameServer cancelled before server could start: %v", ctx.Err()) + return + } + + err := gameServer.Listen(ctx, opts.Addr) + if err != nil { + log.Errorf("Error serving GameServer: %v", err) + } + } + + return g.service.Configure(spawn) +} + +func (g *GameServer) Running() bool { + return g.service.Running() +} + +func (g *GameServer) Start() error { + return g.service.Start() +} + +func (g *GameServer) Stop() error { + return g.service.Stop() +} diff --git a/minibox/loginserver.go b/minibox/loginserver.go new file mode 100644 index 0000000..571dd63 --- /dev/null +++ b/minibox/loginserver.go @@ -0,0 +1,80 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + "github.com/pangbox/server/login" + log "github.com/sirupsen/logrus" +) + +type LoginOptions struct { + Addr string + TopologyClient topologypbconnect.TopologyServiceClient + AccountsService *accounts.Service +} + +type LoginServer struct { + service *Service +} + +func NewLoginServer(ctx context.Context) *LoginServer { + login := new(LoginServer) + login.service = NewService(ctx) + return login +} + +func (l *LoginServer) Configure(opts LoginOptions) error { + spawn := func(ctx context.Context, service *Service) { + loginServer := login.New(login.Options{ + TopologyClient: opts.TopologyClient, + AccountsService: opts.AccountsService, + }) + + service.SetShutdownFunc(func(shutdownCtx context.Context) error { + return loginServer.Shutdown(shutdownCtx) + }) + + if ctx.Err() != nil { + log.Errorf("LoginServer cancelled before server could start: %v", ctx.Err()) + return + } + + err := loginServer.Listen(ctx, opts.Addr) + if err != nil { + log.Errorf("Error serving LoginServer: %v", err) + } + } + + return l.service.Configure(spawn) +} + +func (l *LoginServer) Running() bool { + return l.service.Running() +} + +func (l *LoginServer) Start() error { + return l.service.Start() +} + +func (l *LoginServer) Stop() error { + return l.service.Stop() +} diff --git a/minibox/messageserver.go b/minibox/messageserver.go new file mode 100644 index 0000000..cd7df43 --- /dev/null +++ b/minibox/messageserver.go @@ -0,0 +1,80 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + "github.com/pangbox/server/message" + log "github.com/sirupsen/logrus" +) + +type MessageOptions struct { + Addr string + TopologyClient topologypbconnect.TopologyServiceClient + AccountsService *accounts.Service +} + +type MessageServer struct { + service *Service +} + +func NewMessageServer(ctx context.Context) *MessageServer { + message := new(MessageServer) + message.service = NewService(ctx) + return message +} + +func (m *MessageServer) Configure(opts MessageOptions) error { + spawn := func(ctx context.Context, service *Service) { + messageServer := message.New(message.Options{ + TopologyClient: opts.TopologyClient, + AccountsService: opts.AccountsService, + }) + + service.SetShutdownFunc(func(shutdownCtx context.Context) error { + return messageServer.Shutdown(shutdownCtx) + }) + + if ctx.Err() != nil { + log.Errorf("MessageServer cancelled before server could start: %v", ctx.Err()) + return + } + + err := messageServer.Listen(ctx, opts.Addr) + if err != nil { + log.Errorf("Error serving MessageServer: %v", err) + } + } + + return m.service.Configure(spawn) +} + +func (m *MessageServer) Running() bool { + return m.service.Running() +} + +func (m *MessageServer) Start() error { + return m.service.Start() +} + +func (m *MessageServer) Stop() error { + return m.service.Stop() +} diff --git a/minibox/minibox.go b/minibox/minibox.go new file mode 100644 index 0000000..fcb9b51 --- /dev/null +++ b/minibox/minibox.go @@ -0,0 +1,364 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + "database/sql" + "errors" + "fmt" + "path/filepath" + "sync" + + "github.com/pangbox/pangfiles/crypto/pyxtea" + "github.com/pangbox/pangfiles/pak" + "github.com/pangbox/server/common/hash" + "github.com/pangbox/server/database" + "github.com/pangbox/server/database/accounts" + _ "github.com/pangbox/server/migrations" + "github.com/pangbox/server/pangya/iff" + "github.com/pressly/goose/v3" + log "github.com/sirupsen/logrus" + "github.com/xo/dburl" + _ "modernc.org/sqlite" +) + +type DataOptions struct { + DatabaseURI string `json:"URI"` +} + +type Options struct { + WebAddr string `json:"WebAddr"` + AdminAddr string `json:"AdminAddr"` + QAAuthAddr string `json:"QAAuthAddr"` + LoginAddr string `json:"LoginAddr"` + GameAddr string `json:"GameAddr"` + MessageAddr string `json:"MessageAddr"` + ServerIP string `json:"ServerIP"` + GameServerName string `json:"GameServerName"` + GameChannelName string `json:"GameChannelName"` + PangyaRegion string `json:"PangyaRegion"` + PangyaDir string `json:"PangyaDir"` + PangyaIFF string `json:"PangyaIFF"` +} + +type Server struct { + mu sync.RWMutex + log *log.Entry + + // Fabric services + accountsService *accounts.Service + + // Network services + Topology *TopologyServer + Web *WebServer + Admin *AdminServer + Login *LoginServer + Game *GameServer + Message *MessageServer + QAAuth *QAAuthServer + + // Misc + Rugburn *RugburnPatcher + + pangyaKey pyxtea.Key + pangyaFS *pak.FS + pangyaIFF *iff.Archive + lastDbOpts *DataOptions + lastOpts *Options +} + +func topologyOptions(opts Options) (TopologyServerOptions, error) { + gamePort, err := getPort(opts.GameAddr) + if err != nil { + return TopologyServerOptions{}, fmt.Errorf("failed to parse game server address: %s", opts.GameAddr) + } + + messagePort, err := getPort(opts.MessageAddr) + if err != nil { + return TopologyServerOptions{}, fmt.Errorf("failed to parse message server address: %s", opts.GameAddr) + } + + return TopologyServerOptions{ + ServerIP: opts.ServerIP, + GameServerName: opts.GameServerName, + GamePort: gamePort, + MessagePort: messagePort, + }, nil +} + +func dbConnectMigrate(urlstr string) (*sql.DB, error) { + url, err := dburl.Parse(urlstr) + if err != nil { + return nil, fmt.Errorf("parsing database URL: %w", err) + } + + db, err := database.OpenDBWithDriver(url.Driver, url.DSN) + if err != nil { + return nil, fmt.Errorf("opening database: %w", err) + } + + if err := goose.Up(db, "."); err != nil { + return nil, fmt.Errorf("running database migrations: %w", err) + } + + return db, nil +} + +func NewServer(ctx context.Context, log *log.Entry) *Server { + server := new(Server) + server.log = log + server.Topology = NewLocalTopology(ctx) + server.Web = NewWeb(ctx) + server.Admin = NewAdmin(ctx) + server.Login = NewLoginServer(ctx) + server.Game = NewGameServer(ctx) + server.Message = NewMessageServer(ctx) + server.QAAuth = NewQAAuth(ctx) + server.Rugburn = NewRugburnPatcher() + return server +} + +// ConfigureDatabase configures the database, including running pending +// migrations. If the services are already configured, it will also re-run +// service configuration with the last configuration. +func (server *Server) ConfigureDatabase(opts DataOptions) error { + server.mu.Lock() + defer server.mu.Unlock() + + // Do not do anything if the database settings are the same. + if server.lastDbOpts != nil && server.lastDbOpts.DatabaseURI == opts.DatabaseURI { + return nil + } + + db, err := dbConnectMigrate(opts.DatabaseURI) + if err != nil { + return fmt.Errorf("setting up database: %w", err) + } + + server.accountsService = accounts.NewService(accounts.Options{ + Database: db, + Hasher: hash.Bcrypt{}, + }) + + // If the services are already running, reconfigure all of them. + if server.lastOpts != nil { + configureOpts := *server.lastOpts + server.lastOpts = nil + server.ConfigureServices(configureOpts) + } + + server.lastDbOpts = &opts + + return nil +} + +// ConfigureServices reconfigures services. You must successfully configure +// the database before running this. +func (server *Server) ConfigureServices(opts Options) error { + server.mu.Lock() + defer server.mu.Unlock() + + if server.accountsService == nil { + return errors.New("database not configured yet") + } + + if server.lastOpts.ShouldRedetectPangyaKey(opts) { + key, err := getPakKey(server.log, opts.PangyaRegion, []string{ + filepath.Join(opts.PangyaDir, "projectg*.pak"), + filepath.Join(opts.PangyaDir, "ProjectG*.pak"), + }) + + if err != nil { + return fmt.Errorf("detecting pak region: %w", err) + } + + server.pangyaFS, err = pak.LoadPaks(key, []string{filepath.Join(opts.PangyaDir, "*.pak")}) + if err != nil { + return err + } + + server.pangyaIFF, err = iff.LoadFromPak(*server.pangyaFS) + if err != nil { + return err + } + + server.pangyaKey = key + } + + if server.lastOpts.ShouldConfigureTopology(opts) { + topologyOptions, err := topologyOptions(opts) + if err != nil { + return fmt.Errorf("configuring topology server: %w", err) + } + + if err := server.Topology.Configure(topologyOptions); err != nil { + return fmt.Errorf("configuring topology server: %w", err) + } + } + + if server.lastOpts.ShouldConfigureWeb(opts) { + if err := server.Web.Configure(WebOptions{ + Addr: opts.WebAddr, + PangyaKey: server.pangyaKey, + PangyaDir: opts.PangyaDir, + }); err != nil { + return fmt.Errorf("configuring web server: %w", err) + } + } + + if server.lastOpts.ShouldConfigureAdmin(opts) { + if err := server.Admin.Configure(AdminOptions{ + Addr: opts.AdminAddr, + }); err != nil { + return fmt.Errorf("configuring web server: %w", err) + } + } + + if server.lastOpts.ShouldConfigureQAAuth(opts) { + if err := server.QAAuth.Configure(QAAuthOptions{ + Addr: opts.QAAuthAddr, + }); err != nil { + return fmt.Errorf("configuring QA auth server: %w", err) + } + } + + if server.lastOpts.ShouldConfigureLogin(opts) { + if err := server.Login.Configure(LoginOptions{ + Addr: opts.LoginAddr, + TopologyClient: server.Topology.Client(), + AccountsService: server.accountsService, + }); err != nil { + return fmt.Errorf("configuring login server: %w", err) + } + } + + if server.lastOpts.ShouldConfigureGame(opts) { + if err := server.Game.Configure(GameOptions{ + Addr: opts.GameAddr, + TopologyClient: server.Topology.Client(), + AccountsService: server.accountsService, + PangyaIFF: server.pangyaIFF, + ServerID: 20202, + ChannelName: opts.GameChannelName, + }); err != nil { + return fmt.Errorf("configuring game server: %w", err) + } + } + + if server.lastOpts.ShouldConfigureMessage(opts) { + if err := server.Message.Configure(MessageOptions{ + Addr: opts.MessageAddr, + TopologyClient: server.Topology.Client(), + AccountsService: server.accountsService, + }); err != nil { + return fmt.Errorf("configuring message server: %w", err) + } + } + + server.Rugburn.Configure(RugburnOptions{ + PangyaDir: opts.PangyaDir, + }) + + server.lastOpts = &opts + + return nil +} + +// ShouldRedetectPangyaKey returns true if the options changed require the +// PangYa key to be re-detected. +func (options *Options) ShouldRedetectPangyaKey(newOpts Options) bool { + if options == nil { + return true + } + return (options.PangyaDir != newOpts.PangyaDir || + options.PangyaRegion != newOpts.PangyaRegion || + options.PangyaIFF != newOpts.PangyaIFF) +} + +// ShouldConfigureWeb returns true if the options changed require the webserver +// to be re-configured. +func (options *Options) ShouldConfigureWeb(newOpts Options) bool { + if options == nil { + return true + } + return (options.WebAddr != newOpts.WebAddr || + options.PangyaDir != newOpts.PangyaDir || + options.PangyaRegion != newOpts.PangyaRegion) +} + +// ShouldConfigureAdmin returns true if the options changed require the admin +// server to be re-configured. +func (options *Options) ShouldConfigureAdmin(newOpts Options) bool { + if options == nil { + return true + } + return options.AdminAddr != newOpts.AdminAddr +} + +// ShouldConfigureQAAuth returns true if the options changed require the QA +// auth server to be re-configured. +func (options *Options) ShouldConfigureQAAuth(newOpts Options) bool { + if options == nil { + return true + } + return options.QAAuthAddr != newOpts.QAAuthAddr +} + +// ShouldConfigureLogin returns true if the options changed require the login +// server to be re-configured. +func (options *Options) ShouldConfigureLogin(newOpts Options) bool { + if options == nil { + return true + } + return options.LoginAddr != newOpts.LoginAddr +} + +// ShouldConfigureGame returns true if the options changed require the game +// server to be re-configured. +func (options *Options) ShouldConfigureGame(newOpts Options) bool { + if options == nil { + return true + } + return (options.GameAddr != newOpts.GameAddr || + options.GameChannelName != newOpts.GameChannelName || + options.PangyaDir != newOpts.PangyaDir || + options.PangyaRegion != newOpts.PangyaRegion || + options.PangyaIFF != newOpts.PangyaIFF) +} + +// ShouldConfigureMessage returns true if the options changed require the +// message server to be re-configured. +func (options *Options) ShouldConfigureMessage(newOpts Options) bool { + if options == nil { + return true + } + return options.MessageAddr != newOpts.MessageAddr +} + +// ShouldConfigureMessage returns true if the options changed require the +// message server to be re-configured. +func (options *Options) ShouldConfigureTopology(newOpts Options) bool { + if options == nil { + return true + } + return (options.GameAddr != newOpts.GameAddr || + options.MessageAddr != newOpts.MessageAddr || + options.ServerIP != newOpts.ServerIP || + options.GameServerName != newOpts.GameServerName) +} diff --git a/minibox/qa.go b/minibox/qa.go new file mode 100644 index 0000000..b8803b7 --- /dev/null +++ b/minibox/qa.go @@ -0,0 +1,74 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + "net/http" + + "github.com/pangbox/server/qa/authserv" + log "github.com/sirupsen/logrus" +) + +type QAAuthOptions struct { + Addr string +} + +type QAAuthServer struct { + service *Service +} + +func NewQAAuth(ctx context.Context) *QAAuthServer { + qaauth := new(QAAuthServer) + qaauth.service = NewService(ctx) + return qaauth +} + +func (q *QAAuthServer) Configure(opts QAAuthOptions) error { + spawn := func(ctx context.Context, service *Service) { + qaAuthServer := http.Server{Addr: opts.Addr, Handler: authserv.New()} + + service.SetShutdownFunc(func(shutdownCtx context.Context) error { + return qaAuthServer.Shutdown(shutdownCtx) + }) + + if ctx.Err() != nil { + log.Errorf("QA Auth service cancelled before server could start: %v", ctx.Err()) + return + } + + err := qaAuthServer.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + log.Errorf("Error serving QA Auth server: %v", err) + } + } + + return q.service.Configure(spawn) +} + +func (q *QAAuthServer) Running() bool { + return q.service.Running() +} + +func (q *QAAuthServer) Start() error { + return q.service.Start() +} + +func (q *QAAuthServer) Stop() error { + return q.service.Stop() +} diff --git a/minibox/rugburn.go b/minibox/rugburn.go new file mode 100644 index 0000000..20b2d45 --- /dev/null +++ b/minibox/rugburn.go @@ -0,0 +1,152 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "errors" + "log" + "os" + "path/filepath" + "sync" + + "github.com/pangbox/rugburn/slipstrm/embedded" + "github.com/pangbox/rugburn/slipstrm/patcher" +) + +type RugburnOptions struct { + PangyaDir string +} + +type RugburnPatcher struct { + mu sync.RWMutex + + path string + calc int64 + + haveOrig bool + rugburnVer string + rugburnVerErr error +} + +func NewRugburnPatcher() *RugburnPatcher { + return new(RugburnPatcher) +} + +func (p *RugburnPatcher) Configure(opts RugburnOptions) { + p.mu.Lock() + defer p.mu.Unlock() + + p.path = filepath.Join(opts.PangyaDir, "ijl15.dll") +} + +func (p *RugburnPatcher) recalc() error { + finfo, err := os.Stat(p.path) + if err != nil { + return err + } + + p.mu.Lock() + defer p.mu.Unlock() + + ncalc := finfo.Size() ^ finfo.ModTime().Unix() + if p.calc == ncalc { + return nil + } + + ijl15, err := os.ReadFile(p.path) + if err != nil { + return err + } + + orig := patcher.CheckOriginalData(ijl15) + if orig { + p.haveOrig = true + } else { + orig, err := patcher.UnpackOriginal(ijl15) + if err == nil { + p.haveOrig = patcher.CheckOriginalData(orig) + } + } + p.rugburnVer, p.rugburnVerErr = patcher.GetRugburnVersion(ijl15) + p.calc = ncalc + return nil +} + +func (p *RugburnPatcher) RugburnVersion() (string, error) { + if err := p.recalc(); err != nil { + return "", err + } + + p.mu.RLock() + defer p.mu.RUnlock() + + return p.rugburnVer, p.rugburnVerErr +} + +func (p *RugburnPatcher) HaveOriginal() bool { + if err := p.recalc(); err != nil { + return false + } + + p.mu.RLock() + defer p.mu.RUnlock() + + return p.haveOrig +} + +func (p *RugburnPatcher) Patch() error { + ijl15, err := os.ReadFile(p.path) + if err != nil { + return err + } + + if !patcher.CheckOriginalData(ijl15) { + ijl15, err = patcher.UnpackOriginal(ijl15) + if err != nil { + return err + } + if !patcher.CheckOriginalData(ijl15) { + return errors.New("couldn't recover original ijl15.dll for patching") + } + } + + rugburn, err := patcher.Patch(log.Default(), ijl15, embedded.RugburnDLL, embedded.Version) + if err != nil { + return err + } + + err = os.WriteFile(p.path, rugburn, 0644) + _ = p.recalc() + return err +} + +func (p *RugburnPatcher) Unpatch() error { + rugburn, err := os.ReadFile(p.path) + if err != nil { + return err + } + + ijl15, err := patcher.UnpackOriginal(rugburn) + if err != nil { + return err + } + + err = os.WriteFile(p.path, ijl15, 0644) + _ = p.recalc() + return err +} diff --git a/minibox/service.go b/minibox/service.go new file mode 100644 index 0000000..4fc4681 --- /dev/null +++ b/minibox/service.go @@ -0,0 +1,166 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + "errors" + "sync" + "time" +) + +var ErrServiceRunning = errors.New("service is already running") +var ErrServiceStopped = errors.New("service is already stopped") +var ErrServiceNotConfigured = errors.New("service is not configured") +var ErrStopping = errors.New("stopping service") + +const ShutdownTimeout = 10 * time.Second + +type ServiceShutdownFunc func(shutdownCtx context.Context) error +type ServiceSpawnFunc func(ctx context.Context, service *Service) + +type Service struct { + mu sync.RWMutex + + pctx context.Context + ctx context.Context + cancel context.CancelCauseFunc + + running bool + + spawn ServiceSpawnFunc + shutdown ServiceShutdownFunc +} + +func NewService(ctx context.Context) *Service { + service := new(Service) + service.pctx = ctx + return service +} + +func (s *Service) start() { + s.running = true + ctx, cancel := context.WithCancelCause(s.pctx) + go func() { + s.spawn(ctx, s) + s.stopCtx(ctx, cancel) + }() + s.ctx, s.cancel = ctx, cancel +} + +func (s *Service) stop() { + s.running = false + s.cancel(ErrStopping) + s.ctx = nil + s.cancel = nil + s.shutdown = nil +} + +func (s *Service) stopCtx(ctx context.Context, cancel context.CancelCauseFunc) { + // This function exists to solve a race condition when restarting a service. + // If we restart fast enough, the goroutine of the previous run could run + // stop after we've already started. Still cancel the old context + // defensively even though it should already be cancelled. + + s.mu.Lock() + defer s.mu.Unlock() + + if ctx == s.ctx { + s.stop() + } else { + cancel(ErrStopping) + } +} + +func (s *Service) SetShutdownFunc(shutdown ServiceShutdownFunc) { + s.mu.Lock() + defer s.mu.Unlock() + + s.shutdown = shutdown +} + +func (s *Service) Running() bool { + s.mu.RLock() + defer s.mu.RUnlock() + + return s.running +} + +func (s *Service) Start() error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.running { + return ErrServiceRunning + } + + if s.spawn == nil { + return ErrServiceNotConfigured + } + + s.start() + return nil +} + +func (s *Service) Stop() error { + shutdownCtx, cancel := context.WithTimeout(s.pctx, ShutdownTimeout) + defer cancel() + + return s.StopContext(shutdownCtx) +} + +func (s *Service) StopContext(shutdownCtx context.Context) error { + s.mu.Lock() + defer s.mu.Unlock() + + if !s.running { + return ErrServiceStopped + } + + var err error + if s.shutdown != nil { + err = s.shutdown(shutdownCtx) + } + + s.stop() + + return err +} + +func (s *Service) Configure(spawn ServiceSpawnFunc) error { + shutdownCtx, cancel := context.WithTimeout(s.pctx, ShutdownTimeout) + defer cancel() + + return s.ConfigureContext(shutdownCtx, spawn) +} + +func (s *Service) ConfigureContext(shutdownCtx context.Context, spawn ServiceSpawnFunc) error { + var err error + + s.mu.Lock() + defer s.mu.Unlock() + + if s.running { + err = s.shutdown(shutdownCtx) + s.stop() + } + + s.spawn = spawn + s.start() + return err +} diff --git a/minibox/topology.go b/minibox/topology.go new file mode 100644 index 0000000..4f33c82 --- /dev/null +++ b/minibox/topology.go @@ -0,0 +1,154 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + "crypto/tls" + "net" + "net/http" + "strconv" + + "github.com/pangbox/server/common/bufconn" + "github.com/pangbox/server/common/topology" + "github.com/pangbox/server/gen/proto/go/topologypb" + "github.com/pangbox/server/gen/proto/go/topologypb/topologypbconnect" + log "github.com/sirupsen/logrus" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +type TopologyServerOptions struct { + ServerIP string + GameServerName string + + GamePort uint16 + MessagePort uint16 +} + +type TopologyServer struct { + pipe *bufconn.Listener + service *Service + client topologypbconnect.TopologyServiceClient +} + +func NewLocalTopology(ctx context.Context) *TopologyServer { + listener := bufconn.Listen(65536) + + h2transport := &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) { + return listener.Dial() + }, + } + client := &http.Client{Transport: h2transport} + + topology := &TopologyServer{ + pipe: listener, + service: NewService(ctx), + client: topologypbconnect.NewTopologyServiceClient(client, "https://localhost"), + } + + return topology +} + +func (t *TopologyServer) Configure(opts TopologyServerOptions) error { + server := topology.NewServer(topology.NewMemoryStorage([]*topologypb.ServerEntry{ + { + Server: &topologypb.Server{ + Type: topologypb.Server_TYPE_GAME_SERVER, + Name: opts.GameServerName, + Id: 20202, + NumUsers: 1, + MaxUsers: 200, + Address: opts.ServerIP, + Port: uint32(opts.GamePort), + Flags: 0x800, + }, + }, + { + Server: &topologypb.Server{ + Type: topologypb.Server_TYPE_MESSAGE_SERVER, + Name: "MessageServer1", + Id: 30303, + NumUsers: 1, + MaxUsers: 200, + Address: opts.ServerIP, + Port: uint32(opts.MessagePort), + Flags: 0x1000, + }, + }, + })) + + _, handler := topologypbconnect.NewTopologyServiceHandler(server) + + spawn := func(ctx context.Context, service *Service) { + httpserver := &http.Server{ + Handler: h2c.NewHandler(handler, &http2.Server{}), + BaseContext: func(l net.Listener) context.Context { + return ctx + }, + } + + service.SetShutdownFunc(func(shutdownCtx context.Context) error { + return httpserver.Shutdown(shutdownCtx) + }) + + if ctx.Err() != nil { + log.Errorf("Topology service cancelled before server could start: %v", ctx.Err()) + } + + err := httpserver.Serve(t.pipe) + if err != nil && err != http.ErrServerClosed { + log.Errorf("Error serving topology server: %v", err) + } + } + + t.service.Configure(spawn) + + return nil +} + +func (t *TopologyServer) Client() topologypbconnect.TopologyServiceClient { + return t.client +} + +func (t *TopologyServer) Running() bool { + return t.service.Running() +} + +func (t *TopologyServer) Start() error { + return t.service.Start() +} + +func (t *TopologyServer) Stop() error { + return t.service.Stop() +} + +func getPort(addr string) (uint16, error) { + _, portstr, err := net.SplitHostPort(addr) + if err != nil { + return 0, err + } + + port, err := strconv.Atoi(portstr) + if err != nil { + return 0, err + } + + return uint16(port), nil +} diff --git a/minibox/web.go b/minibox/web.go new file mode 100644 index 0000000..d46ec81 --- /dev/null +++ b/minibox/web.go @@ -0,0 +1,102 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package minibox + +import ( + "context" + "net/http" + "strings" + + "github.com/pangbox/pangfiles/crypto/pyxtea" + "github.com/pangbox/pangfiles/pak" + "github.com/pangbox/server/common/pycrypto" + "github.com/pangbox/server/database/accounts" + "github.com/pangbox/server/web" + log "github.com/sirupsen/logrus" +) + +type WebOptions struct { + Addr string + PangyaKey pyxtea.Key + PangyaDir string + AccountsService *accounts.Service +} + +type WebServer struct { + service *Service +} + +func NewWeb(ctx context.Context) *WebServer { + web := new(WebServer) + web.service = NewService(ctx) + return web +} + +func (w *WebServer) Configure(opts WebOptions) error { + spawn := func(ctx context.Context, service *Service) { + webServer := http.Server{Addr: opts.Addr, Handler: web.New(web.Options{ + ServePangYaData: true, + UpdateList: &web.UpdateListOptions{ + Key: opts.PangyaKey, + Dir: opts.PangyaDir, + }, + AccountsService: opts.AccountsService, + })} + + service.SetShutdownFunc(func(shutdownCtx context.Context) error { + return webServer.Shutdown(shutdownCtx) + }) + + if ctx.Err() != nil { + log.Errorf("Web server cancelled before server could start: %v", ctx.Err()) + return + } + + err := webServer.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + log.Errorf("Error serving web server: %v", err) + } + } + + return w.service.Configure(spawn) +} + +func (w *WebServer) Running() bool { + return w.service.Running() +} + +func (w *WebServer) Start() error { + return w.service.Start() +} + +func (w *WebServer) Stop() error { + return w.service.Stop() +} + +func getPakKey(log *log.Entry, region string, patterns []string) (pyxtea.Key, error) { + if region == "" { + log.Println("Auto-detecting pak region (use -region to improve startup delay.)") + key, err := pak.DetectRegion(patterns, pycrypto.Keys) + if err != nil { + return pyxtea.Key{}, err + } + log.Printf("Detected pak region as %s.", strings.ToUpper(pycrypto.GetKeyRegion(key))) + return key, nil + } + return pycrypto.GetRegionKey(region) +} diff --git a/pangya/config.go b/pangya/config.go new file mode 100755 index 0000000..422112e --- /dev/null +++ b/pangya/config.go @@ -0,0 +1,26 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pangya + +type MacroData struct { + Text string `struct:"[64]byte"` +} + +type MacroList struct { + Macros [8]MacroData +} diff --git a/pangya/iff/common.go b/pangya/iff/common.go new file mode 100644 index 0000000..f8cdad9 --- /dev/null +++ b/pangya/iff/common.go @@ -0,0 +1,44 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package iff + +import "github.com/pangbox/server/pangya" + +type Header struct { + RecordCount uint16 + BindingID uint16 + Version uint32 +} + +type Common struct { + Active uint32 + ID uint32 + Name string `struct:"[40]byte"` + Level byte + Icon string `struct:"[40]byte"` + Price uint32 + DiscountPrice uint32 + UsedPrice uint32 + ShopFlag byte + MoneyFlag byte + TimeFlag byte + TimeByte byte + Point uint32 + StartTime pangya.SystemTime + EndTime pangya.SystemTime +} diff --git a/pangya/iff/course.go b/pangya/iff/course.go new file mode 100644 index 0000000..924fd8f --- /dev/null +++ b/pangya/iff/course.go @@ -0,0 +1,28 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package iff + +type Course struct { + Common Common + ShortName string `struct:"[40]byte"` + LocalizedName string `struct:"[40]byte"` + CourseFlag byte + PropertyFileName string `struct:"[40]byte"` + Unknown uint32 + CourseSequence string `struct:"[40]byte"` +} diff --git a/pangya/iff/file.go b/pangya/iff/file.go new file mode 100644 index 0000000..edcddc6 --- /dev/null +++ b/pangya/iff/file.go @@ -0,0 +1,76 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package iff + +import ( + "archive/zip" + "bytes" + "errors" + "fmt" + + "github.com/pangbox/pangfiles/pak" + log "github.com/sirupsen/logrus" +) + +type Archive struct { +} + +// Filenames to look for to find client IFF. +var iffSearchOrder = []string{ + "pangya_gb.iff", + "pangya_jp.iff", + "pangya_eu.iff", + "pangya_th.iff", + "pangya_sg.iff", // nb: uses jp key + "pangya_idnes.iff", + "pangya.iff", // kr (present in some gb ver too) +} + +func LoadFromPak(fs pak.FS) (*Archive, error) { + data, err := findPangYaIFF(fs) + if err != nil { + return nil, err + } + return Load(data) +} + +func Load(data []byte) (*Archive, error) { + r, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return nil, err + } + for _, f := range r.File { + log.Debugf("Found IFF: %s", f.Name) + } + return &Archive{}, nil +} + +func findPangYaIFF(fs pak.FS) ([]byte, error) { + var errs error + if fs.NumFiles() == 0 { + return nil, fmt.Errorf("no pak files found, aborting IFF search") + } + for _, fn := range iffSearchOrder { + data, err := fs.ReadFile(fn) + if err == nil { + return data, err + } + errs = errors.Join(errs, fmt.Errorf("trying: %q: %w", fn, err)) + } + return nil, fmt.Errorf("error finding IFF file: %w", errs) +} diff --git a/pangya/player.go b/pangya/player.go new file mode 100755 index 0000000..fb0d64f --- /dev/null +++ b/pangya/player.go @@ -0,0 +1,151 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pangya + +type UserInfo struct { + Username string `struct:"[22]byte"` + Nickname string `struct:"[22]byte"` + Unknown [33]byte + GMFlag byte + Unknown2 [7]byte + ConnnectionID uint32 + Unknown3 [32]byte + ChatFlag byte + Unknown4 [139]byte + PlayerID uint32 +} + +type PlayerStats struct { + Unknown uint32 + TotalStrokes uint32 + TotalPlayTime uint32 + AverageStrokeTime uint32 + Unknown2 [12]byte + OBRate uint32 + TotalDistance uint32 + TotalHoles uint32 + Unknown3 uint32 + HIO uint32 + Unknown4 [26]byte + Experience uint32 + Rank Rank + Pangs uint64 + Unknown5 [58]byte + QuitRateY uint32 + Unknown6 [32]byte + GameComboX uint32 + GameComboY uint32 + QuitRateX uint32 + TotalPangsWin uint64 + Unknown7 [38]byte +} + +type PlayerEquippedItems struct { + ItemIDs [10]uint32 +} + +type Decorations struct { + Background uint32 + Frame uint32 + Sticker uint32 + Slot uint32 + Unknown uint32 + Title uint32 +} + +type PlayerEquipment struct { + CaddieID uint32 + CharacterID uint32 + ClubSetID uint32 + AztecIffID uint32 + + Items PlayerEquippedItems + + Unknown13 uint32 + Unknown14 uint32 + Unknown15 uint32 + Unknown16 uint32 + Unknown17 uint32 + Unknown18 uint32 + + Decorations Decorations + + MascotID uint32 + + Unknown26 uint32 + Unknown27 uint32 +} + +type PlayerCharacterData struct { + CharTypeID uint32 + ID uint32 + HairColor uint8 + Shirt uint8 + Unknown1 byte + Unknown2 byte + PartTypeIDs [24]uint32 + PartIDs [24]uint32 + Unknown3 [216]byte + AuxParts [5]uint32 + CutInID uint32 + Unknown4 [12]byte + Stats [5]byte + Mastery int + CardChar [4]uint32 + CardCaddie [4]uint32 + CardNPC [4]uint32 +} + +type PlayerItem struct { + ID uint32 + IFFID uint32 +} + +type PlayerCaddieData struct { + Item PlayerItem + Unknown [17]byte +} + +type ClubStats struct { + UpgradeStats [5]uint16 +} + +type PlayerClubData struct { + Item PlayerItem + Unknown [10]byte + Stats ClubStats +} + +type PlayerMascotData struct { + Item PlayerItem + Unknown [5]byte + Text string `struct:"[16]byte"` + Unknown2 [33]byte +} + +type PlayerData struct { + UserInfo UserInfo + PlayerStats PlayerStats + Unknown [78]byte + Items PlayerEquipment + JunkData [10836]byte + EquippedCharacter PlayerCharacterData + EquippedCaddie PlayerCaddieData + EquippedClub PlayerClubData + EquippedMascot PlayerMascotData +} diff --git a/pangya/rank.go b/pangya/rank.go new file mode 100755 index 0000000..54fa1dd --- /dev/null +++ b/pangya/rank.go @@ -0,0 +1,96 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pangya + +// Rank represents an in-game rank. +type Rank byte + +// These are the known possible in-game rank values. +const ( + RookieF Rank = 0x00 + RookieE Rank = 0x01 + RookieD Rank = 0x02 + RookieC Rank = 0x03 + RookieB Rank = 0x04 + RookieA Rank = 0x05 + BeginnerE Rank = 0x06 + BeginnerD Rank = 0x07 + BeginnerC Rank = 0x08 + BeginnerB Rank = 0x09 + BeginnerA Rank = 0x0A + JuniorE Rank = 0x0B + JuniorD Rank = 0x0C + JuniorC Rank = 0x0D + JuniorB Rank = 0x0E + JuniorA Rank = 0x0F + SeniorE Rank = 0x10 + SeniorD Rank = 0x11 + SeniorC Rank = 0x12 + SeniorB Rank = 0x13 + SeniorA Rank = 0x14 + AmateurE Rank = 0x15 + AmateurD Rank = 0x16 + AmateurC Rank = 0x17 + AmateurB Rank = 0x18 + AmateurA Rank = 0x19 + SemiProE Rank = 0x1A + SemiProD Rank = 0x1B + SemiProC Rank = 0x1C + SemiProB Rank = 0x1D + SemiProA Rank = 0x1E + ProE Rank = 0x1F + ProD Rank = 0x20 + ProC Rank = 0x21 + ProB Rank = 0x22 + ProA Rank = 0x23 + NationalProE Rank = 0x24 + NationalProD Rank = 0x25 + NationalProC Rank = 0x26 + NationalProB Rank = 0x27 + NationalProA Rank = 0x28 + WorldProE Rank = 0x29 + WorldProD Rank = 0x2A + WorldProC Rank = 0x2B + WorldProB Rank = 0x2C + WorldProA Rank = 0x2D + MasterE Rank = 0x2E + MasterD Rank = 0x2F + MasterC Rank = 0x30 + MasterB Rank = 0x31 + MasterA Rank = 0x32 + TopMasterE Rank = 0x33 + TopMasterD Rank = 0x34 + TopMasterC Rank = 0x35 + TopMasterB Rank = 0x36 + TopMasterA Rank = 0x37 + WorldMasterE Rank = 0x38 + WorldMasterD Rank = 0x39 + WorldMasterC Rank = 0x3A + WorldMasterB Rank = 0x3B + WorldMasterA Rank = 0x3C + LegendE Rank = 0x3D + LegendD Rank = 0x3E + LegendC Rank = 0x3F + LegendB Rank = 0x40 + LegendA Rank = 0x41 + InfinityLegendE Rank = 0x42 + InfinityLegendD Rank = 0x43 + InfinityLegendC Rank = 0x44 + InfinityLegendB Rank = 0x45 + InfinityLegendA Rank = 0x46 +) diff --git a/pangya/server.go b/pangya/server.go new file mode 100755 index 0000000..90bdc2b --- /dev/null +++ b/pangya/server.go @@ -0,0 +1,43 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pangya + +type ChannelEntry struct { + ChannelName string `struct:"[64]byte"` + MaxUsers uint16 + NumUsers uint16 + Unknown1 uint16 + Unknown2 uint16 + Unknown3 [5]byte +} + +// ServerEntry represents a server in a ServerListMessage. +type ServerEntry struct { + ServerName string `struct:"[40]byte"` + ServerID uint32 + MaxUsers uint32 + NumUsers uint32 + IPAddress string `struct:"[18]byte"` + Port uint16 + Unknown3 uint16 + Flags uint16 + Unknown4 [16]byte + + Count byte `struct:"sizeof=Channels"` + Channels []ChannelEntry +} diff --git a/pangya/systemtime.go b/pangya/systemtime.go new file mode 100644 index 0000000..bc03c1c --- /dev/null +++ b/pangya/systemtime.go @@ -0,0 +1,23 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package pangya + +type SystemTime struct { + Year, Month, DayOfWeek, Day uint16 + Hour, Minute, Second, Milliseconds uint16 +} diff --git a/proto/buf.yaml b/proto/buf.yaml new file mode 100644 index 0000000..51f48ee --- /dev/null +++ b/proto/buf.yaml @@ -0,0 +1,7 @@ +version: v1 +breaking: + use: + - FILE +lint: + use: + - DEFAULT diff --git a/proto/topologypb/topology.proto b/proto/topologypb/topology.proto new file mode 100755 index 0000000..f54edb0 --- /dev/null +++ b/proto/topologypb/topology.proto @@ -0,0 +1,84 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/pangbox/server/gen/proto/go/topologypb"; + +// ServerEntry is the internal server entry used for storage. +message ServerEntry { + Server server = 1; + + google.protobuf.Timestamp last_ping = 2; + + google.protobuf.Timestamp last_healthy = 3; +} + +// Configuration stores static server configuration. +message Configuration { + repeated Server servers = 1; +} + +// Server is the server data provided by a node. +message Server { + enum Type { + TYPE_UNSPECIFIED = 0; + TYPE_LOGIN_SERVER = 1; + TYPE_GAME_SERVER = 2; + TYPE_MESSAGE_SERVER = 3; + } + + Type type = 1; + string name = 2; + uint32 id = 3; + uint32 num_users = 4; + uint32 max_users = 5; + string address = 6; + uint32 port = 7; + uint32 flags = 8; +} + +message AddServerRequest { + Server server = 1; +} + +message AddServerResponse { +} + +message ListServersRequest { + Server.Type type = 1; +} + +message ListServersResponse { + repeated Server server = 1; +} + +message GetServerRequest { + uint32 id = 1; +} + +message GetServerResponse { + Server server = 1; +} + +service TopologyService { + rpc AddServer (AddServerRequest) returns (AddServerResponse); + rpc ListServers (ListServersRequest) returns (ListServersResponse); + rpc GetServer (GetServerRequest) returns (GetServerResponse); +} diff --git a/qa/authserv/server.go b/qa/authserv/server.go new file mode 100755 index 0000000..e2b9c29 --- /dev/null +++ b/qa/authserv/server.go @@ -0,0 +1,45 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package authserv + +import ( + "net/http" + "strconv" + + log "github.com/sirupsen/logrus" +) + +type Listener struct { +} + +func New() *Listener { + return &Listener{} +} + +func serveData(w http.ResponseWriter, data []byte) { + w.Header().Set("Content-Length", strconv.Itoa(len(data))) + w.Write(data) +} + +func (l *Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Println(r.Method, r.URL.String()) + switch r.URL.Path { + case "/Secure/Login/LoginForGame.php", "/qalogin": + serveData(w, ([]byte)(`true123412341234`)) + } +} diff --git a/queries/character.sql b/queries/character.sql new file mode 100644 index 0000000..3c13a8e --- /dev/null +++ b/queries/character.sql @@ -0,0 +1,21 @@ +-- name: GetCharacter :one +SELECT * FROM character +WHERE character_id = ? LIMIT 1; + +-- name: GetCharactersByPlayer :many +SELECT * FROM character +WHERE player_id = ?; + +-- name: PlayerHasCharacters :one +SELECT count(*) > 0 FROM character +WHERE player_id = ?; + +-- name: CreateCharacter :one +INSERT INTO character ( + player_id, + character_type_id, + character_data +) VALUES ( + ?, ?, ? +) +RETURNING *; diff --git a/queries/player.sql b/queries/player.sql new file mode 100644 index 0000000..723fb7f --- /dev/null +++ b/queries/player.sql @@ -0,0 +1,20 @@ +-- name: GetPlayer :one +SELECT * FROM player +WHERE player_id = ? LIMIT 1; + +-- name: GetPlayerByUsername :one +SELECT * FROM player +WHERE username = ? LIMIT 1; + +-- name: CreatePlayer :one +INSERT INTO player ( + username, + nickname, + password_hash +) VALUES ( + ?, ?, ? +) +RETURNING *; + +-- name: SetPlayerNickname :one +UPDATE player SET nickname = ? WHERE player_id = ? RETURNING *; diff --git a/queries/session.sql b/queries/session.sql new file mode 100644 index 0000000..b7d1c63 --- /dev/null +++ b/queries/session.sql @@ -0,0 +1,28 @@ +-- name: GetSession :one +SELECT * FROM session +WHERE session_id = ? LIMIT 1; + +-- name: GetSessionByKey :one +SELECT * FROM session +WHERE session_key = ? LIMIT 1; + +-- name: GetSessionsByPlayer :many +SELECT * FROM session +WHERE player_id = ?; + +-- name: CreateSession :one +INSERT INTO session ( + player_id, + session_key, + session_address, + session_expires_at +) VALUES ( + ?, ?, ?, ? +) +RETURNING *; + +-- name: UpdateSessionExpiry :one +UPDATE session SET session_expires_at = ? WHERE session_id = ? RETURNING *; + +-- name: DeleteExpiredSessions :exec +DELETE FROM session WHERE session_expires_at < ?; diff --git a/res/embed.go b/res/embed.go new file mode 100644 index 0000000..054a7dd --- /dev/null +++ b/res/embed.go @@ -0,0 +1,23 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package res + +import _ "embed" + +//go:embed pangbox.png +var PangboxPNG []byte diff --git a/res/pangbox.ico b/res/pangbox.ico new file mode 100755 index 0000000000000000000000000000000000000000..7d54a508aca7924b4ef2ec81141c6afedba2ae8b GIT binary patch literal 102134 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 YtHe7wep6$`(qH^$1BVkg9FYP1{{XYi@c;k- literal 0 HcmV?d00001 diff --git a/res/pangbox.png b/res/pangbox.png new file mode 100644 index 0000000000000000000000000000000000000000..20049a71a13e133d0d43c8d9c078a8f604d1b9ee GIT binary patch literal 5367 zcmb7Ig>`55J$k;J5qC%=7F#J2Sicn%U=>2u*cG3NRxW000VQC3&qIiTp<(;v3!}Wzlyd zSgn+_)BwPj^JX0d0O$YoYXIN@1Aq;40C<`V0Q4^D-(E=ED2Odp6y<^Ie>1zOAOQeq zk;?M2I^L7p8D8$Xy3IW;7wJ<7w-L5b88{ro;SRSAqoxKaQ^PRfwZO)3{vw}^|6^c{IS()HlEPk>rceIAnmCWo? z1lm^~n81~s(+nc;$eVU+Nx>ud8g#?I8?W8`i6eagAo%c$r*Qq;;qRa5iy3EnnV<0@ zE5WfEz*?y@rlo8k(d10IM2flOR645jcphiXHX@P z&Bgh@tt9P-fK_XyAy+T+b5drT3lJ_I0se96T*r*H!qUMGC(p9OtvP*8PB%@<-K|Zb z`kYT}=FM`W%_Esg`~lV?ezpD;hCqW{ck7#77>TCfTH!nMn#%-$vV+a!=}&Lt*}jiB z*A&1FQWl9wV4#{m&1}w9{q*~ne_T?1riV4&+XxKi*fHu8E)stvUDYzSG-D#`tG@?} z{_=MY{N$>m0Z?TvHPXD>ov$sjnGrLrAL1ew*e6HI>@<{qomK)~b*LpY_9kg%l4@}g zYvX#qg82czp!txXo>Xp(GpRJ;5GC?>P;S?xjQjG-#+ z?yPNF`47DuW67%H5jvK}&&X!@&alWQ&F5nWI5kq;x$g#aI{i%>4c0K(zBg2e+`)C7;Tj{ zto&5r@|s;!{p%mP)y3Nr97_SbTG`>l%lx2gC0h#?KDag~ysOCb>L9)!Ai% z5k)V~z~Jmw21wyUWl3_`_^;$rdw^>lRP=fjtl%WU3Pqo+W< zz#FeHK=Aa}y~j$NjAjG3qQUVNW@_T(oNbD*M|R_RMoH=rI`IU>0zR1?%PVSp4Es_= z3jGjfU`fGj0I52U=@h_S0 zAg|w`-qiH`a~e+$+t3b(c{hI3hd3cL-qGBl4QMY(l!BOOT)&m_+`+oR0yb|LrP~ue zm$SsSZX}S&Q|$5uIdhNKOgb75hp&hAhQDL#vmB+&r{lDt&bM@_{G!JFjy&-dXH0tY z$wEX%*@wLCN}|{Y$-2NT)Fu71qFBcJUK=@*bR`NPuXlxcr?n0;c`_FtnCowk3KMhS>Xxy^qy_@>3#t86j z%FN;WOB(c=T~lEXo(@svvVjQ0m)>a~{t3p!q79@w=LscMf5v|d;pV^^p?iv6=8=fo zFo3tiwt6W1P9haV1S4dmD-}O>bV2xaX4Cov2he1SrG^uR`xAWS<8;^h zvyvCieYQ%;E$nuvKj13y3>#bXN|$*Sqf{9^m!lc_Q2F4%pcZT&mzv8eh&L+GIKhFm z(*Mo}grF{O4sG&zhjNK9nXy}t8~p7D&F$3(2M1#{tllBm-FX)xV{;~!Y%9{&3h$1; zuElO$ew;YTv;G}&pARR<){-d0aPDBzf=uF+h=+QYCiZ*bD8!X(R@(7_@06dq?5gk4 ze!k*|@6-DYS3N+McAej^stdF)+$19bNbU71HDp0Pzf5nhk{n+%n~`k2e*x zT_+-9eW*j^E53S5Ajnqm0nw&b1JxjW<=}g+zPl*r7s}4oJOflHp=^Ri zn9!X+hvQ-iQteJ{)aJL~-hIwtlgeXL)qHwI^;CkoCs>RD`nYrDTQDkynz>jGsSrA{ z+Si{EP6^G#t#>7$`MdMeiPK?Qj^J|{Xw&D8xXjm$bjY%j(7H2GTDoOcr(kj2tN0bJ zOaLg9z8su-S;ZTuBcSN;ik2}v2Kj>ih}HOfoOVmhn^>fe<%rY(Ji-U;rWW+nZ@&)1 z{gSidoAR$Khp>ktG+rx$rl9n`8!R=5Q%*1o4wia?^F zGmd!>dm#Gl#=laDoDr5o05fMAZG+h zP(b9t6y$dfn_fp?By>}BT2?|9iObirXk2goG_h-&i=wtS3SD>^?&Wp9Ly0Jf8A_3Q z=kcd7QSXjI=?27QpUd+8B~`uJqFAwuSE$#?`#1XM@O{kV^H@F^ksce)1E;UsCKQMk z++8I|cG{u`nK|OlKXk7Z9ayeV;kx&Q*>_ciNip6ipJ1n!=r z_|qkK6kW&hYZVm4{BM(D2N~}!x-k7*Xg(I0ikYpGI2x{k-mhhz+cquBJu?ajg+2IK zn3+PBpkOuMe2I17E3Yp%v3RjcX$0g7fPRhCn7`ZJPW&F{!mlF`o}e&i$wJ zSZBD*9605Z>uBeg33zfckHC2d4RdFwv6KEsiyu>36Kqsm8;>|GbK8aNZKb2`mYRfi zcSDQ?(C<^fDL~1~K;(n067u1w2BpfET-$MXyIgXL7~`TM)!Hq% z=$mHpsi;WT^$oRTZ>hb~U))0XIB3MQwP_#=R~`Dy^sPUyxcJL?OE#;VJDh2^q{32C za$y09f#96x90rzvMgki_)7uok$6vMnWw&u%J0n_BRqN{SA9B7xMN4{5L#psU8oksF z_n6)3?z_GaJ7E^65}jDsOq?z1E)Mj!syP_2Lx?O~w99#4N5P@*XD*`3dx&DiAM5Ju zkHPm=I;z6MzCcQGw!Azg=@*o(LkUUy$1875`&xR^kfcQfGz>J+YT_T}E|)ih=bxf9 zD>qF?U=~L*SsM}_d$=&k!Um&`O?t?!cL8s$vh^NlnrO!yCRKC{{rnA6YjXw&T888aBxm0 znqi0~4D(x6uq$k0 zRrCc#34I3S>N^A zKgM3u^%M{e2C@GSN%}vWr&6jn3(F8o4abp`;l{7c>Uo+q?)Q|{l@FCSly`Cw4a#~a zQ@)UtYn!y)oWj6t1}D3>2Y8v*(s_JPmV51X4i&rLmE7VwyioK9i`lrECtN7Y&+UVP zGl$nnEDi@H~6G=%hFRMIKYOBsMF5u-lgp`#&mS5;`PdY> z;#A^GEnl(-PtZYcgbGuACQaLaXS^{lenM-tn{%u7Yt%Kd{!RT3I0E{8y~%ft)*j$L z;ty)~}gZ->RE3wEd=%H(_}&5tzR1u2xZ{wemQO)_q~LA_6S8C3z*>2sWf z05U$RWg#mIvUl=%VBL1Hz~;=f%d1tG{8N8l=hSW>mc~ua>LJ+k?z6V?+$SRu5#(}u zE8G={ZHxG4uloXqew>C!if|Pn9zg~MS|^Xi=llH=1!O{2KP|g7cP>1*vH$J-wW(KN ziT4}MJW7;8S*N1eUyxkMl@)HL>E51WK%Kr9v5zre$9`_K1wL4f6tR(ij<46!0FvIi zWVnb)r!&bNv`a(t=zC#!E{dV}7M_1ZyMkZue zjJm=}qE5PmA02(gw7i-Ld#VxnAdxLK4Fr|ABn3^MCIL5UM7K%`0-r2kZx57Idz6yC z_#Jw3l+S((X_%3uVVIxf_(gexiQFwiBFkKM>f3ajP=QCHKtc~y+tR+&b%n!hGH8SE z83}eS>b;-Lx?~W4pd(K=`R?wpACs6X4ND*n0afFq#^*~<2vT0u!k8SXFOEdXo|p^2 z@UP#V3t?^;deweYHw9X-HyoC_v#pUwd*(bPTe1Djg(Cog$FrlCTQuQj(mb&-nabkH z+@&F%RpXKt`;>Y@TY;>*O1-9N0y)d$Fg-Ty4Q`=t-3IRz8@{62zbhuv$Lm^9J;sy4 zGb9=(2^YJdbk^6oXaX*qNedBkKY8U*8}f_O4R4bcJu`X+()XyHQ#~luW&{EX_Wkho z26)_>;~W&6OxSm)0!lEEUP&4x&oduQIjY)R5CjMUL|BayL&H$smAwcRjCNh3)}{YK zwYfy|kCU;ZGTwg{)b9PEg{=GW?d5fpU4S(B z-y<^6L{7C_&6I0{F;{`J>`wB+@DKt`-4W!XO+b~y@9m;D`QErPEbJb~e!#RZlR$H? z-ILG0VV6w1b?~K+c5GJC`d$mQRc?onUFo$30fC7;;Fdy;6-bNNgJqb^ z)44G)XGzl)btJ?-5oYN7`-W%9?ZOkHh$5L!xlECIBlES!J>sTctRJ8zNB-PkU!`U)CMnoDA-KGq;GbmBE= zO$Y!%c#KQfzRu}VSMElGFmbQV!Er~VUYWdn10_L%FFSW^prWfg7ABD(efz7&dr`mj zr#pIMhMj84ALNjAn>zj^CeMMpgv5nHo=ZT%0`Yn+ROCl9L8eKNzz|8S3$rrthw1-A z#~g5AepO?J#w z%MHc%KuE`jEl=N6^-nFwy2L+R?JLaym)>o_hcSo{cvoupVuMD~okngoYZ&>7pRkBI z?7VVJN~tplUvzY8axX4?VrQVhf=^C@gvi|if#NSB0_#lL>&JDxp#=kc01?i6imT$f z?@k5ld}}#nZ<9csP`78zVE&Tm-Jo9@OH_b{)`QV?!r~FK6K+%(!%F&e>=Fc zNMKX|9x}nE;`2?K-F$$(=-$k;as}-gygOh@%f-5om7j^Xs$Dh9^ z%C1kX%-0&yX@zrGXlfx)VT3ZdTJUziM_-|ux_|yxWY>!NY#nQ%=PAIY&*mguAblj_ zP*vw-LFDyq_*hWuVc^5qHS%Lt_n|Qjvspe!?-%|S_u@79@L<0ODhDqc3cjbtav{oz zbc9v+Zv|`UF;&rx=V-^(d@BEt4;%O_1L0nf9a5<#Hx2uYM<^nr`CKipm0QGF;8H2D kV`?J)zc%2W(_fb0>W}+x&1UItx|4wNGj;iLxOwpZ0i%CY1ONa4 literal 0 HcmV?d00001 diff --git a/res/pangbox.svg b/res/pangbox.svg new file mode 100755 index 0000000..7b2a08a --- /dev/null +++ b/res/pangbox.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..b90a282 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: +- engine: "sqlite" + queries: "queries" + schema: "migrations" + gen: + go: + out: "gen/dbmodels" diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..41fded1 --- /dev/null +++ b/tools.go @@ -0,0 +1,28 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +//go:build tools + +package tools + +import ( + _ "github.com/akavel/rsrc" + _ "github.com/bufbuild/buf/cmd/buf" + _ "github.com/josephspurrier/goversioninfo" + _ "github.com/kyleconroy/sqlc/cmd/sqlc" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" +) diff --git a/web/assets/style.css b/web/assets/style.css new file mode 100644 index 0000000..9ed4caf --- /dev/null +++ b/web/assets/style.css @@ -0,0 +1,53 @@ +html { + font-family: sans-serif; + display: flex; + align-items: center; + justify-content: center; + background: #eee; + height: 100vh; +} + +.window { + margin: 1em; + padding: 1em; + background: rgba(255, 255, 255, 95%); + border-radius: 10px; + border: 2px solid #ddd; + box-shadow: 0px 5px 5px rgba(0, 0, 0, 10%); +} + +h1, h2, h3, h4, h5, h6 { + margin: 0.2em 0; +} + +.window h1 { + text-align: center; +} + +fieldset { + border: none; + padding: 0; + margin: 0; +} + +.form-layout { + display: table; +} + +.form-layout .form-control { + display: table-row; +} + +.form-layout .form-control label, +.form-layout .form-control input, +.form-layout .form-control .skip-column { + display: table-cell; + margin: 0.5em; +} + +.errors { + background-color: #ffaaaa; + border-radius: 10px; + padding: 5px; + margin: 5px; +} diff --git a/web/data.go b/web/data.go new file mode 100644 index 0000000..aac3bb7 --- /dev/null +++ b/web/data.go @@ -0,0 +1,52 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package web + +import ( + _ "embed" + "encoding/base64" + "net/http" + "strconv" + + "github.com/julienschmidt/httprouter" +) + +//go:embed data/translation.xml +var translation []byte +var translationB64 = []byte(base64.StdEncoding.EncodeToString(translation)) + +//go:embed data/extracontents.xml +var extraContents []byte + +//go:embed data/pangya_default.xml +var pangyaDefault []byte + +func (l *Handler) serveTranslations(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + w.Header().Set("Content-Length", strconv.Itoa(len(translationB64))) + w.Write(translationB64) +} + +func (l *Handler) serveExtraContents(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + w.Header().Set("Content-Length", strconv.Itoa(len(extraContents))) + w.Write(extraContents) +} + +func (l *Handler) servePangyaDefault(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + w.Header().Set("Content-Length", strconv.Itoa(len(pangyaDefault))) + w.Write(pangyaDefault) +} diff --git a/web/data/extracontents.xml b/web/data/extracontents.xml new file mode 100644 index 0000000..96675d6 --- /dev/null +++ b/web/data/extracontents.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/web/data/pangya_default.xml b/web/data/pangya_default.xml new file mode 100644 index 0000000..d6858c7 --- /dev/null +++ b/web/data/pangya_default.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/data/translation.xml b/web/data/translation.xml new file mode 100644 index 0000000..0de3ace --- /dev/null +++ b/web/data/translation.xml @@ -0,0 +1,5 @@ +2014061902Rewards from Grand Prix event2014061901Play to Win Event Rewards items.200400Do you want to receive all of the items on this page? \n*Please take note of the time limit for Mascot.200300Passcode change200223You have completed the mission. /n Rewards will be in your Pangya mailbox.200222Rewards will be presented to you in your Pangya mailbox.200221Play in West Wiz, Wiz Wiz, White Wiz and Wiz City200220Used Arin Comets \n %d / %d comets200219Use 30 Arin Comets200218Hole count \n%d / %d times200217Play 18 holes twice with Arin.200216Attendance \n%d / %d days200215Login for 5 days200214Pangya Shot success rate \n%d / %d times200213Hit Pangya 500 times200212Can you see this pretty shiny circle?\nIt was a wonderful magic show. \nThank you everyone for your help. I am so happy !200211Finally the magic circle is completed.\nIt is so beautiful !200210Mission clear !\nI can feel the magic power arising.200209The circle shown here can summon the gift of magic power.\nBut I need you to clear a mission. It shoudn't be too difficult.200208This year, I'm going to perform a magic show. \nI'll need your help.200207Hello. Thank you everyone for coming. \nThank you for being my audience.200206Wow ~ ! \nYou have collected all the ticket pieces. \nQuckily enter as the Magic Show is about to start very soon.200205You can get a magic show ticket \nby collecting the pieces.200204I'm sorry. \nYou do not have an admission ticket.200203Welcome to Arin's Magical Show ! \nDo you have a ticket?200202Have a good time !200201Congratulations, you have a ticket ! \nYou may enter now.200200Welcome to Arin's Magical Show ! \nDo you have a ticket?200112For giving %s (%d) to character, you have recieved a special effect.200111For giving %s (%d) to character, you have recieved a special item.200110This is too small, \n we need to get a bigger space.200109Feeling something in my heart. \n I don’t know what is it.200108Accuracy is important. \n And shooting too.200107Calm down to make\n the right decision.200106Don’t forget that feeling \n when you hit Pangya.200105This is my experience as the \n Lunar Tomb’s Captain!200104Listen my song. \n Don’t you like my voice?200103Too many pumpkins around \n in Halloween.200102Spooky furniture. \n Would you want it?200101Try on my new clothes ! \n Do you like them?200100You're invited to my \n Magic Show next month.200001No Stats100502Check My Room for the prize.100501Scratch Result100432This slot is currently disabled.100431Special accessory required to activate this slot.100430%s required.100429Confirm to unequip card to\n%s slot?100428Confirm to equip card to\n%s slot?100427Bonus100426Does not use Card Patcher.100425Uses 1 Card Patcher.100424<%s> Cards\nunequipped.100423<%s> Cards\nequipped.100422<%s> slot\nhas expanded.100421After you have reached this point,\nyou will get Control and Accuracy -1 penalty.100420Power slot bonus depends on rank.\nNext Bonus Rank : [%s] 100419Every rank level-up\nyou will get +1 Power Slot Bonus\nand 1 Power Penalty\ndecrease.100418Spend [%d] TP to\nexpand the slot?100417[%s] Achievement done.100416[%s] Reached100415Current TP : %d100414Required TP : %d100413[%d] more Slot count is required.100412Over [%d] Power Points\n[-%d] Points Penalty100411\nCard : %d100410\nParts : %d100409\nCharacter Mastery : %d100408Special Card : %d100407NPC Card : %d100406Character Primary Ability100405*After [%d] Power points you will get\n-1 Control and Accuracy points penalty.100404Do you want to downgrade the \n[%s ability]?100403To upgrade the [%s ability]\nyou need [%d Pangs].\nConfirm upgrading?100402<%s> stat\nhas downgraded.100401<%s> stat\nhas upgraded.100301"It shows the predicted trajectory of your comet due to the wind. +The Miracle Sign's effect is automatically triggered when your comet is on the green. +When using these functions, you will receive 30% less pangs. +Pang deduction penalty applies only to players ranked from Junior E to Infinity Legend A"100201(Short Game)100113Insufficent Pangs.\nPlease try again later.100112%d Pang is required. +100111Please select the quantity.100110You already have over 20000 TP.\nPlease retry after you spend your TP.100109%d mileage earned.100107\\c0xffff0000\\cCongratulations.\\c0xff000000\\c\nYou earned extra %d mileage! Success!\nTotal %d mileage earned.100106\\c0xffff0000\\c[Warning]\\c0xff000000\\c This process can not be undone.100105Are you sure to recycle?100104\\c0xffff0000\\c[Warning]\\c0xff000000\\c Rare cards are included.100103\\c0xffff0000\\c[Warning]\\c0xff000000\\c Club Modification kit included.100102\\c0xffff0000\\c[Warning]\\c0xff000000\\c Rare items are included.100101\\c0xffff0000\\c[Warning]\\c0xff000000\\c Self-design items are included.100100Would like to recycle your item and get mileage?100031Wow! Congratulations! You earned TP Points~\nYou can use your TP points while combining,merging and upgrading your character mastery.100022Your mileage is less than 1000.\nYou can get more mileage by recycling the better items.100021Ummm… As you know many a mickle makes a muckle…\nWhy don't you be more brave and try more?100012Umm… Do you have any items to recyle?\nPlease be careful to not recycle your RARE items!100011Emm… Sometimes you can get extra mileage.\nThis depends on your luck. Good luck!100002You can get TP points when you have 1000 Mileage points.\nYou can get mileage when you recycle the items.100001Ooooh, you can get TP points by recycling the items.\nDo you have any items to recyle?95003"If you already have a Decoration item\nit will be dissapear.\nConfirm?"95002You can combine and create new items with Cadie's Cauldron.\nSo lets create new items!95001※ Important Notice about Auto Calipers\n\n1) If you already used some of the Auto Calipers you can not refund the remaining ones.\n2) The items you get by exchanging the Auto Caliopers can not be refunded.\n\nConfirm the combination process?90007Adjust the brightness.90006Turn ON/OFF the Anti-aliasing.90005Turn ON/OFF the Contrast.90004Turn ON/OFF the Rim Light.90003Turn ON/OFF the DOF.90002Turn ON/OFF the HDR Star Effect.90001Turn ON/OFF the HDR Effect60005You cannot use Replay Tape in the Lounge.60004Your gifts have arrived.\nCheck your Mailbox.60003You have a new message.50005Not enough Pang to play BIG mode50004Check your received items in "My Room"50003Items Won.50002You didn't win anything.50001You have Insufficient Pang40004The comet has been blessed by %s's Animal Power and flies with precision.40003The comet has been blessed by %s's ring and flies with accuracy.40002Power transfer 100%40001Earned additional Pangya Combo gauge30039You need to be in the Multiplay mode to enter Lounge.30037Course : SPECIAL30036You can have up to 50 Grand Prix tickets and you can acquire more by completing multiplayer mode and logging in.30035Grand Prix Award Ceremony30034%s until game starts.30033%s acquired!!30032Please try again at another time.30031Special Rule +1 Penalty Stroke added30030You can now enter %s tourney.30029Class %d Tourney Completed %d%30028Tourney Skill Limit: only players with %d or higher average strokes can participate.30027Tourney Skill Limit: Only players with %d or lower average strokes can participate.30026Tourney Level Limit: Only %s to %s can participate.300253 X Hole Cup300242 X Hole Cup30023Special rule :30022Prize : %s30021Game Time : Start %02d:%02d End %02d:%02d30020You cannot select your equipments for equipment restricted tourney.30019Grand Prix30018%d Difficulty30017Entry Ticket : %d30016Alarm has been set, you will be notified via in-game announcement 10 minutes before the tourney.30015You must finish progressing %s to enter.30014%d : %d %s entry has started, please enter the waiting room now to participate.30013Entry time alarm has been set.30012(If you previously had an alarm set up, recent alarm will replace it.)30011You cannot enter at this time.\nWould you like to set an alarm for the entry time?30010%d tickets will be consumed when participating in the tourney.\nTickets will be used when the tourney starts.)30009Game has already ended.30008Game has already started.30007You do not have the required ticket.30006You do not have the required equipment.30005Please check the rules and restrictions of the tourney.30004Your average stokes are lower than the recommended level.\nThis mode might be too easy for you.\n\nYou can look at your average strokes in "My Profile>Advanced"30003Your average strokes are higher than the recommended level.\nThis mode might be too difficult for you to participate.\n\nYou can look at your average strokes in "My Profile>Advanced".30002Your level is too low to participate.30001Your level is too high to participate.6006Scratchy Card system is under maintenance. Sorry for the inconvenience.6005Event Scratchy system is under maintenance. Sorry for the inconvenience.6004Recycle system is under maintenance. Sorry for the inconvenience.6003Card system is under maintenance. Sorry for the inconvenience.6002Character Mastery system is under maintenance. Sorry for the inconvenience.600190090007102Translation request: "System Error" Sorry for the inconvenience.101Disconnected from server due to network problems (socked error: %d) \ No newline at end of file diff --git a/web/handler.go b/web/handler.go new file mode 100755 index 0000000..c997476 --- /dev/null +++ b/web/handler.go @@ -0,0 +1,91 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package web + +import ( + "io/fs" + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/pangbox/pangfiles/crypto/pyxtea" + "github.com/pangbox/server/database/accounts" + log "github.com/sirupsen/logrus" +) + +const maxFormSize = 4096 + +type UpdateListOptions struct { + Key pyxtea.Key + Dir string +} + +type Options struct { + ServePangYaData bool + UpdateList *UpdateListOptions + AccountsService *accounts.Service +} + +type Handler struct { + router httprouter.Router + updateHandler *updateHandler + accountsService *accounts.Service +} + +func New(opt Options) *Handler { + listener := &Handler{ + router: *httprouter.New(), + accountsService: opt.AccountsService, + } + + if opt.UpdateList != nil { + listener.updateHandler = newUpdateListHandler(opt.UpdateList.Key, opt.UpdateList.Dir) + } + + assets, err := fs.Sub(assetFS, "assets") + if err != nil { + log.Fatalf("Error getting assets directory: %v", err) + } + listener.router.ServeFiles("/static/*filepath", http.FS(assets)) + listener.router.GET("/register", listener.handleRegisterGet) + listener.router.POST("/register", listener.handleRegisterPost) + listener.router.GET("/", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + http.Redirect(w, r, "/register", http.StatusFound) + }) + + // Update list paths + if listener.updateHandler != nil { + listener.router.GET("/pangya/season4/patch/updatelist", listener.handleUpdateList) + listener.router.GET("/pangya/season4/patch/qa/updatelist", listener.handleUpdateList) + listener.router.GET("/new/Service/S4_Patch/updatelist", listener.handleUpdateList) + } + + // PangYa game data + if opt.ServePangYaData { + listener.router.GET("/Translation/Read.aspx", listener.serveTranslations) + listener.router.GET("/new/Service/S4_Patch/extracontents/extracontents.xml", listener.serveExtraContents) + listener.router.GET("/pangya/season4/patch/extracontents/extracontents.xml", listener.serveExtraContents) + listener.router.GET("/S4_Patch/extracontents/default/pangya_default.xml", listener.servePangyaDefault) + } + + return listener +} + +func (l *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.Debugf("WEB: %s %s", r.Method, r.URL.String()) + l.router.ServeHTTP(w, r) +} diff --git a/web/templates/register.html b/web/templates/register.html new file mode 100644 index 0000000..f8abef6 --- /dev/null +++ b/web/templates/register.html @@ -0,0 +1,37 @@ +{{ define "register" }} + + + + Pangbox - Register + + + +

+ + +{{ end }} diff --git a/web/templates/register_complete.html b/web/templates/register_complete.html new file mode 100644 index 0000000..62a9fab --- /dev/null +++ b/web/templates/register_complete.html @@ -0,0 +1,17 @@ +{{ define "register_complete" }} + + + + Pangbox - Register + + + +
+

Registration Completed

+
+ Log in via PangYa to continue. +
+
+ + +{{ end }} diff --git a/web/ui.go b/web/ui.go new file mode 100644 index 0000000..8b98a3f --- /dev/null +++ b/web/ui.go @@ -0,0 +1,106 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package web + +import ( + "crypto/md5" + "embed" + "encoding/hex" + "fmt" + "html/template" + "io" + "net/http" + "net/url" + "strings" + + "github.com/julienschmidt/httprouter" + log "github.com/sirupsen/logrus" +) + +//go:embed assets/* +var assetFS embed.FS + +//go:embed templates/*.html +var templateFS embed.FS + +var templates = template.Must(template.ParseFS(templateFS, "templates/*.html")) + +type RegisterPageParams struct { + Errors []string +} + +func (l *Handler) renderRegisterPage(w http.ResponseWriter, params RegisterPageParams) { + if err := templates.ExecuteTemplate(w, "register", params); err != nil { + log.Errorf("Error executing register template: %v", err) + } +} + +func (l *Handler) handleRegisterGet(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + l.renderRegisterPage(w, RegisterPageParams{}) +} + +func (l *Handler) handleRegisterPost(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + formdata, err := io.ReadAll(io.LimitReader(r.Body, maxFormSize)) + if err != nil { + l.renderRegisterPage(w, RegisterPageParams{ + Errors: []string{err.Error()}, + }) + return + } + values, err := url.ParseQuery(string(formdata)) + if err != nil { + l.renderRegisterPage(w, RegisterPageParams{ + Errors: []string{err.Error()}, + }) + return + } + formErrors := []string{} + username := values.Get("username") + password := values.Get("password") + if len(username) < 3 { + formErrors = append(formErrors, "Username too short.") + } + if len(username) > 22 { + formErrors = append(formErrors, "Username too long.") + } + if len(password) < 5 { + formErrors = append(formErrors, "Password too short.") + } + if len(formErrors) > 0 { + l.renderRegisterPage(w, RegisterPageParams{ + Errors: formErrors, + }) + return + } + + // The client will MD5 the password before sending it. + passwordMD5 := md5.Sum([]byte(password)) + passwordMD5Hex := strings.ToUpper(hex.EncodeToString(passwordMD5[:])) + + _, err = l.accountsService.Register(r.Context(), username, passwordMD5Hex) + if err != nil { + l.renderRegisterPage(w, RegisterPageParams{ + Errors: []string{fmt.Sprintf("An error occurred: %v", err)}, + }) + return + } + + if err := templates.ExecuteTemplate(w, "register_complete", nil); err != nil { + log.Errorf("Error executing register template: %v", err) + } +} diff --git a/web/updatelist.go b/web/updatelist.go new file mode 100644 index 0000000..2790459 --- /dev/null +++ b/web/updatelist.go @@ -0,0 +1,162 @@ +// Copyright (C) 2018-2023, John Chadwick +// +// Permission to use, copy, modify, and/or distribute this software for any purpose +// with or without fee is hereby granted, provided that the above copyright notice +// and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +// OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +// THIS SOFTWARE. +// +// SPDX-FileCopyrightText: Copyright (c) 2018-2023 John Chadwick +// SPDX-License-Identifier: ISC + +package web + +import ( + "bytes" + "io" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/julienschmidt/httprouter" + "github.com/pangbox/pangfiles/crypto/pyxtea" + "github.com/pangbox/pangfiles/encoding/litexml" + "github.com/pangbox/pangfiles/updatelist" + log "github.com/sirupsen/logrus" +) + +func (l *Handler) handleUpdateList(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + if err := l.updateHandler.updateList(w); err != nil { + log.Printf("Error writing updateList: %v", err) + + w.WriteHeader(500) + w.Write([]byte("server error")) + } +} + +type updateListCacheEntry struct { + modTime time.Time + fSize int64 + fInfo updatelist.FileInfo +} + +type updateHandler struct { + key pyxtea.Key + dir string + cache map[string]updateListCacheEntry + mutex sync.RWMutex +} + +func newUpdateListHandler(key pyxtea.Key, dir string) *updateHandler { + ul := &updateHandler{ + key: key, + dir: dir, + cache: map[string]updateListCacheEntry{}, + } + + // Warm the updatelist. + go ul.updateList(io.Discard) + + return ul +} + +func (s *updateHandler) calcEntry(wg *sync.WaitGroup, entry *updatelist.FileInfo, f os.FileInfo) { + defer wg.Done() + var err error + + name := f.Name() + *entry, err = updatelist.MakeFileInfo(s.dir, "", f, f.Size()) + + if err != nil { + log.Printf("Error calculating entry for %s: %s", name, err) + entry.Filename = name + } else { + s.mutex.Lock() + defer s.mutex.Unlock() + + s.cache[name] = updateListCacheEntry{ + modTime: f.ModTime(), + fSize: f.Size(), + fInfo: *entry, + } + } +} + +func (s *updateHandler) updateList(rw io.Writer) error { + start := time.Now() + + files, err := os.ReadDir(s.dir) + if err != nil { + return err + } + + doc := updatelist.Document{} + doc.Info.Version = "1.0" + doc.Info.Encoding = "euc-kr" + doc.Info.Standalone = "yes" + doc.PatchVer = "FakeVer" + doc.PatchNum = 9999 + doc.UpdateListVer = "20090331" + + hit, miss := 0, 0 + + var wg sync.WaitGroup + doc.UpdateFiles.Files = make([]updatelist.FileInfo, 0, len(files)) + for _, f := range files { + if f.IsDir() { + continue + } + name := f.Name() + if !strings.HasSuffix(name, ".pak") { + continue + } + + s.mutex.RLock() + cache, ok := s.cache[name] + s.mutex.RUnlock() + + info, err := f.Info() + if err != nil { + panic(err) + } + if ok && cache.modTime == info.ModTime() && cache.fSize == info.Size() { + // Cache hit + hit++ + doc.UpdateFiles.Files = append(doc.UpdateFiles.Files, cache.fInfo) + doc.UpdateFiles.Count++ + } else { + // Cache miss, calculate concurrently. + miss++ + doc.UpdateFiles.Files = append(doc.UpdateFiles.Files, updatelist.FileInfo{}) + doc.UpdateFiles.Count++ + entry := &doc.UpdateFiles.Files[len(doc.UpdateFiles.Files)-1] + wg.Add(1) + go s.calcEntry(&wg, entry, info) + } + } + if doc.UpdateFiles.Count == 0 { + log.Errorf("Did not find pak files; did you set -pangya_dir?") + } + + wg.Wait() + + data, err := litexml.Marshal(doc) + if err != nil { + return err + } + + if err := pyxtea.EncipherStreamPadNull(s.key, bytes.NewReader(data), rw); err != nil { + return err + } + + log.Printf("Updatelist calculated in %s (cache hits: %d, misses: %d)", time.Since(start), hit, miss) + return nil +}