76 changed files with 3519 additions and 200 deletions
+154
View File
@@ -0,0 +1,154 @@
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
name: Build & Release
runs-on: ubuntu-latest
timeout-minutes: 30
env:
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
TAG_NAME: ${{ github.ref_name }}
steps:
- name: Install tools
run: apt-get update -qq && apt-get install -y -qq curl unzip zip
- name: Checkout Code
run: |
git config --global credential.helper store
echo "https://god:${{ secrets.TEKTON_RELEASE_TOKEN }}@git.klud.top" > ~/.git-credentials
git clone https://git.klud.top/danchie/tekton.git .
git checkout $TAG_NAME
- name: Setup Godot (Cached)
run: |
apt-get update -qq && apt-get install -y -qq unzip curl
if [ ! -f /cache/godot_4.6.3 ]; then
echo "Downloading Godot 4.6.3..."
curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.6.3-stable/Godot_v4.6.3-stable_linux.x86_64.zip"
unzip -q -o /tmp/godot.zip -d /cache/
mv /cache/Godot_v4.6.3-stable_linux.x86_64 /cache/godot_4.6.3
fi
cp /cache/godot_4.6.3 /usr/local/bin/godot
chmod +x /usr/local/bin/godot
mkdir -p ~/.local/share/godot/export_templates/4.6.3.stable
if [ ! -f /cache/Godot_v4.6.3-stable_export_templates.tpz ]; then
echo "Downloading templates..."
curl -sL -o /cache/Godot_v4.6.3-stable_export_templates.tpz \
"https://github.com/godotengine/godot-builds/releases/download/4.6.3-stable/Godot_v4.6.3-stable_export_templates.tpz"
fi
cd ~/.local/share/godot/export_templates/4.6.3.stable
unzip -q -o /cache/Godot_v4.6.3-stable_export_templates.tpz
mv templates/* .
rm -rf templates
cd $GITHUB_WORKSPACE
mkdir -p build
- name: Export Windows
run: |
mkdir -p build/windows
cp addons/godotsteam/libgodotsteam* build/windows/ 2>/dev/null || true
godot --headless --export-release "Windows Desktop" build/windows/tekton_armageddon_windows.exe || true
cd build/windows && zip -r ../tekton_armageddon_windows_${TAG_NAME}.zip .
- name: Export Linux
run: |
mkdir -p build/linux
godot --headless --export-release "Linux/X11" build/linux/tekton_armageddon_linux.x86_64 || true
cd build/linux && zip -r ../tekton_armageddon_linux_${TAG_NAME}.zip .
- name: Export macOS
run: |
mkdir -p build/macos
godot --headless --export-release "macOS" build/macos/tekton_armageddon_macos.zip 2>&1 | tail -5 || true
if [ -f build/macos/tekton_armageddon_macos.zip ]; then
mv build/macos/tekton_armageddon_macos.zip build/tekton_armageddon_macos_${TAG_NAME}.zip
fi
- name: Extract changelog
run: |
# Extract changelog for this tag version from CHANGELOG_DRAFT.md
V="${TAG_NAME#v}"
BODY=$(awk -v ver="[$V]" '
/^## / { if (found) exit }
/^## / && index($0, ver) { found=1; next }
found { print }
' CHANGELOG_DRAFT.md | sed 's/^ *//')
echo "CHANGELOG_BODY<<EOF" >> $GITHUB_ENV
echo "$BODY" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Gitea Release
run: |
set -e
API="https://git.klud.top/api/v1/repos/danchie/tekton/releases"
TAG="$TAG_NAME"
echo "Checking existing release for $TAG..."
RELEASE_JSON=$(curl -s -H "Authorization: token $GITEA_TOKEN" "$API/tags/$TAG" 2>/dev/null || echo "")
RELEASE_ID=$(echo "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*' || true)
if [ -z "$RELEASE_ID" ]; then
echo "Creating new release for $TAG..."
# Escape body for JSON
BODY_ESCAPED=$(echo "$CHANGELOG_BODY" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read().strip()))" 2>/dev/null || echo '""')
RELEASE_JSON=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":$BODY_ESCAPED,\"draft\":true}" \
"$API")
echo "API response: $RELEASE_JSON"
RELEASE_ID=$(echo "$RELEASE_JSON" | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
if [ -z "$RELEASE_ID" ]; then
echo "FATAL: Could not create release"
exit 1
fi
fi
echo "release_id=$RELEASE_ID"
echo "$RELEASE_ID" > /tmp/release_id.txt
- name: Upload Windows asset
run: |
RELEASE_ID=$(cat /tmp/release_id.txt)
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "attachment=@build/tekton_armageddon_windows_${TAG_NAME}.zip" \
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
echo "Windows uploaded"
- name: Upload Linux asset
run: |
RELEASE_ID=$(cat /tmp/release_id.txt)
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "attachment=@build/tekton_armageddon_linux_${TAG_NAME}.zip" \
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
echo "Linux uploaded"
- name: Upload macOS asset
run: |
RELEASE_ID=$(cat /tmp/release_id.txt)
if [ -f "build/tekton_armageddon_macos_${TAG_NAME}.zip" ]; then
curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "attachment=@build/tekton_armageddon_macos_${TAG_NAME}.zip" \
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID/assets"
echo "macOS uploaded"
else
echo "macOS asset not built, skipping"
fi
- name: Publish release
run: |
RELEASE_ID=$(cat /tmp/release_id.txt)
curl -s -X PATCH \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"draft":false}' \
"https://git.klud.top/api/v1/repos/danchie/tekton/releases/$RELEASE_ID"
echo "Published: https://git.klud.top/danchie/tekton/releases/tag/$TAG_NAME"
+68
View File
@@ -0,0 +1,68 @@
name: Deploy Patch
on:
workflow_dispatch:
inputs:
version:
description: "Patch version (e.g., 2.4.4)"
required: true
type: string
notes:
description: "Release notes"
required: false
type: string
jobs:
build-and-deploy:
name: Build & Deploy Patch
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository (shallow)
env:
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
run: |
git clone --depth 1 https://god:$GITEA_TOKEN@git.klud.top/danchie/tekton.git .
git config user.name "god"
git config user.email "god@noreply.git.klud.top"
- name: Setup Godot (Cached)
run: |
if [ ! -f /cache/godot_4.6.3 ]; then
echo "Downloading Godot 4.6.3..."
apt-get update -qq && apt-get install -y -qq unzip curl
curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.6.3-stable/Godot_v4.6.3-stable_linux.x86_64.zip"
unzip -q -o /tmp/godot.zip -d /cache/
mv /cache/Godot_v4.6.3-stable_linux.x86_64 /cache/godot_4.6.3
fi
cp /cache/godot_4.6.3 /usr/local/bin/godot
chmod +x /usr/local/bin/godot
mkdir -p build
- name: Generate version.json & bump version
env:
PATCH_VERSION: ${{ github.event.inputs.version }}
PATCH_NOTES: ${{ github.event.inputs.notes }}
run: |
python3 tools/generate_version_json.py --skip-changelog
- name: Export patch PCK
run: |
godot --headless --export-pack "Windows Desktop" build/patch.pck 2>&1 | tail -5
- name: Push to patches branch
env:
GITEA_TOKEN: ${{ secrets.TEKTON_RELEASE_TOKEN }}
run: |
mkdir -p patch-deploy
cp build/patch.pck patch-deploy/
cp assets/data/version.json patch-deploy/
cd patch-deploy
git init
git config user.name "god"
git config user.email "god@noreply.git.klud.top"
git remote add origin https://god:$GITEA_TOKEN@git.klud.top/danchie/tekton.git
git checkout -b patches
git add .
git commit -m "patch ${{ github.event.inputs.version }}"
git push -f origin patches
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

-81
View File
@@ -1,81 +0,0 @@
name: Build and Release Patch PCK
on:
push:
branches:
- 'patch-release'
paths:
- 'scripts/**'
- 'scenes/**'
- 'assets/**'
- 'CHANGELOG_DRAFT.md'
workflow_dispatch:
jobs:
build-and-deploy-patch:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# ── 1. Auto-generate version.json from CHANGELOG_DRAFT.md ────────────
- name: Generate Version JSON & Bump Version
run: python3 tools/generate_version_json.py
# ── 2. Commit bumped files back to the repo ───────────────────────────
- name: Commit Version Bump
run: |
git config user.name "PatchBot"
git config user.email "action@github.com"
git add assets/data/version.json project.godot CHANGELOG_DRAFT.md
git diff --staged --quiet || git commit -m "[AUTO] Version bump & changelog update"
git push
# ── 3. Detect changed files for patch PCK ────────────────────────────
- name: Generate Changed Files List
run: |
git diff --name-only HEAD^ HEAD -- 'scripts/**' 'scenes/**' 'assets/**' > changed_files.txt
echo "Files to patch:"
cat changed_files.txt
# ── 4. Build patch.pck ────────────────────────────────────────────────
- name: Setup Godot
uses: chickensoft-games/setup-godot@v1
with:
version: '4.3.0'
use-dotnet: false
- name: Run Build Patch Script
run: godot --headless -s tools/build_patch.gd
# ── 5. Push patch.pck to public repo ─────────────────────────────────
- name: Push patch.pck to Public Repository
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
with:
source_file: 'patch.pck'
destination_repo: '${{ github.actor }}/tekton-updates'
destination_folder: 'latest'
user_email: 'action@github.com'
user_name: 'PatchBot'
commit_message: '[AUTO] Pushed new patch.pck'
# ── 6. Push version.json to public repo ──────────────────────────────
- name: Push version.json to Public Repository
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
with:
source_file: 'assets/data/version.json'
destination_repo: '${{ github.actor }}/tekton-updates'
destination_folder: 'latest'
user_email: 'action@github.com'
user_name: 'PatchBot'
commit_message: '[AUTO] Pushed new version.json'
+130
View File
@@ -0,0 +1,130 @@
# Change Log
All notable changes to this project are documented below.
The format is based on [keep a changelog](http://keepachangelog.com/) and this project uses [semantic versioning](http://semver.org/).
## [3.3.1] - 2023-04-17
### Fixed
- Fix arguments for HTTPRequest.request() for beta17
- Fix typehints for enums in Godot 4.0-rc1
- Fix type check and typehint for Godot 4.0-rc3
- Fix null byte array error in GodotHttpAdapter for C#
## [3.3.0] - 2023-01-30
### Added
- Add support for subscription validation APIs that were added in Nakama v3.13.0
- Add support for sending events
- Allow disabling threads for making HTTP requests
- Add support for delete_account_sync() and other API changes that were added in Nakama v3.15.0
## [3.2.0] - 2022-08-30
### Fixed
- Fix NakamaSocket.add_matchmaker_party_async() and the tests for it
- Fix MatchData.op_code type in schema to TYPE_INT
- Fix circular reference in Nakama singleton to itself
### Added
- Add support for receiving binary data in "NakamaRTAPI.MatchState"
- Add support for sending and receiving binary data in "NakamaRTAPI.PartyData"
- Add NakamaMultiplayerBridge to integrate with Godot's High-Level Multiplayer API
## [3.1.0] - 2022-04-28
### Added
- Expose the "seen_before" property on "NakamaAPI.ApiValidatedPurchase"
- Add support for creating match by name
- Add support for "count_multple" on "NakamaSocket.add_matchmaker_async()" and "NakamaSocket.add_matchmaker_party_async()"
- Add C# support classes to better integrate the .NET client with the Mono version of Godot, allowing HTML5 exports to work
### Fixed
- Fix receiving "NakamaRTAPI.PartyClose" message
- Fix sending and receiving of PartyData
## [3.0.0] - 2022-03-28
### Added
- Add realtime party support.
- Add purchase validation functions.
- Add Apple authentication functions.
- Add "demote_group_users_async" function.
- A session can be refreshed on demand with "session_refresh_async" method.
- Session and/or refresh tokens can now be invalidated with a client logout.
- The client now supports session auto-refresh using refresh tokens. This is enabled by default.
- The client now supports auto-retrying failed request due to network error. This is enabled by defulut.
- The client now support cancelling requests in-flight via "client.cancel_request".
### Fixed
- Fix Dictionary serialization (e.g. "NakamaSocket.add_matchmaker_async" "p_string_props" and "p_numeric_props").
- Pass join metadata onwards into match join message.
- Don't stop processing messages when the game is paused.
- Fix "rpc_async", "rpc_async_with_key". Now uses GET request only if no payload is passed.
- Fix client errors parsing in Nakama 3.x
- Make it possible to omit the label and query on NakamaClient.list_matches_async().
### Backwards incompatible changes
- The "received_error" signal on "NakamaSocket" is now emited with an "NakamaRTAPI.Error" object received from the server.
Previously, it was emitted with an integer error code when the socket failed to connect.
If you have old code using the "received_error" signal, you can switch to the new "connection_error" signal, which was added to replace it.
## [2.1.0] - 2020-08-01
### Added
- Add an optional log level parameter to "Nakama.create_client".
### Changed
- Update variable definitions to new gdscript variable controls.
### Fixed
- Fix "add_friends_async" should have its "id" field input as optional.
- Fix "add_matchmaker_async" and "MatchmakerAdd" parameter assignment.
- Fix missing "presence" property in NakamaRTAPI.MatchData.
- Fix NakamaSocket not emitting "received_error" correctly.
- Fix "DEFAULT_LOG_LEVEL" in Nakama.gd not doing anything.
## [2.0.0] - 2020-04-02
### Added
- Decode base64 data in "MatchData". (breaks compat)
- Add "FacebookInstantGame" endpoints (link/unlink/authenticate).
- GDScript-style comments (removing all XML tags).
- Add "list_storage_objects_async" "p_user_id" parameter to allow listing user(s) objects.
### Fixed
- Fix encoding of "op_code" in "MatchDataSend" and marshalling of "NakamaSocket.send_match_state_[raw_]async".
- Fix parsing of "MatchmakerMatched" messages when no token is specified.
- Disable "HTTPRequest.use_threads" in HTML5 exports.
- "NakamaSession.is_expired" returned reversed result.
- Fix "NakamaClient.update_account_async" to allow updating account without username change.
- Fix "NakamaClient.update_group_async" to allow updating group without name change.
- Fix "HTTPAdapter._send_async" error catching for some edge cases.
- Fix "NakamaClient.send_rpc_async" with empty payload (will send empty string now).
- Fix "NakamaRTAPI.Status" parsing.
- Fix "NakamaClient" "list_leaderboard_records_around_owner_async" and "list_leaderboard_records_async" parameter order. (breaks compat)
- Rename "NakamaClient.JoinTournamentAsync" to "join_tournament_async" for consistent naming.
- Update all "p_limit" parameters default in "NakamaClient" to "10".
- Fix "NakamaRTAPI.Stream" parsing.
## [1.0.0] - 2020-01-28
### Added
- Initial public release.
- Client API implementation.
- Realtime Socket implementation.
- Helper singleton.
- Setup instructions.
+203
View File
@@ -0,0 +1,203 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
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.
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dhw5mg25iw5dr"
path="res://.godot/imported/dasher_getting_hit.glb-c78f6332496d820acfe217a7dee1a171.scn"
[deps]
source_file="res://assets/characters/dashers/dasher_getting_hit.glb"
dest_files=["res://.godot/imported/dasher_getting_hit.glb-c78f6332496d820acfe217a7dee1a171.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://du6f10iq0vup5"
path="res://.godot/imported/dasher_hit.glb-30828368c8a512ea3a0f74180da77e8d.scn"
[deps]
source_file="res://assets/characters/dashers/dasher_hit.glb"
dest_files=["res://.godot/imported/dasher_hit.glb-30828368c8a512ea3a0f74180da77e8d.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://c4noulpnr0sxc"
path="res://.godot/imported/dasher_hold.glb-91cd82b87dfb263f0f79ba3e311e0190.scn"
[deps]
source_file="res://assets/characters/dashers/dasher_hold.glb"
dest_files=["res://.godot/imported/dasher_hold.glb-91cd82b87dfb263f0f79ba3e311e0190.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://c1ymy6xseihds"
path="res://.godot/imported/dasher_put.glb-6349d495a8444cf30de4860b525f36c7.scn"
[deps]
source_file="res://assets/characters/dashers/dasher_put.glb"
dest_files=["res://.godot/imported/dasher_put.glb-6349d495a8444cf30de4860b525f36c7.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://3ipo6a5j5s48"
path="res://.godot/imported/dasher_stun.glb-5c67aab95f2516134af8dc025fb37bf5.scn"
[deps]
source_file="res://assets/characters/dashers/dasher_stun.glb"
dest_files=["res://.godot/imported/dasher_stun.glb-5c67aab95f2516134af8dc025fb37bf5.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://jqfmoyxrxuk6"
path="res://.godot/imported/dasher_take.glb-855d3406fda9c42eaacac6fd49155c0f.scn"
[deps]
source_file="res://assets/characters/dashers/dasher_take.glb"
dest_files=["res://.godot/imported/dasher_take.glb-855d3406fda9c42eaacac6fd49155c0f.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bt8pfg1j14lq4"
path="res://.godot/imported/tekton_hold.glb-200d797f4b202164003717fecbdfba97.scn"
[deps]
source_file="res://assets/characters/dashers/tekton_hold.glb"
dest_files=["res://.godot/imported/tekton_hold.glb-200d797f4b202164003717fecbdfba97.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bkhla0bd8gh6k"
path="res://.godot/imported/tekton_put.glb-10897df056830be9e27ad171553e71b3.scn"
[deps]
source_file="res://assets/characters/dashers/tekton_put.glb"
dest_files=["res://.godot/imported/tekton_put.glb-10897df056830be9e27ad171553e71b3.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://ci2lcm0tf02s0"
path="res://.godot/imported/tekton_take.glb-e5326a2ca2848c1e748b4be0ea916233.scn"
[deps]
source_file="res://assets/characters/dashers/tekton_take.glb"
dest_files=["res://.godot/imported/tekton_take.glb-e5326a2ca2848c1e748b4be0ea916233.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
@@ -1,9 +0,0 @@
[gd_resource type="StandardMaterial3D" format=3 uid="uid://v1qdug4x2ifm"]
[ext_resource type="Texture2D" uid="uid://d0egs6j3sg0me" path="res://assets/characters/skins/clothing/oldpop_cloth_white_pant.png" id="1_fw7uu"]
[resource]
transparency = 2
alpha_scissor_threshold = 0.5
alpha_antialiasing_mode = 0
albedo_texture = ExtResource("1_fw7uu")
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://cuporokvsp4ml"
path="res://.godot/imported/tekton_fishing_animation.glb-4469ef86e01e801d40365a39e33a43d9.scn"
[deps]
source_file="res://assets/characters/tektons/tekton_fishing_animation.glb"
dest_files=["res://.godot/imported/tekton_fishing_animation.glb-4469ef86e01e801d40365a39e33a43d9.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

@@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://csjf5vwqc7kjc"
path.s3tc="res://.godot/imported/tekton_fishing_animation_Ted_tex.png-843bfeb42afe9f0cc8bbef67a1660342.s3tc.ctex"
path.etc2="res://.godot/imported/tekton_fishing_animation_Ted_tex.png-843bfeb42afe9f0cc8bbef67a1660342.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "ad130a61dfc420142fb25fb3f8aa3c6f"
}
[deps]
source_file="res://assets/characters/tektons/tekton_fishing_animation_Ted_tex.png"
dest_files=["res://.godot/imported/tekton_fishing_animation_Ted_tex.png-843bfeb42afe9f0cc8bbef67a1660342.s3tc.ctex", "res://.godot/imported/tekton_fishing_animation_Ted_tex.png-843bfeb42afe9f0cc8bbef67a1660342.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
@@ -1,7 +1,3 @@
[gd_resource type="StandardMaterial3D" format=3 uid="uid://deufkavgeo0jg"] [gd_resource type="StandardMaterial3D" format=3 uid="uid://deufkavgeo0jg"]
[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_ywepx"]
load_path = "res://.godot/imported/4e3103ab214e443c83147afcd786aa6e_RGB_M_Buche_albedo.jpeg-69394e445366683380463d4afc846305.s3tc.ctex"
[resource] [resource]
albedo_texture = SubResource("CompressedTexture2D_ywepx")
@@ -9,4 +9,4 @@ Ke 0.000000 0.000000 0.000000
Ni 1.500000 Ni 1.500000
d 1.000000 d 1.000000
illum 2 illum 2
map_Kd C:/Users/beng/Downloads/3d_Ripper_Pro_v103/Downloads/SarahMyriamMadi/01- Handpainted.Log/4e3103ab214e443c83147afcd786aa6e_RGB_M_Buche_albedo.jpeg
Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

@@ -0,0 +1,42 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cyb7umn1uk7j8"
path.s3tc="res://.godot/imported/Block_box_tex.png-3a9361e801fe4eacf19d5d43c9246e12.s3tc.ctex"
path.etc2="res://.godot/imported/Block_box_tex.png-3a9361e801fe4eacf19d5d43c9246e12.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://assets/models/props/Block_box_tex.png"
dest_files=["res://.godot/imported/Block_box_tex.png-3a9361e801fe4eacf19d5d43c9246e12.s3tc.ctex", "res://.godot/imported/Block_box_tex.png-3a9361e801fe4eacf19d5d43c9246e12.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.
+543
View File
@@ -0,0 +1,543 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v4.3.47",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
4
]
}
],
"nodes":[
{
"name":"Bone.002",
"rotation":[
0,
1.1920928955078125e-07,
0,
1
],
"translation":[
0,
0.7185347676277161,
0
]
},
{
"children":[
0
],
"name":"Bone.001",
"translation":[
0,
0.7113578915596008,
0
]
},
{
"children":[
1
],
"name":"Bone"
},
{
"mesh":0,
"name":"wall_body",
"skin":0
},
{
"children":[
3,
2
],
"name":"Wall"
}
],
"animations":[
{
"channels":[
{
"sampler":0,
"target":{
"node":2,
"path":"translation"
}
},
{
"sampler":1,
"target":{
"node":2,
"path":"rotation"
}
},
{
"sampler":2,
"target":{
"node":2,
"path":"scale"
}
},
{
"sampler":3,
"target":{
"node":1,
"path":"translation"
}
},
{
"sampler":4,
"target":{
"node":1,
"path":"rotation"
}
},
{
"sampler":5,
"target":{
"node":1,
"path":"scale"
}
},
{
"sampler":6,
"target":{
"node":0,
"path":"translation"
}
},
{
"sampler":7,
"target":{
"node":0,
"path":"rotation"
}
},
{
"sampler":8,
"target":{
"node":0,
"path":"scale"
}
},
{
"sampler":9,
"target":{
"node":4,
"path":"translation"
}
},
{
"sampler":10,
"target":{
"node":4,
"path":"rotation"
}
},
{
"sampler":11,
"target":{
"node":4,
"path":"scale"
}
}
],
"name":"Wall.001|Wall Animation|Anima_Layer",
"samplers":[
{
"input":7,
"interpolation":"LINEAR",
"output":8
},
{
"input":9,
"interpolation":"STEP",
"output":10
},
{
"input":7,
"interpolation":"LINEAR",
"output":11
},
{
"input":9,
"interpolation":"LINEAR",
"output":12
},
{
"input":9,
"interpolation":"STEP",
"output":13
},
{
"input":7,
"interpolation":"LINEAR",
"output":14
},
{
"input":9,
"interpolation":"LINEAR",
"output":15
},
{
"input":9,
"interpolation":"STEP",
"output":16
},
{
"input":7,
"interpolation":"LINEAR",
"output":17
},
{
"input":9,
"interpolation":"STEP",
"output":18
},
{
"input":9,
"interpolation":"STEP",
"output":19
},
{
"input":9,
"interpolation":"STEP",
"output":20
}
]
}
],
"materials":[
{
"doubleSided":true,
"name":"Material",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.5
}
}
],
"meshes":[
{
"name":"Cube",
"primitives":[
{
"attributes":{
"POSITION":0,
"NORMAL":1,
"TEXCOORD_0":2,
"JOINTS_0":3,
"WEIGHTS_0":4
},
"indices":5,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
}
],
"images":[
{
"mimeType":"image/png",
"name":"Block_box_tex",
"uri":"Block_box_tex.png"
}
],
"skins":[
{
"inverseBindMatrices":6,
"joints":[
2,
1,
0
],
"name":"Wall"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":553,
"max":[
1,
2.0071864128112793,
1
],
"min":[
-1,
0.007186145056039095,
-1.0000003576278687
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":553,
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":553,
"type":"VEC2"
},
{
"bufferView":3,
"componentType":5121,
"count":553,
"type":"VEC4"
},
{
"bufferView":4,
"componentType":5126,
"count":553,
"type":"VEC4"
},
{
"bufferView":5,
"componentType":5123,
"count":2952,
"type":"SCALAR"
},
{
"bufferView":6,
"componentType":5126,
"count":3,
"type":"MAT4"
},
{
"bufferView":7,
"componentType":5126,
"count":15,
"max":[
0.5
],
"min":[
0.03333333333333333
],
"type":"SCALAR"
},
{
"bufferView":8,
"componentType":5126,
"count":15,
"type":"VEC3"
},
{
"bufferView":9,
"componentType":5126,
"count":2,
"max":[
0.5
],
"min":[
0.03333333333333333
],
"type":"SCALAR"
},
{
"bufferView":10,
"componentType":5126,
"count":2,
"type":"VEC4"
},
{
"bufferView":11,
"componentType":5126,
"count":15,
"type":"VEC3"
},
{
"bufferView":12,
"componentType":5126,
"count":2,
"type":"VEC3"
},
{
"bufferView":13,
"componentType":5126,
"count":2,
"type":"VEC4"
},
{
"bufferView":14,
"componentType":5126,
"count":15,
"type":"VEC3"
},
{
"bufferView":15,
"componentType":5126,
"count":2,
"type":"VEC3"
},
{
"bufferView":16,
"componentType":5126,
"count":2,
"type":"VEC4"
},
{
"bufferView":17,
"componentType":5126,
"count":15,
"type":"VEC3"
},
{
"bufferView":18,
"componentType":5126,
"count":2,
"type":"VEC3"
},
{
"bufferView":19,
"componentType":5126,
"count":2,
"type":"VEC4"
},
{
"bufferView":20,
"componentType":5126,
"count":2,
"type":"VEC3"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":6636,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":6636,
"byteOffset":6636,
"target":34962
},
{
"buffer":0,
"byteLength":4424,
"byteOffset":13272,
"target":34962
},
{
"buffer":0,
"byteLength":2212,
"byteOffset":17696,
"target":34962
},
{
"buffer":0,
"byteLength":8848,
"byteOffset":19908,
"target":34962
},
{
"buffer":0,
"byteLength":5904,
"byteOffset":28756,
"target":34963
},
{
"buffer":0,
"byteLength":192,
"byteOffset":34660
},
{
"buffer":0,
"byteLength":60,
"byteOffset":34852
},
{
"buffer":0,
"byteLength":180,
"byteOffset":34912
},
{
"buffer":0,
"byteLength":8,
"byteOffset":35092
},
{
"buffer":0,
"byteLength":32,
"byteOffset":35100
},
{
"buffer":0,
"byteLength":180,
"byteOffset":35132
},
{
"buffer":0,
"byteLength":24,
"byteOffset":35312
},
{
"buffer":0,
"byteLength":32,
"byteOffset":35336
},
{
"buffer":0,
"byteLength":180,
"byteOffset":35368
},
{
"buffer":0,
"byteLength":24,
"byteOffset":35548
},
{
"buffer":0,
"byteLength":32,
"byteOffset":35572
},
{
"buffer":0,
"byteLength":180,
"byteOffset":35604
},
{
"buffer":0,
"byteLength":24,
"byteOffset":35784
},
{
"buffer":0,
"byteLength":32,
"byteOffset":35808
},
{
"buffer":0,
"byteLength":24,
"byteOffset":35840
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":35864,
"uri":"Box_block_2.bin"
}
]
}
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://b6xusldnaa288"
path="res://.godot/imported/Box_block_2.gltf-6b278c07bb52956b8b62d9b428e8f6a6.scn"
[deps]
source_file="res://assets/models/props/Box_block_2.gltf"
dest_files=["res://.godot/imported/Box_block_2.gltf-6b278c07bb52956b8b62d9b428e8f6a6.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.
@@ -0,0 +1,44 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bh5epppot37nh"
path="res://.godot/imported/wall_animation.fbx-22f993a05720796858e5daa12a9ef4c5.scn"
[deps]
source_file="res://assets/models/props/wall_animation.fbx"
dest_files=["res://.godot/imported/wall_animation.fbx-22f993a05720796858e5daa12a9ef4c5.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=true
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
fbx/importer=0
fbx/allow_geometry_helper_nodes=false
fbx/embedded_image_handling=1
fbx/naming_version=2
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://dahuvrhr1u74y"
path="res://.godot/imported/wheat_block_0.glb-35fd252da9f0a42b79ed5759815c731c.scn"
[deps]
source_file="res://assets/models/props/wheat_block_0.glb"
dest_files=["res://.godot/imported/wheat_block_0.glb-35fd252da9f0a42b79ed5759815c731c.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdfu3bvdiisg2"
path.s3tc="res://.godot/imported/wheat_block_0_wheat_tex.png-c6a087ae50523310eace371eb3353908.s3tc.ctex"
path.etc2="res://.godot/imported/wheat_block_0_wheat_tex.png-c6a087ae50523310eace371eb3353908.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "448396071c54a15f593f0eb2628889c1"
}
[deps]
source_file="res://assets/models/props/wheat_block_0_wheat_tex.png"
dest_files=["res://.godot/imported/wheat_block_0_wheat_tex.png-c6a087ae50523310eace371eb3353908.s3tc.ctex", "res://.godot/imported/wheat_block_0_wheat_tex.png-c6a087ae50523310eace371eb3353908.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
Binary file not shown.
@@ -0,0 +1,42 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://ddjg2x57kr6"
path="res://.godot/imported/wheat_block_1.glb-5d996ad9d4004837507f80c51b730927.scn"
[deps]
source_file="res://assets/models/props/wheat_block_1.glb"
dest_files=["res://.godot/imported/wheat_block_1.glb-5d996ad9d4004837507f80c51b730927.scn"]
[params]
nodes/root_type=""
nodes/root_name=""
nodes/root_script=null
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
nodes/use_name_suffixes=true
nodes/use_node_type_suffixes=true
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
materials/extract=0
materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
gltf/embedded_image_handling=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,45 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://blluf0gwi8nbi"
path.s3tc="res://.godot/imported/wheat_block_1_wheat_tex.png-0784d9deebdc1ba07043c2e1e3363717.s3tc.ctex"
path.etc2="res://.godot/imported/wheat_block_1_wheat_tex.png-0784d9deebdc1ba07043c2e1e3363717.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
"md5": "448396071c54a15f593f0eb2628889c1"
}
[deps]
source_file="res://assets/models/props/wheat_block_1_wheat_tex.png"
dest_files=["res://.godot/imported/wheat_block_1_wheat_tex.png-0784d9deebdc1ba07043c2e1e3363717.s3tc.ctex", "res://.godot/imported/wheat_block_1_wheat_tex.png-0784d9deebdc1ba07043c2e1e3363717.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
+21
View File
@@ -0,0 +1,21 @@
codegen
=======
> A util tool to generate a client from the Swagger spec of Nakama's server API.
## Usage
```shell
go run main.go "$GOPATH/src/github.com/heroiclabs/nakama/apigrpc/apigrpc.swagger.json" > ../addons/com.heroiclabs.nakama/api/NakamaAPI.gd
```
### Rationale
We want to maintain a simple lean low level client within our GDScript client which has minimal dependencies so we built our own. This gives us complete control over the dependencies required and structure of the code generated.
The generated code is designed to be supported Godot Engine `3.1+`.
### Limitations
The code generator has __only__ been checked against the Swagger specification generated for Nakama server. YMMV.
+639
View File
@@ -0,0 +1,639 @@
// Copyright 2018 The Nakama Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"strings"
"text/template"
)
var utilities = map[string]string {
"ApiAccount":
`
var _wallet_dict = null
var wallet_dict : Dictionary:
get:
if _wallet_dict == null:
if _wallet == null:
return {}
var json = JSON.new()
if json.parse(_wallet) != OK:
return {}
_wallet_dict = json.get_data()
return _wallet_dict as Dictionary
`,
}
const codeTemplate string = `### Code generated by codegen/main.go. DO NOT EDIT. ###
extends RefCounted
class_name NakamaAPI
{{- range $defname, $definition := .Definitions }}
{{- $classname := $defname | title }}
{{- if isRefToEnum $classname }}
# {{ enumSummary $definition | stripNewlines }}
{{- range $idx, $val := ($definition | enumDescriptions) }}
# {{ $val }}
{{- end -}}
# {{ $definition | enumDescriptions }}
enum {{ $classname | title }} { {{- range $idx, $enum := $definition.Enum }}{{ $enum }} = {{ $idx }},{{- end -}} }
{{- else }}
# {{ $definition.Description | stripNewlines }}
class {{ $classname }} extends NakamaAsyncResult:
const _SCHEMA = {
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- $gdType := godotType $property.Type $property.Ref $property.Items.Type $property.Items.Ref (isRefToEnum (cleanRef $property.Ref)) }}
"{{ $fieldname }}": {"name": "{{ $_field }}", "type": {{ $gdType | godotSchemaType }}, "required": false
{{- if eq $property.Type "array" -}}
, "content": {{ (godotType $property.Items.Type $property.Items.Ref "" "" false) | godotSchemaType }}
{{- else if eq $property.Type "object" -}}
, "content": {{ (godotType $property.AdditionalProperties.Type "" "" "" false) | godotSchemaType }}
{{- end -}}
},
{{- end }}
}
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- $gdType := godotType $property.Type $property.Ref $property.Items.Type $property.Items.Ref (isRefToEnum (cleanRef $property.Ref)) }}
{{- $gdDef := $gdType | godotDef }}
# {{ $property.Description }}
var {{ $_field }}
var {{ $fieldname }} : {{ $gdType }}:
get:
{{- if $property.Ref }}
{{- if isRefToEnum (cleanRef $property.Ref) }}{{/* Enums */}}
return {{ cleanRef $property.Ref }}.values()[0] if not {{ cleanRef $property.Ref }}.values().has({{ $_field }}) else {{ $_field }}
{{- else }}{{/* Object reference */}}
return _{{ $fieldname }} as {{ $gdType }}
{{- end }}
{{- else if eq $property.Type "object"}}{{/* Dictionaries */}}
return Dictionary() if not {{ $_field }} is Dictionary else {{ $_field }}.duplicate()
{{- else }}{{/* Simple type */}}
return {{ $gdDef }} if not {{ $_field }} is {{ $gdType }} else {{ $gdType }}({{ $_field }})
{{- end }}
{{- end }}
{{- godotClassUtils $classname }}
func _init(p_exception = null):
super(p_exception)
static func create(p_ns : GDScript, p_dict : Dictionary) -> {{ $classname }}:
return _safe_ret(NakamaSerializer.deserialize(p_ns, "{{ $classname }}", p_dict), {{ $classname }}) as {{ $classname }}
func serialize() -> Dictionary:
return NakamaSerializer.serialize(self)
func _to_string() -> String:
if is_exception():
return get_exception()._to_string()
var output : String = ""
{{- range $propname, $property := $definition.Properties }}
{{- $fieldname := $propname | pascalToSnake }}
{{- $_field := printf "_%s" $fieldname }}
{{- if eq $property.Type "array" }}
output += "{{ $fieldname }}: %s, " % [{{ $_field }}]
{{- else if eq $property.Type "object" }}
var map_string : String = ""
if typeof({{ $_field }}) == TYPE_DICTIONARY:
for k in {{ $_field }}:
map_string += "{%s=%s}, " % [k, {{ $_field }}[k]]
output += "{{ $fieldname }}: [%s], " % map_string
{{- else }}
output += "{{ $fieldname }}: %s, " % {{ $_field }}
{{- end }}
{{- end }}
return output
{{- end }}
{{- end }}
# The low level client for the Nakama API.
class ApiClient extends RefCounted:
var _base_uri : String
var _http_adapter
var _namespace : GDScript
var _server_key : String
var auto_refresh := true
var auto_refresh_time := 300
var auto_retry : bool:
set(p_value):
_http_adapter.auto_retry = p_value
get:
return _http_adapter.auto_retry
var auto_retry_count : int:
set(p_value):
_http_adapter.auto_retry_count = p_value
get:
return _http_adapter.auto_retry_count
var auto_retry_backoff_base : int:
set(p_value):
_http_adapter.auto_retry_backoff_base = p_value
get:
return _http_adapter.auto_retry_backoff_base
var last_cancel_token:
get:
return _http_adapter.get_last_token()
func _init(p_base_uri : String, p_http_adapter, p_namespace : GDScript, p_server_key : String, p_timeout : int = 10):
_base_uri = p_base_uri
_http_adapter = p_http_adapter
_http_adapter.timeout = p_timeout
_namespace = p_namespace
_server_key = p_server_key
func _refresh_session(p_session : NakamaSession):
if auto_refresh and p_session.is_valid() and p_session.refresh_token and not p_session.is_refresh_expired() and p_session.would_expire_in(auto_refresh_time):
var request = ApiSessionRefreshRequest.new()
request._token = p_session.refresh_token
return await session_refresh_async(_server_key, "", request)
return null
func cancel_request(p_token):
if p_token:
_http_adapter.cancel_request(p_token)
{{- range $url, $path := .Paths }}
{{- range $method, $operation := $path}}
# {{ $operation.Summary | stripNewlines }}
{{- if $operation.Responses.Ok.Schema.Ref }}
func {{ $operation.OperationId | apiFuncName }}_async(
{{- else }}
func {{ $operation.OperationId | apiFuncName }}_async(
{{- end}}
{{- if $operation.Security }}
{{- with (index $operation.Security 0) }}
{{- range $key, $value := . }}
{{- if eq $key "BasicAuth" }}
p_basic_auth_username : String
, p_basic_auth_password : String
{{- else if eq $key "HttpKeyAuth" }}
p_bearer_token : String
{{- end }}
{{- end }}
{{- end }}
{{- else }}
p_session : NakamaSession
{{- end }}
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if not $parameter.Required }}{{/* Godot does not support typed optional parameters yet. */}}
, {{ $argument }} = null # : {{ $parameter.Type }}
{{- else if eq $parameter.In "body" }}
{{- if eq $parameter.Schema.Type "string" }}
, {{ $argument }} : String
{{- else }}
, {{ $argument }} : {{ $parameter.Schema.Ref | cleanRef }}
{{- end }}
{{- else }}
, {{ $argument }} : {{ godotType $parameter.Type $parameter.Schema.Ref $parameter.Items.Type "" (isRefToEnum (cleanRef $parameter.Schema.Ref)) }}
{{- end }}
{{- end }}
)
{{- if $operation.Responses.Ok.Schema.Ref }} -> {{ $operation.Responses.Ok.Schema.Ref | cleanRef }}
{{- else }} -> NakamaAsyncResult
{{- end }}:
{{- $classname := "NakamaAsyncResult" }}
{{- if $operation.Responses.Ok.Schema.Ref }}
{{- $classname = $operation.Responses.Ok.Schema.Ref | cleanRef }}
{{- end }}
{{- if not $operation.Security }}
var try_refresh = await _refresh_session(p_session)
if try_refresh != null:
if try_refresh.is_exception():
return {{ $classname }}.new(try_refresh.get_exception())
await p_session.refresh(try_refresh)
{{- end }}
var urlpath : String = "{{- $url }}"
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if eq $parameter.In "path" }}
urlpath = urlpath.replace("{{- print "{" $parameter.Name "}"}}", NakamaSerializer.escape_http({{ $argument }}))
{{- end }}
{{- end }}
var query_params = ""
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- $snakecase := $parameter.Name | pascalToSnake }}
{{- if eq $parameter.In "query"}}
{{- if $parameter.Required }}
if true: # Hack for static checks
{{- else }}
if {{ $argument }} != null:
{{- end }}
{{- if eq $parameter.Type "integer" }}
query_params += "{{- $snakecase }}=%d&" % {{ $argument }}
{{- else if eq $parameter.Type "string" }}
query_params += "{{- $snakecase }}=%s&" % NakamaSerializer.escape_http({{ $argument }})
{{- else if eq $parameter.Type "boolean" }}
query_params += "{{- $snakecase }}=%s&" % str(bool({{ $argument }})).to_lower()
{{- else if eq $parameter.Type "array" }}
for elem in {{ $argument }}:
query_params += "{{- $snakecase }}=%s&" % elem
{{- else }}
{{ $parameter }} // ERROR
{{- end }}
{{- end }}
{{- end }}
var uri = "%s%s%s" % [_base_uri, urlpath, "?" + query_params if query_params else ""]
var method = "{{- $method | uppercase }}"
var headers = {}
{{- if $operation.Security }}
{{- with (index $operation.Security 0) }}
{{- range $key, $value := . }}
{{- if eq $key "BasicAuth" }}
var credentials = Marshalls.utf8_to_base64(p_basic_auth_username + ":" + p_basic_auth_password)
var header = "Basic %s" % credentials
headers["Authorization"] = header
{{- else if eq $key "HttpKeyAuth" }}
if (p_bearer_token):
var header = "Bearer %s" % p_bearer_token
headers["Authorization"] = header
{{- end }}
{{- end }}
{{- end }}
{{- else }}
var header = "Bearer %s" % p_session.token
headers["Authorization"] = header
{{- end }}
var content : PackedByteArray
{{- range $parameter := $operation.Parameters }}
{{- $argument := $parameter.Name | prependParameter }}
{{- if eq $parameter.In "body" }}
{{- if eq $parameter.Schema.Type "string" }}
content = JSON.stringify({{ $argument }}).to_utf8_buffer()
{{- else }}
content = JSON.stringify({{ $argument }}.serialize()).to_utf8_buffer()
{{- end }}
{{- end }}
{{- end }}
var result = await _http_adapter.send_async(method, uri, headers, content)
if result is NakamaException:
return {{ $classname }}.new(result)
{{- if $operation.Responses.Ok.Schema.Ref }}
var out : {{ $classname }} = NakamaSerializer.deserialize(_namespace, "{{ $classname }}", result)
return out
{{- else }}
return NakamaAsyncResult.new()
{{- end}}
{{- end }}
{{- end }}
`
func convertRefToClassName(input string) (className string) {
cleanRef := strings.TrimPrefix(input, "#/definitions/")
className = strings.Title(cleanRef)
return
}
func stripNewlines(input string) (output string) {
output = strings.Replace(input, "\n", " ", -1)
return
}
func prependParameter(input string) (output string) {
output = "p_" + pascalToSnake(input)
return
}
func pascalToSnake(input string) (output string) {
output = ""
prev_low := false
for _, v := range input {
is_cap := v >= 'A' && v <= 'Z'
is_low := v >= 'a' && v <= 'z'
if is_cap && prev_low {
output = output + "_"
}
output += strings.ToLower(string(v))
prev_low = is_low
}
return
}
func apiFuncName(input string) (output string) {
output = pascalToSnake(input[7:])
return
}
func godotType(p_type string, p_ref string, p_item_type string, p_extra string, p_is_enum bool) (out string) {
is_array := false
is_dict := false
switch p_type {
case "integer":
out = "int"
case "string":
out = "String"
case "boolean":
out = "bool"
case "array":
is_array = true
case "object":
is_dict = true
default:
if p_is_enum {
out = "int"
} else {
out = convertRefToClassName(p_ref)
}
}
if is_array {
switch p_item_type {
case "integer":
out = "PackedIntArray"
return
case "string":
out = "PackedStringArray"
return
case "boolean":
out = "PackedIntArray"
return
default:
out = "Array"
}
}
if is_dict {
out = "Dictionary"
}
return
}
func godotDef(p_type string) (out string) {
switch(p_type) {
case "bool": out = "false"
case "int": out = "0"
case "String": out = "\"\""
case "PackedIntArray": out = "PackedIntArray()"
case "PackedStringArray": out = "PackedStringArray()"
case "Array": out = "Array()"
case "Dictionary": out = "Dictionary()"
}
return
}
func godotLooseType(p_type string) (out string) {
switch(p_type) {
case "PackedStringArray", "PackedIntArray":
out = "Array"
default:
out = p_type
}
return
}
func godotSchemaType(p_type string) (out string) {
out = "TYPE_"
switch(p_type) {
case "bool": out += "BOOL"
case "int": out += "INT"
case "String": out += "STRING"
case "PackedIntArray": out += "ARRAY"
case "PackedStringArray": out += "ARRAY"
case "Array": out += "ARRAY"
case "Dictionary": out += "DICTIONARY"
default: out = "\"" + p_type + "\""
}
return
}
func pascalToCamel(input string) (camelCase string) {
if input == "" {
return ""
}
camelCase = strings.ToLower(string(input[0]))
camelCase += string(input[1:])
return camelCase
}
func camelToPascal(camelCase string) (pascalCase string) {
if len(camelCase) <= 0 {
return ""
}
pascalCase = strings.ToUpper(string(camelCase[0])) + camelCase[1:]
return
}
func enumSummary(def Definition) string {
// quirk of swagger generation: if enum doesn't have a title
// then the title can be found as the first entry in the split description.
if def.Title != "" {
return def.Title
}
split := strings.Split(def.Description, "\n")
if len(split) <= 0 {
panic("No newlines in enum description found.")
}
return split[0]
}
func enumDescriptions(def Definition) (output []string) {
split := strings.Split(def.Description, "\n")
if len(split) <= 0 {
panic("No newlines in enum description found.")
}
if def.Title != "" {
return split
}
// quirk of swagger generation: if enum doesn't have a title
// then the title can be found as the first entry in the split description.
// so ignore for individual enum descriptions.
return split[2:]
}
type Definition struct {
Properties map[string]struct {
Type string
Ref string `json:"$ref"` // used with object
Items struct { // used with type "array"
Type string
Ref string `json:"$ref"`
}
AdditionalProperties struct {
Type string // used with type "map"
}
Format string // used with type "boolean"
Description string
}
Enum []string
Description string
// used only by enums
Title string
}
func godotClassUtils(p_name string) string {
if val, ok := utilities[p_name]; ok {
return val
}
return ""
}
func main() {
// Argument flags
var output = flag.String("output", "", "The output for generated code.")
flag.Parse()
inputs := flag.Args()
if len(inputs) < 1 {
fmt.Printf("No input file found: %s\n\n", inputs)
fmt.Println("openapi-gen [flags] inputs...")
flag.PrintDefaults()
return
}
input := inputs[0]
content, err := ioutil.ReadFile(input)
if err != nil {
fmt.Printf("Unable to read file: %s\n", err)
return
}
var schema struct {
Paths map[string]map[string]struct {
Summary string
OperationId string
Responses struct {
Ok struct {
Schema struct {
Ref string `json:"$ref"`
}
} `json:"200"`
}
Parameters []struct {
Name string
In string
Required bool
Type string // used with primitives
Items struct { // used with type "array"
Type string
}
Schema struct { // used with http body
Type string
Ref string `json:"$ref"`
}
Format string // used with type "boolean"
}
Security []map[string][]struct {
}
}
Definitions map[string]Definition
}
if err := json.Unmarshal(content, &schema); err != nil {
fmt.Printf("Unable to decode input %s : %s\n", input, err)
return
}
fmap := template.FuncMap{
"cleanRef": convertRefToClassName,
"stripNewlines": stripNewlines,
"title": strings.Title,
"uppercase": strings.ToUpper,
"prependParameter": prependParameter,
"pascalToSnake": pascalToSnake,
"apiFuncName": apiFuncName,
"godotType": godotType,
"godotLooseType": godotLooseType,
"godotSchemaType": godotSchemaType,
"godotDef": godotDef,
"isRefToEnum": func(ref string) bool {
if len(ref) == 0 {
return false
}
// swagger schema definition keys have inconsistent casing
var camelOk bool
var pascalOk bool
var enums []string
asCamel := pascalToCamel(ref)
if _, camelOk = schema.Definitions[asCamel]; camelOk {
enums = schema.Definitions[asCamel].Enum
}
asPascal := camelToPascal(ref)
if _, pascalOk = schema.Definitions[asPascal]; pascalOk {
enums = schema.Definitions[asPascal].Enum
}
if !pascalOk && !camelOk {
fmt.Printf("no definition found: %v", ref)
return false
}
return len(enums) > 0
},
"enumDescriptions": enumDescriptions,
"enumSummary": enumSummary,
"godotClassUtils": godotClassUtils,
}
tmpl, err := template.New(input).Funcs(fmap).Parse(codeTemplate)
if err != nil {
fmt.Printf("Template parse error: %s\n", err)
return
}
if len(*output) < 1 {
tmpl.Execute(os.Stdout, schema)
return
}
f, err := os.Create(*output)
if err != nil {
fmt.Printf("Unable to create file: %s\n", err)
return
}
defer f.Close()
writer := bufio.NewWriter(f)
tmpl.Execute(writer, schema)
writer.Flush()
}
+33
View File
@@ -0,0 +1,33 @@
[8a2fb36a98](https://git.klud.top/danchie/tekton/commit/8a2fb36a9867abb06ffef0d1967e9b45529d5b90)
/assets/characters/*
/assets/models/*
/scenes/player.gd
`:249-:251``_load_dasher_animations()`
`:376-:416``_load_dasher_animation():`
`:908-:950``if rank <=4:`
`:958-:961` `pos_label.modul..`
/scenes/ui/lobby_room.gd
`:128-:131` # Update area visual immediately when game mode changes
/scripts/managers/auth_manager.gd
`:445-:448`
`:454-:458`
`:463-:471`
/scripts/managers/lobby_manager.gd
`:728-:729`
`:732-:742`
`:749-:759`
/scripts/managers/settings_manager.gd
`:320-:329`
/scripts/nakama_manager.gd
`:126-:135`
/scripts/tekton.gd
`:417-:426`
`:451-:456`
+6
View File
@@ -450,6 +450,12 @@ func _on_server_option_selected(index: int) -> void:
NakamaManager.set_server("tektondash.vps.webdock.cloud") NakamaManager.set_server("tektondash.vps.webdock.cloud")
LobbyManager.is_lan_mode = false LobbyManager.is_lan_mode = false
connection_status.text = "Mode: Online (Tekton Dash EU)" connection_status.text = "Mode: Online (Tekton Dash EU)"
elif index == 4:
# Tekton Dash Asia
if server_ip_input: server_ip_input.visible = false
NakamaManager.set_server("tekton-asia.klud.top")
LobbyManager.is_lan_mode = false
connection_status.text = "Mode: Online (Tekton Dash Asia)"
func _on_server_ip_submitted(new_text: String) -> void: func _on_server_ip_submitted(new_text: String) -> void:
if server_option and server_option.selected == 1: if server_option and server_option.selected == 1:
+4 -2
View File
@@ -926,8 +926,8 @@ text = "TEKTON DASH"
[node name="ServerOption" type="OptionButton" parent="MainMenuPanel/HiddenLogic" unique_id=645] [node name="ServerOption" type="OptionButton" parent="MainMenuPanel/HiddenLogic" unique_id=645]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 0 layout_mode = 0
selected = 3 selected = 4
item_count = 4 item_count = 5
popup/item_0/text = "Nakama - Localhost (Testing)" popup/item_0/text = "Nakama - Localhost (Testing)"
popup/item_0/id = 0 popup/item_0/id = 0
popup/item_1/text = "Nakama - Remote Server (Host IP)" popup/item_1/text = "Nakama - Remote Server (Host IP)"
@@ -936,6 +936,8 @@ popup/item_2/text = "LAN Direct (No Server)"
popup/item_2/id = 2 popup/item_2/id = 2
popup/item_3/text = "Tekton Dash EU" popup/item_3/text = "Tekton Dash EU"
popup/item_3/id = 3 popup/item_3/id = 3
popup/item_4/text = "Tekton Dash Asia"
popup/item_4/id = 4
[node name="ServerIPInput" type="LineEdit" parent="MainMenuPanel/HiddenLogic" unique_id=1616424715] [node name="ServerIPInput" type="LineEdit" parent="MainMenuPanel/HiddenLogic" unique_id=1616424715]
unique_name_in_owner = true unique_name_in_owner = true
+88 -67
View File
@@ -70,25 +70,23 @@ var is_carrying_tekton: bool = false:
emit_signal("tekton_carried_changed", value) emit_signal("tekton_carried_changed", value)
# Visual/Logic side effects if any # Visual/Logic side effects if any
var is_attack_mode: bool = false: var is_charged_strike: bool = false:
set(value): set(value):
if is_attack_mode == value: if is_charged_strike == value:
return # Prevent infinite recursion / redundant updates return
is_attack_mode = value is_charged_strike = value
if is_attack_mode: if is_charged_strike:
attack_mode_timer = MAX_ATTACK_MODE_TIME charged_strike_timer = MAX_CHARGED_STRIKE_TIME
_refresh_player_visuals() _refresh_player_visuals()
# Sync to others if we are the authority # Sync to others if we are the authority
if is_multiplayer_authority() and can_rpc(): if is_multiplayer_authority() and can_rpc():
rpc("sync_attack_mode", is_attack_mode) rpc("sync_charged_strike", is_charged_strike)
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func sync_attack_mode(state: bool): func sync_charged_strike(state: bool):
# We WANT to trigger the setter to apply visuals on clients is_charged_strike = state
# Using self.var triggers setter in GDScript
is_attack_mode = state
@export var is_bot: bool = false @export var is_bot: bool = false
@@ -248,6 +246,9 @@ func _ready():
# Character Pointer Visibility # Character Pointer Visibility
# Visible to all human players. Green for local player, Red for others. # Visible to all human players. Green for local player, Red for others.
var pointer = get_node_or_null("CharacterPointer") var pointer = get_node_or_null("CharacterPointer")
# === Dynamically load new Dasher animations ===
_load_dasher_animations()
if pointer: if pointer:
pointer.visible = true pointer.visible = true
@@ -372,6 +373,47 @@ func _init_floor_spawn_anchor():
if floor_spawn_top: if floor_spawn_top:
floor_spawn_top.reparent(floor_spawn_anchor, false) floor_spawn_top.reparent(floor_spawn_anchor, false)
func _load_dasher_animations():
"""Dynamically loads dasher animations from GLB files and adds them to the AnimationPlayer."""
if not anim_player: return
var anim_library = anim_player.get_animation_library("animation-pack")
if not anim_library:
anim_library = AnimationLibrary.new()
anim_player.add_animation_library("animation-pack", anim_library)
var dasher_files = [
{"path": "res://assets/characters/dashers/dasher_getting_hit.glb", "name": "dasher_getting_hit"},
{"path": "res://assets/characters/dashers/dasher_hit.glb", "name": "dasher_hit"},
{"path": "res://assets/characters/dashers/dasher_hold.glb", "name": "dasher_hold"},
{"path": "res://assets/characters/dashers/dasher_put.glb", "name": "dasher_put"},
{"path": "res://assets/characters/dashers/dasher_stun.glb", "name": "dasher_stun"},
{"path": "res://assets/characters/dashers/dasher_take.glb", "name": "dasher_take"}
]
for file_data in dasher_files:
var gltf_doc = GLTFDocument.new()
var gltf_state = GLTFState.new()
var error = gltf_doc.append_from_file(file_data.path, gltf_state)
if error == OK:
var anim_player_node = gltf_state.get_animation_player(0)
# Godot's GLTF importer creates an AnimationPlayer inside the scene
var scene = gltf_doc.generate_scene(gltf_state)
if scene:
var scene_anim_player = scene.find_child("AnimationPlayer", true, false)
if scene_anim_player:
var libs = scene_anim_player.get_animation_library_list()
for lib_name in libs:
var temp_lib = scene_anim_player.get_animation_library(lib_name)
for anim_name in temp_lib.get_animation_list():
var anim = temp_lib.get_animation(anim_name)
if not anim_library.has_animation(file_data.name):
anim_library.add_animation(file_data.name, anim)
scene.queue_free()
print("[Player] Dasher animations loaded into 'animation-pack'.")
@onready var floor_spawn_bot: AnimatedSprite3D = $floor_spawn_bot @onready var floor_spawn_bot: AnimatedSprite3D = $floor_spawn_bot
@onready var floor_spawn_top: AnimatedSprite3D = $floor_spawn_top @onready var floor_spawn_top: AnimatedSprite3D = $floor_spawn_top
@onready var vfx_scatter_knock: AnimatedSprite3D = $scatter_knock @onready var vfx_scatter_knock: AnimatedSprite3D = $scatter_knock
@@ -874,10 +916,10 @@ func _refresh_player_visuals():
color_to_apply = Color.CYAN # Stop n Go Freeze color_to_apply = Color.CYAN # Stop n Go Freeze
elif is_slowed: elif is_slowed:
color_to_apply = Color(0.6, 0.8, 1.0) # Slowed / Icy Blue color_to_apply = Color(0.6, 0.8, 1.0) # Slowed / Icy Blue
elif is_attack_mode: elif is_charged_strike:
color_to_apply = Color(1.0, 0.5, 0.5) # Attack Mode (Red Tint) color_to_apply = Color(1.0, 0.5, 0.5) # Charged Strike (Red Tint)
elif is_carrying_tekton or is_knock_mode: elif is_carrying_tekton:
color_to_apply = Color(1.0, 1.0, 0.0) # Carrying or Knocking (Yellow) color_to_apply = Color(1.0, 1.0, 0.0) # Carrying (Yellow)
alpha_to_apply = 0.5 # 50% opacity when carrying Tekton alpha_to_apply = 0.5 # 50% opacity when carrying Tekton
elif immunity_timer > 0: elif immunity_timer > 0:
color_to_apply = Color(0.5, 1.0, 0.5) # Immunity (Light Green) color_to_apply = Color(0.5, 1.0, 0.5) # Immunity (Light Green)
@@ -905,7 +947,7 @@ func update_rank_visuals(rank: int):
if not pos_label: if not pos_label:
return return
if rank <= 3: if rank <= 4:
pos_label.visible = true pos_label.visible = true
if race_manager: if race_manager:
pos_label.text = race_manager.get_ordinal_string(rank) pos_label.text = race_manager.get_ordinal_string(rank)
@@ -913,9 +955,10 @@ func update_rank_visuals(rank: int):
pos_label.text = str(rank) pos_label.text = str(rank)
match rank: match rank:
1: pos_label.modulate = Color(0.85, 0.0, 0.0) # Red 1: pos_label.modulate = Color(1.0, 0.84, 0.0) # Gold
2: pos_label.modulate = Color(0.0, 0.0, 1.0) # Blue 2: pos_label.modulate = Color(0.75, 0.75, 0.75) # Silver
3: pos_label.modulate = Color(1.0, 0.9, 0.0) # Yellow 3: pos_label.modulate = Color(0.8, 0.5, 0.2) # Bronze
4: pos_label.modulate = Color(0.5, 0.5, 0.5) # Grey
else: else:
pos_label.visible = false pos_label.visible = false
@@ -941,8 +984,8 @@ var slow_timer: float = 0.0
var tekton_carry_timer: float = 0.0 var tekton_carry_timer: float = 0.0
const MAX_TEKTON_CARRY_TIME: float = 3.0 const MAX_TEKTON_CARRY_TIME: float = 3.0
var attack_mode_timer: float = 0.0 var charged_strike_timer: float = 0.0
const MAX_ATTACK_MODE_TIME: float = 5.0 const MAX_CHARGED_STRIKE_TIME: float = 5.0
@rpc("any_peer", "call_local") @rpc("any_peer", "call_local")
func apply_stagger(duration: float = 1.5): func apply_stagger(duration: float = 1.5):
@@ -1220,7 +1263,7 @@ func attempt_target_action(target_index: int):
inventory_ui.deselect() inventory_ui.deselect()
func activate_powerup(effect_id: int): func activate_powerup(effect_id: int):
if is_carrying_tekton or is_knock_mode or is_attack_mode: if is_carrying_tekton or is_charged_strike:
NotificationManager.send_message(self, "Cannot use Power-Up right now!", NotificationManager.MessageType.WARNING) NotificationManager.send_message(self, "Cannot use Power-Up right now!", NotificationManager.MessageType.WARNING)
return return
@@ -1253,7 +1296,7 @@ func activate_powerup(effect_id: int):
func activate_held_powerup(): func activate_held_powerup():
"""Finds whichever powerup is currently held and activates it.""" """Finds whichever powerup is currently held and activates it."""
if is_carrying_tekton or is_knock_mode or is_attack_mode: if is_carrying_tekton or is_charged_strike:
NotificationManager.send_message(self, "Cannot use Power-Up right now!", NotificationManager.MessageType.WARNING) NotificationManager.send_message(self, "Cannot use Power-Up right now!", NotificationManager.MessageType.WARNING)
return return
@@ -1299,15 +1342,14 @@ func _process(delta):
if movement_manager: if movement_manager:
movement_manager._process(delta) movement_manager._process(delta)
# Attack/Knock Mode Expiration Timer # Charged Strike Expiration Timer
if is_multiplayer_authority() and (is_attack_mode or is_knock_mode): if is_multiplayer_authority() and is_charged_strike:
if attack_mode_timer > 0: if charged_strike_timer > 0:
attack_mode_timer -= delta charged_strike_timer -= delta
if attack_mode_timer <= 0: if charged_strike_timer <= 0:
attack_mode_timer = 0.0 charged_strike_timer = 0.0
is_attack_mode = false is_charged_strike = false
is_knock_mode = false NotificationManager.send_message(self, "Charged Strike Expired!", NotificationManager.MessageType.WARNING)
NotificationManager.send_message(self, "Knock Mode Expired!", NotificationManager.MessageType.WARNING)
if powerup_manager: if powerup_manager:
powerup_manager.reset_boost() powerup_manager.reset_boost()
@@ -2393,8 +2435,8 @@ func sync_snatch_tekton(carrier_path: NodePath, tekton_path: NodePath):
tekton_carry_timer = 0.0 tekton_carry_timer = 0.0
# Visual/Logic side effects # Visual/Logic side effects
if is_attack_mode: if is_charged_strike:
is_attack_mode = false is_charged_strike = false
SfxManager.play("pick_up_tekton_roaming") SfxManager.play("pick_up_tekton_roaming")
play_pickup_animation() play_pickup_animation()
@@ -2414,9 +2456,9 @@ func sync_grab_tekton(tekton_path: NodePath):
self.is_carrying_tekton = true self.is_carrying_tekton = true
tekton.set_carried(true, self ) tekton.set_carried(true, self )
# Disposed of AttackMode upon grab # Disposed of Charged Strike upon grab
if is_attack_mode: if is_charged_strike:
is_attack_mode = false is_charged_strike = false
SfxManager.play("pick_up_tekton_roaming") SfxManager.play("pick_up_tekton_roaming")
play_pickup_animation() play_pickup_animation()
@@ -2558,38 +2600,17 @@ func sync_drop_tekton():
print("[Player %s] Dropped Tekton at %s" % [name, current_position]) print("[Player %s] Dropped Tekton at %s" % [name, current_position])
# is_attack_mode is already declared at top of file (or inherited?)
# Keeping is_knock_mode here for now or moving it up would be better, but let's just fix the error first.
var is_knock_mode: bool = false:
set(value):
if is_knock_mode == value: return
is_knock_mode = value
if is_knock_mode:
attack_mode_timer = MAX_ATTACK_MODE_TIME
_refresh_player_visuals()
func enter_attack_mode():
func enter_charged_strike():
if not is_multiplayer_authority(): return if not is_multiplayer_authority(): return
if is_invisible: if is_invisible:
NotificationManager.send_message(self , "Cannot enter Attack Mode while in Ghost mode!", NotificationManager.MessageType.WARNING) NotificationManager.send_message(self , "Cannot use Charged Strike while in Ghost mode!", NotificationManager.MessageType.WARNING)
return return
is_attack_mode = true is_charged_strike = true
is_knock_mode = false # Mutually exclusive NotificationManager.send_message(self , "Charged Strike ACTIVATED (Red)", NotificationManager.MessageType.POWERUP)
NotificationManager.send_message(self , "Attack Mode ACTIVATED (Red)", NotificationManager.MessageType.POWERUP)
update_active_player_indicator()
func enter_knock_mode():
if not is_multiplayer_authority(): return
if is_invisible:
NotificationManager.send_message(self , "Cannot enter Knock Mode while in Ghost mode!", NotificationManager.MessageType.WARNING)
return
is_knock_mode = true
is_attack_mode = false # Mutually exclusive
NotificationManager.send_message(self , "Knock Mode ACTIVATED (Yellow)", NotificationManager.MessageType.POWERUP)
update_active_player_indicator() update_active_player_indicator()
func update_active_player_indicator(): func update_active_player_indicator():
@@ -2619,8 +2640,8 @@ func knock_tekton():
if not is_multiplayer_authority() or is_frozen or is_stop_frozen or is_invisible: if not is_multiplayer_authority() or is_frozen or is_stop_frozen or is_invisible:
return return
# Requirement: Full Powerup Bar (or we are already in knock mode) # Requirement: Full Powerup Bar (or we are already charged)
if not is_knock_mode and (not powerup_manager or not powerup_manager.can_use_special()): if not is_charged_strike and (not powerup_manager or not powerup_manager.can_use_special()):
NotificationManager.send_message(self , "Need Full Boost to Knock!", NotificationManager.MessageType.WARNING) NotificationManager.send_message(self , "Need Full Boost to Knock!", NotificationManager.MessageType.WARNING)
return return
@@ -2633,8 +2654,8 @@ func knock_tekton():
if is_multiplayer_authority(): if is_multiplayer_authority():
rpc("sync_knock_tekton", tekton.get_path()) rpc("sync_knock_tekton", tekton.get_path())
# Reset Knock Mode after successful hit # Reset Charged Strike Mode after successful hit
is_knock_mode = false is_charged_strike = false
NotificationManager.send_message(self , "Knock Successful!", NotificationManager.MessageType.POWERUP) NotificationManager.send_message(self , "Knock Successful!", NotificationManager.MessageType.POWERUP)
update_active_player_indicator() update_active_player_indicator()
else: else:
+1
View File
@@ -219,6 +219,7 @@ custom_minimum_size = Vector2(0, 20)
layout_mode = 2 layout_mode = 2
theme_override_styles/background = SubResource("StyleBoxFlat_wusgu") theme_override_styles/background = SubResource("StyleBoxFlat_wusgu")
theme_override_styles/fill = SubResource("StyleBoxFlat_2h0ma") theme_override_styles/fill = SubResource("StyleBoxFlat_2h0ma")
show_percentage = false
[node name="ProgressLabel" type="Label" parent="CenterContainer/MainVBox/ProgressBar" unique_id=1293848486] [node name="ProgressLabel" type="Label" parent="CenterContainer/MainVBox/ProgressBar" unique_id=1293848486]
unique_name_in_owner = true unique_name_in_owner = true
+4 -2
View File
@@ -351,8 +351,8 @@ grow_vertical = 0
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 layout_mode = 2
selected = 3 selected = 4
item_count = 4 item_count = 5
popup/item_0/text = "Nakama - Localhost (Testing)" popup/item_0/text = "Nakama - Localhost (Testing)"
popup/item_0/id = 0 popup/item_0/id = 0
popup/item_1/text = "Nakama - Remote Server (Host IP)" popup/item_1/text = "Nakama - Remote Server (Host IP)"
@@ -361,6 +361,8 @@ popup/item_2/text = "LAN Direct (No Server)"
popup/item_2/id = 2 popup/item_2/id = 2
popup/item_3/text = "Nakama - Tekton Dash EU" popup/item_3/text = "Nakama - Tekton Dash EU"
popup/item_3/id = 3 popup/item_3/id = 3
popup/item_4/text = "Nakama - Tekton Dash Asia"
popup/item_4/id = 4
[node name="ServerIPInput" type="LineEdit" parent="ServerSelectionSection" unique_id=1444265129] [node name="ServerIPInput" type="LineEdit" parent="ServerSelectionSection" unique_id=1444265129]
unique_name_in_owner = true unique_name_in_owner = true
+22 -18
View File
@@ -22,8 +22,8 @@ var auth_mode: AuthMode = AuthMode.GUEST
const SESSION_FILE := "user://auth_session.dat" const SESSION_FILE := "user://auth_session.dat"
const CREDENTIALS_FILE := "user://auth_credentials.dat" const CREDENTIALS_FILE := "user://auth_credentials.dat"
# Encryption key for session storage (replace with your own!) # Encryption key for session storage (device-specific)
const ENCRYPTION_KEY := "tekton_secret_key_change_me_123" var ENCRYPTION_KEY: String = OS.get_unique_id().sha256_text()
func _ready() -> void: func _ready() -> void:
# Try to restore session on startup # Try to restore session on startup
@@ -368,7 +368,7 @@ func login_with_steam() -> bool:
return true return true
func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession: func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession:
# Try proper Steam ticket auth first # Proper Steam ticket auth
var auth_ticket = steamworks.get_auth_session_ticket() var auth_ticket = steamworks.get_auth_session_ticket()
if not auth_ticket.is_empty(): if not auth_ticket.is_empty():
print("[AuthManager] Got Steam auth ticket, authenticating with Nakama...") print("[AuthManager] Got Steam auth ticket, authenticating with Nakama...")
@@ -376,22 +376,10 @@ func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession:
if not session.is_exception(): if not session.is_exception():
return session return session
print("[AuthManager] Steam ticket auth failed: %s" % session.get_exception().message) print("[AuthManager] Steam ticket auth failed: %s" % session.get_exception().message)
print("[AuthManager] Falling back to Steam ID custom auth (dev mode)...")
# Fallback: use Steam ID + username to create an email-style account (works without publisher key)
var steam_id = str(steamworks.get_steam_user_id())
var steam_name = steamworks.get_steam_user_name()
if steam_id == "0" or steam_id.is_empty():
return null return null
# Derive email and password from Steam credentials print("[AuthManager] Steam auth ticket is empty.")
var email = steam_name.to_lower().replace(" ", "_") + "@steam.local" return null
var password = steam_name # Default password = Steam username
var username = steam_name
print("[AuthManager] Using Steam email auth: %s (%s)" % [email, username])
var fallback_session: NakamaSession = await NakamaManager.client.authenticate_email_async(email, password, username, true)
return fallback_session
# ============================================================================= # =============================================================================
# Account Linking (Convert Guest to Full Account) # Account Linking (Convert Guest to Full Account)
@@ -454,17 +442,33 @@ func logout() -> void:
# ============================================================================= # =============================================================================
func _connect_socket() -> bool: func _connect_socket() -> bool:
if not NakamaManager.session:
push_error("[AuthManager] Socket connection failed: no Nakama session")
return false
if NakamaManager.socket and NakamaManager.socket.is_connected_to_host(): if NakamaManager.socket and NakamaManager.socket.is_connected_to_host():
if not multiplayer.has_multiplayer_peer() and NakamaManager.bridge: if not multiplayer.has_multiplayer_peer() and NakamaManager.bridge:
multiplayer.set_multiplayer_peer(NakamaManager.bridge.multiplayer_peer) multiplayer.set_multiplayer_peer(NakamaManager.bridge.multiplayer_peer)
NakamaManager.connected_to_nakama.emit() NakamaManager.connected_to_nakama.emit()
return true return true
if NakamaManager.socket:
NakamaManager.socket.close()
NakamaManager.socket = null
NakamaManager.socket = Nakama.create_socket_from(NakamaManager.client) NakamaManager.socket = Nakama.create_socket_from(NakamaManager.client)
var result = await NakamaManager.socket.connect_async(NakamaManager.session) var result = await NakamaManager.socket.connect_async(NakamaManager.session)
if result.is_exception(): if result.is_exception():
push_error("[AuthManager] Socket connection failed: " + result.get_exception().message) var exception = result.get_exception()
var error_message = "Socket connection failed"
if exception and not exception.message.is_empty():
error_message = exception.message
elif exception and exception.status_code >= 0:
error_message = "Socket connection failed with error code %s" % exception.status_code
push_error("[AuthManager] " + error_message)
NakamaManager.socket.close()
NakamaManager.socket = null
return false return false
# Initialize multiplayer bridge # Initialize multiplayer bridge
+77 -1
View File
@@ -31,6 +31,11 @@ signal doors_swap_time_changed(time: int)
signal doors_refresh_time_changed(time: int) signal doors_refresh_time_changed(time: int)
signal doors_required_goals_changed(goals: int) signal doors_required_goals_changed(goals: int)
# Gauntlet settings signals
signal gauntlet_round_duration_changed(duration: int)
signal gauntlet_cannon_interval_changed(interval: int)
signal gauntlet_volley_size_changed(size: int)
# Room data structure # Room data structure
var current_room: Dictionary = {} var current_room: Dictionary = {}
var players_in_room: Array = [] # [{id, name, is_ready}] var players_in_room: Array = [] # [{id, name, is_ready}]
@@ -74,13 +79,18 @@ var doors_swap_time: int = 15
var doors_refresh_time: int = 25 var doors_refresh_time: int = 25
var doors_required_goals: int = 8 var doors_required_goals: int = 8
# Gauntlet settings
var gauntlet_round_duration: int = 180
var gauntlet_cannon_interval: int = 5
var gauntlet_volley_size: int = 5
# Rematch tracking # Rematch tracking
var rematch_votes: Array = [] # [player_id, ...] var rematch_votes: Array = [] # [player_id, ...]
# Character and area selection # Character and area selection
var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"] var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"]
var available_areas: Array[String] = [] var available_areas: Array[String] = []
var available_game_modes: Array[String] = ["Freemode", "Stop n Go"] var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Candy Cannon Survival"]
var selected_area: String = "Freemode Arena" # Host-controlled var selected_area: String = "Freemode Arena" # Host-controlled
var game_mode: String = "Freemode" # Host-controlled var game_mode: String = "Freemode" # Host-controlled
var local_character_index: int = 0 # Local player's character index var local_character_index: int = 0 # Local player's character index
@@ -135,6 +145,8 @@ func _update_available_areas(mode: String) -> void:
available_areas = ["Freemode Arena", "Classic", "Colloseum"] available_areas = ["Freemode Arena", "Classic", "Colloseum"]
"Stop n Go": "Stop n Go":
available_areas = ["Stop N Go Arena"] available_areas = ["Stop N Go Arena"]
"Candy Cannon Survival":
available_areas = ["Gauntlet Arena"]
_: _:
available_areas = ["Classic"] available_areas = ["Classic"]
@@ -537,6 +549,37 @@ func sync_doors_required_goals(goals: int) -> void:
doors_required_goals = goals doors_required_goals = goals
emit_signal("doors_required_goals_changed", goals) emit_signal("doors_required_goals_changed", goals)
# =============================================================================
# Gauntlet Settings
# =============================================================================
func set_gauntlet_round_duration(duration: int) -> void:
gauntlet_round_duration = duration
if is_host: rpc("sync_gauntlet_round_duration", duration)
@rpc("authority", "call_local", "reliable")
func sync_gauntlet_round_duration(duration: int) -> void:
gauntlet_round_duration = duration
emit_signal("gauntlet_round_duration_changed", duration)
func set_gauntlet_cannon_interval(interval: int) -> void:
gauntlet_cannon_interval = interval
if is_host: rpc("sync_gauntlet_cannon_interval", interval)
@rpc("authority", "call_local", "reliable")
func sync_gauntlet_cannon_interval(interval: int) -> void:
gauntlet_cannon_interval = interval
emit_signal("gauntlet_cannon_interval_changed", interval)
func set_gauntlet_volley_size(size: int) -> void:
gauntlet_volley_size = size
if is_host: rpc("sync_gauntlet_volley_size", size)
@rpc("authority", "call_local", "reliable")
func sync_gauntlet_volley_size(size: int) -> void:
gauntlet_volley_size = size
emit_signal("gauntlet_volley_size_changed", size)
# ============================================================================= # =============================================================================
# Character Selection # Character Selection
# ============================================================================= # =============================================================================
@@ -682,14 +725,40 @@ func set_game_mode(mode: String) -> void:
rpc("sync_game_mode", mode) rpc("sync_game_mode", mode)
_update_available_areas(mode) _update_available_areas(mode)
# Only force switch the area if the selected area is NOT valid for this mode
if selected_area not in available_areas: if selected_area not in available_areas:
set_area(available_areas[0]) set_area(available_areas[0])
else:
# Important: even if the area is technically in the list, if they just clicked Free Mode
# we should default them to Free Mode Area if they were on Stop n Go Area before.
if mode == "Free Mode" and "Free Mode Area" in available_areas:
set_area("Free Mode Area")
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
set_area("Stop n Go Area")
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
set_area("Tekton Doors Area")
elif mode == "Gauntlet" and "Candy Pump Arena" in available_areas:
set_area("Candy Pump Arena")
@rpc("authority", "call_local", "reliable") @rpc("authority", "call_local", "reliable")
func sync_game_mode(mode: String) -> void: func sync_game_mode(mode: String) -> void:
"""Sync game mode selection from host to clients.""" """Sync game mode selection from host to clients."""
game_mode = mode game_mode = mode
_update_available_areas(mode) _update_available_areas(mode)
# Try to smart-match the client's local area to the mode as well so their UI matches
if mode == "Free Mode" and "Free Mode Area" in available_areas:
selected_area = "Free Mode Area"
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
selected_area = "Stop n Go Area"
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
selected_area = "Tekton Doors Area"
elif mode == "Gauntlet" and "Candy Pump Arena" in available_areas:
selected_area = "Candy Pump Arena"
elif selected_area not in available_areas:
selected_area = available_areas[0]
emit_signal("game_mode_changed", mode) emit_signal("game_mode_changed", mode)
func start_game(force: bool = false) -> void: func start_game(force: bool = false) -> void:
@@ -715,6 +784,10 @@ func start_game(force: bool = false) -> void:
rpc("sync_doors_swap_time", doors_swap_time) rpc("sync_doors_swap_time", doors_swap_time)
rpc("sync_doors_refresh_time", doors_refresh_time) rpc("sync_doors_refresh_time", doors_refresh_time)
rpc("sync_doors_required_goals", doors_required_goals) rpc("sync_doors_required_goals", doors_required_goals)
# Sync gauntlet settings
rpc("sync_gauntlet_round_duration", gauntlet_round_duration)
rpc("sync_gauntlet_cannon_interval", gauntlet_cannon_interval)
rpc("sync_gauntlet_volley_size", gauntlet_volley_size)
# Sync game mode # Sync game mode
rpc("sync_game_mode", game_mode) rpc("sync_game_mode", game_mode)
@@ -790,6 +863,9 @@ func request_room_info(requester_id: int, requester_name: String, requester_char
rpc_id(requester_id, "sync_doors_swap_time", doors_swap_time) rpc_id(requester_id, "sync_doors_swap_time", doors_swap_time)
rpc_id(requester_id, "sync_doors_refresh_time", doors_refresh_time) rpc_id(requester_id, "sync_doors_refresh_time", doors_refresh_time)
rpc_id(requester_id, "sync_doors_required_goals", doors_required_goals) rpc_id(requester_id, "sync_doors_required_goals", doors_required_goals)
rpc_id(requester_id, "sync_gauntlet_round_duration", gauntlet_round_duration)
rpc_id(requester_id, "sync_gauntlet_cannon_interval", gauntlet_cannon_interval)
rpc_id(requester_id, "sync_gauntlet_volley_size", gauntlet_volley_size)
rpc_id(requester_id, "sync_game_mode", game_mode) rpc_id(requester_id, "sync_game_mode", game_mode)
rpc_id(requester_id, "sync_area", selected_area) rpc_id(requester_id, "sync_area", selected_area)
+12 -1
View File
@@ -289,7 +289,9 @@ func get_action_display(action_key: String) -> String:
"grab": "ctrl_grab", "grab": "ctrl_grab",
"use_powerup": "ctrl_use_powerup", "use_powerup": "ctrl_use_powerup",
"tekton_grab": "ctrl_tekton_grab", "tekton_grab": "ctrl_tekton_grab",
"action_grab_tekton": "ctrl_tekton_grab",
"attack_mode": "ctrl_attack_mode", "attack_mode": "ctrl_attack_mode",
"action_knock_tekton": "ctrl_attack_mode",
} }
if ctrl_key_map.has(action_key): if ctrl_key_map.has(action_key):
return get_controller_binding_text(ctrl_key_map[action_key]) return get_controller_binding_text(ctrl_key_map[action_key])
@@ -315,7 +317,16 @@ func is_controller_button_used(button_index: int) -> String:
return "" return ""
func get_control_keycode(action_name: String) -> int: func get_control_keycode(action_name: String) -> int:
return settings.controls.get(action_name, -1) # Map friendly names to their internal settings.controls keys
var mapped_name = action_name
if action_name == "tekton_grab":
mapped_name = "action_grab_tekton"
elif action_name == "attack_mode":
mapped_name = "action_knock_tekton"
elif action_name == "grab":
mapped_name = "action_grab"
return settings.controls.get(mapped_name, -1)
func get_control_text(action_name: String) -> String: func get_control_text(action_name: String) -> String:
var code = get_control_keycode(action_name) var code = get_control_keycode(action_name)
+14 -6
View File
@@ -1,10 +1,10 @@
extends Node extends Node
# Standard Nakama Configuration # Standard Nakama Configuration
var nakama_server_key = "defaultkey" var nakama_server_key = OS.get_environment("NAKAMA_SERVER_KEY") if OS.has_environment("NAKAMA_SERVER_KEY") else ProjectSettings.get_setting("network/nakama/server_key", "defaultkey")
var nakama_host = "tektondash.vps.webdock.cloud" var nakama_host = OS.get_environment("NAKAMA_HOST") if OS.has_environment("NAKAMA_HOST") else ProjectSettings.get_setting("network/nakama/host", "tektondash.vps.webdock.cloud")
var nakama_port = 7350 var nakama_port = OS.get_environment("NAKAMA_PORT").to_int() if OS.has_environment("NAKAMA_PORT") else ProjectSettings.get_setting("network/nakama/port", 7350)
var nakama_scheme = "http" var nakama_scheme = OS.get_environment("NAKAMA_SCHEME") if OS.has_environment("NAKAMA_SCHEME") else ProjectSettings.get_setting("network/nakama/scheme", "http")
# Core Nakama Variables # Core Nakama Variables
var client: NakamaClient var client: NakamaClient
@@ -123,8 +123,16 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool:
return false return false
elif socket_result.is_exception(): elif socket_result.is_exception():
var err = socket_result.get_exception() var err = socket_result.get_exception()
printerr("[NakamaManager] Socket Error: %s (Code: %s)" % [err.message, err.status_code]) var err_msg = "Socket connection failed"
emit_signal("connection_failed", err.message) if err and not err.message.is_empty():
err_msg = err.message
elif err and err.status_code >= 0:
err_msg = "Socket connection failed with code %s" % err.status_code
printerr("[NakamaManager] Socket Error: %s (Code: %s)" % [err_msg, err.status_code if err else -1])
emit_signal("connection_failed", err_msg)
if socket:
socket.close()
socket = null
return false return false
# 3. Initialize Multiplayer Bridge # 3. Initialize Multiplayer Bridge
+18
View File
@@ -414,6 +414,15 @@ func spawn_tiles_around(count: int = 4):
# FIX 1: Make tekton look/rotate toward a random spawning direction # FIX 1: Make tekton look/rotate toward a random spawning direction
if not is_carried and not is_thrown: if not is_carried and not is_thrown:
var random_angle = rng.randf_range(0, TAU) var random_angle = rng.randf_range(0, TAU)
# If it's a static turret, make it face the target tile it's about to spawn instead
if is_static_turret:
# We don't have a specific target yet, but we can pick an average direction
# Or just let it throw randomly like the others. Wait, the user wants:
# "static tekton, should facing toward where they're going to thrown the tiles"
# We'll calculate rotation inside the spawning loop for static turrets.
pass
else:
rotation.y = random_angle rotation.y = random_angle
# Play throw animation # Play throw animation
@@ -439,6 +448,12 @@ func spawn_tiles_around(count: int = 4):
var pos = current_position + Vector2i(x, y) var pos = current_position + Vector2i(x, y)
# For static turret, update rotation to face the exact tile being thrown
if is_static_turret and not is_carried and not is_thrown:
var throw_dir = Vector3(x, 0, y).normalized()
if throw_dir.length_squared() > 0.01:
rotation.y = atan2(throw_dir.x, throw_dir.z)
# Don't overwrite the Tekton's own cell? Or do? # Don't overwrite the Tekton's own cell? Or do?
# Maybe avoid center. # Maybe avoid center.
if x == 0 and y == 0: continue if x == 0 and y == 0: continue
@@ -472,6 +487,9 @@ func spawn_tiles_around(count: int = 4):
if roll < 0.6 or (LobbyManager and LobbyManager.game_mode == "Stop n Go"): if roll < 0.6 or (LobbyManager and LobbyManager.game_mode == "Stop n Go"):
# 60% Normal Tile (7-10) OR 100% if Stop n Go (User Request) # 60% Normal Tile (7-10) OR 100% if Stop n Go (User Request)
item_id = rng.randi_range(7, 10) item_id = rng.randi_range(7, 10)
elif LobbyManager and LobbyManager.get_game_mode() == GameMode.Mode.GAUNTLET:
# Gauntlet mode: No power-up spawns from Tekton grabs
item_id = rng.randi_range(7, 10)
else: else:
# 40% PowerUp (11-14) # 40% PowerUp (11-14)
var mode = GameMode.Mode.FREEMODE var mode = GameMode.Mode.FREEMODE
+7
View File
@@ -52,6 +52,8 @@ func _ready() -> void:
server_option.selected = 0 server_option.selected = 0
elif NakamaManager.nakama_host == "tektondash.vps.webdock.cloud": elif NakamaManager.nakama_host == "tektondash.vps.webdock.cloud":
server_option.selected = 3 server_option.selected = 3
elif NakamaManager.nakama_host == "thunderobot.tapir-atria.ts.net":
server_option.selected = 4
else: else:
server_option.selected = 1 server_option.selected = 1
server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1" server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1"
@@ -241,6 +243,11 @@ func _on_server_option_selected(index: int) -> void:
server_ip_input.visible = false server_ip_input.visible = false
if lan_section: lan_section.visible = false if lan_section: lan_section.visible = false
NakamaManager.set_server("tektondash.vps.webdock.cloud") NakamaManager.set_server("tektondash.vps.webdock.cloud")
elif index == 4:
# Tekton Dash Asia
server_ip_input.visible = false
if lan_section: lan_section.visible = false
NakamaManager.set_server("thunderobot.tapir-atria.ts.net")
func _on_server_ip_submitted(new_text: String) -> void: func _on_server_ip_submitted(new_text: String) -> void:
if server_option and server_option.selected == 1: if server_option and server_option.selected == 1:
+5 -2
View File
@@ -18,8 +18,8 @@ services:
restart: unless-stopped restart: unless-stopped
nakama: nakama:
container_name: nakama container_name: Tekton-Dash-Asia
image: heroiclabs/nakama:3.24.2 image: heroiclabs/nakama:latest
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@@ -35,6 +35,9 @@ services:
--console.username admin --console.username admin
--console.password password --console.password password
--runtime.path /nakama/data/modules --runtime.path /nakama/data/modules
--runtime.js_entrypoint tekton_admin.js
volumes: volumes:
- ./nakama:/nakama/data/modules - ./nakama:/nakama/data/modules
ports: ports:
+133
View File
@@ -0,0 +1,133 @@
extends Node
class Log:
enum {ERROR, INFO, DEBUG}
static func _s(lvl):
if lvl == ERROR:
return "ERROR"
elif lvl == INFO:
return "INFO"
elif lvl == DEBUG:
return "DEBUG"
return "WTF"
static func __log(lvl, msg, data):
print("======= %s: %s" % [_s(lvl), msg])
if not data.is_empty():
var json = JSON.new()
print(json.stringify(data, " ", true))
static func error(msg, data={}):
__log(ERROR, msg, data)
static func info(msg, data={}):
__log(INFO, msg, data)
static func debug(msg, data={}):
__log(DEBUG, msg, data)
const REAL_ASSERT = false
var _success = false
var _start_time = 0
var _quit = false
var _disabled = false
var __me = ""
# Override this to specify test initialization (called on _ready if not disabled)
# You can run your whole test here, and call done() when finished
func setup():
pass
# Override this to do cleanup, make assertions at the end of the test (called on _exit_tree)
# NOTE: You can still fail here if you like with fail() or by failing an assertion
func teardown():
pass
func _init():
__me = get_script()
while __me.get_base_script() != null:
__me = __me.get_base_script()
func _ready():
if _disabled:
set_process(false)
set_physics_process(false)
set_process_input(false)
set_process_unhandled_input(false)
set_process_unhandled_key_input(false)
done()
Log.info("SKIP: %s" % __get_source(self))
return
_start_time = Time.get_ticks_usec()
Log.info("RUNNING: %s" % __get_source(self))
setup()
func _exit_tree():
if _disabled:
return
teardown()
Log.info("%s: %s" % ["SUCCESS" if _success else "!!!!!!!!!!!!!!!!!!! FAILURE", __get_source(self)])
func __get_source(who):
if who == null or who.get_script() == null:
return "Unknown source: %s" % str(who)
return who.get_script().resource_path
func __get_caller():
for s in get_stack():
if __me.resource_path != s.source:
return s
return null
func __get_assertion():
var stack = get_stack()
stack.reverse()
for s in stack:
if __me.resource_path == s.source:
return s
return null
func __assert(v1, v2):
if not (v1 == v2):
Log.error("Assert Failed", {"caller": __get_caller(), "assertion": __get_assertion(), "full_stack": get_stack()})
print_tree_pretty()
_quit = true
if REAL_ASSERT:
assert(v1 == v2)
if v1 == v2:
return false
return true
func assert_time(max_time):
__assert(max_time > float(Time.get_ticks_usec() - _start_time) / 1000.0 / 1000.0, true)
### Returns true if the assertion failed, so you can do:
### if assert_cond(cond):
### return # Assertion failed!
func assert_cond(cond):
return __assert(cond, true)
func assert_false(cond):
return __assert(cond, false)
func assert_equal(v1, v2):
return __assert(v1, v2)
func done():
_success = true
_quit = true
func fail():
_success = false
_quit = true
__assert(false, true)
func disable():
_disabled = true
func is_disabled():
return _disabled
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+119
View File
@@ -0,0 +1,119 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
_global_script_classes=[{
"base": "RefCounted",
"class": &"NakamaAPI",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/api/NakamaAPI.gd"
}, {
"base": "RefCounted",
"class": &"NakamaAsyncResult",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/utils/NakamaAsyncResult.gd"
}, {
"base": "RefCounted",
"class": &"NakamaClient",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/client/NakamaClient.gd"
}, {
"base": "RefCounted",
"class": &"NakamaException",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/utils/NakamaException.gd"
}, {
"base": "Node",
"class": &"NakamaHTTPAdapter",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/client/NakamaHTTPAdapter.gd"
}, {
"base": "RefCounted",
"class": &"NakamaLogger",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/utils/NakamaLogger.gd"
}, {
"base": "RefCounted",
"class": &"NakamaMultiplayerBridge",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/utils/NakamaMultiplayerBridge.gd"
}, {
"base": "MultiplayerPeerExtension",
"class": &"NakamaMultiplayerPeer",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/utils/NakamaMultiplayerPeer.gd"
}, {
"base": "NakamaAsyncResult",
"class": &"NakamaRTAPI",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd"
}, {
"base": "RefCounted",
"class": &"NakamaRTMessage",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/api/NakamaRTMessage.gd"
}, {
"base": "RefCounted",
"class": &"NakamaSerializer",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/utils/NakamaSerializer.gd"
}, {
"base": "NakamaAsyncResult",
"class": &"NakamaSession",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/api/NakamaSession.gd"
}, {
"base": "RefCounted",
"class": &"NakamaSocket",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/socket/NakamaSocket.gd"
}, {
"base": "Node",
"class": &"NakamaSocketAdapter",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/socket/NakamaSocketAdapter.gd"
}, {
"base": "RefCounted",
"class": &"NakamaStorageObjectId",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/api/NakamaStorageObjectId.gd"
}, {
"base": "RefCounted",
"class": &"NakamaWriteStorageObject",
"language": &"GDScript",
"path": "res://addons/com.heroiclabs.nakama/api/NakamaWriteStorageObject.gd"
}]
_global_script_class_icons={
"NakamaAPI": "",
"NakamaAsyncResult": "",
"NakamaClient": "",
"NakamaException": "",
"NakamaHTTPAdapter": "",
"NakamaLogger": "",
"NakamaMultiplayerBridge": "",
"NakamaMultiplayerPeer": "",
"NakamaRTAPI": "",
"NakamaRTMessage": "",
"NakamaSerializer": "",
"NakamaSession": "",
"NakamaSocket": "",
"NakamaSocketAdapter": "",
"NakamaStorageObjectId": "",
"NakamaWriteStorageObject": ""
}
[application]
run/main_scene="res://tester.tscn"
config/features=PackedStringArray("4.0")
[autoload]
Nakama="*res://addons/com.heroiclabs.nakama/Nakama.gd"
Config="*res://utils/config.gd"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
END_STRING="======= TESTS END"
PROJECT_PATH="test_suite/"
GODOT_BIN="godot"
OUT=`$GODOT_BIN --headless --debug --path test_suite/ -s res://runner.gd`
RUN=`echo $OUT | grep "$END_STRING"`
RES=`echo $OUT | grep FAILURE`
echo "$OUT"
if [ -z "$RUN" ]; then
echo "Run failed!"
exit 1
fi
if [ -z "$RES" ]; then
echo "Tests passed!"
exit 0
fi
echo "Tests failed!"
exit 1
+15
View File
@@ -0,0 +1,15 @@
extends SceneTree
func _init():
var me = ProjectSettings.localize_path(ProjectSettings.globalize_path(get_script().resource_path))
var dir = ProjectSettings.localize_path(me.get_base_dir())
if dir != "res://":
print("Must be run with the script dir as the res:// path")
print("Current path: ", ProjectSettings.globalize_path(dir))
print("RES:// path: ", ProjectSettings.globalize_path("res://"))
quit()
return
var n = load("res://tester.gd").new()
root.add_child(n)
current_scene = n
+7
View File
@@ -0,0 +1,7 @@
{
"HOST": "127.0.0.1",
"PORT": 7350,
"SCHEME": "http",
"SERVER_KEY": "defaultkey"
}
+56
View File
@@ -0,0 +1,56 @@
extends Node
var current = null
var tests = []
var frame_time = 0.0
var fixed_time = 0.0
var dirs = ["tests"]
func _ready():
while not dirs.is_empty():
var d = dirs.pop_front()
_expand(d, dirs)
print("Running %d tests:\n%s" % [tests.size(), tests])
func _process(delta):
frame_time += delta
_check_end()
func _physics_process(delta):
fixed_time += delta
_check_end()
func _check_end():
if current == null:
if tests.size() == 0:
_end_tests()
return
current = load(tests.pop_front()).new()
add_child(current)
elif current._quit:
if not current._success:
_end_tests()
current.queue_free()
current = null
func _end_tests():
print("======= TESTS END")
set_process(false)
set_physics_process(false)
await get_tree().create_timer(1.0).timeout
get_tree().call_deferred("quit")
func _expand(p_name, r_dirs):
var dir = DirAccess.open("res://")
if dir.change_dir(p_name) != OK:
print("Unable to chdir into: %s" % p_name)
return
dir.list_dir_begin()
var f = dir.get_next()
while f != "":
if dir.current_is_dir():
r_dirs.append("%s/%s" % [p_name, f])
if f.ends_with("_test.gd"):
tests.append("%s/%s" % [p_name, f])
f = dir.get_next()
dir.list_dir_end()
+12
View File
@@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3 uid="uid://bglgahpqv1ojs"]
[ext_resource type="Script" path="res://tester.gd" id="1"]
[node name="Control" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
+32
View File
@@ -0,0 +1,32 @@
extends "res://base_test.gd"
func setup():
var username = str(randi_range(1000, 100000))
var client = Nakama.create_client(Config.SERVER_KEY, Config.HOST, Config.PORT, Config.SCHEME)
# POST
var session = await client.authenticate_custom_async("MyIdentifier")
if assert_cond(session.is_valid()) or assert_cond(!session.is_expired()):
return
# PUT
var update = await client.update_account_async(session, username)
if assert_false(update.is_exception()):
return
# GET
var account : NakamaAPI.ApiAccount = await client.get_account_async(session)
if assert_false(account.is_exception()):
return
if assert_cond(account.user.username == username):
return
# POST - DELETE
var group : NakamaAPI.ApiGroup = await client.create_group_async(session, "MyGroupName3")
if assert_false(group.is_exception()):
return
var delete = await client.delete_group_async(session, group.id)
if assert_false(delete.is_exception()):
return
# All good
done()
return
func _process(_delta):
assert_time(3)
+79
View File
@@ -0,0 +1,79 @@
extends "res://base_test.gd"
var content = {"My": "message"}
var match_string_props = {"region": "europe"}
var match_numeric_props = {"rank": 8}
var got_msg = false
var got_match = false
var socket1 = null
var socket2 = null
func setup():
var client = Nakama.create_client(Config.SERVER_KEY, Config.HOST, Config.PORT, Config.SCHEME)
var session1 = await client.authenticate_custom_async("MyIdentifier")
if assert_cond(session1.is_valid()):
return
var session2 = await client.authenticate_custom_async("MyIdentifier2")
if assert_cond(session1.is_valid()):
return
socket1 = Nakama.create_socket_from(client)
socket1.received_channel_message.connect(self._on_socket1_message)
socket1.received_matchmaker_matched.connect(self._on_socket1_matchmaker_matched)
var done = await socket1.connect_async(session1)
# Check that connection succeded
if assert_false(done.is_exception()):
return
socket2 = Nakama.create_socket_from(client)
done = await socket2.connect_async(session2)
# Check that connection succeded
if assert_false(done.is_exception()):
return
# Join room
var room1 = await socket1.join_chat_async("MyRoom", NakamaSocket.ChannelType.Room)
if assert_false(room1.is_exception()):
return
var room2 = await socket2.join_chat_async("MyRoom", NakamaSocket.ChannelType.Room)
if assert_false(room2.is_exception()):
return
# Socket 2 send message to socket 1
var msg_ack = await socket2.write_chat_message_async(room2.id, content)
if assert_false(msg_ack.is_exception()):
return
var ticket1 = await socket1.add_matchmaker_async("+properties.region:europe +properties.rank:>=7 +properties.rank:<=9", 2, 8, match_string_props, match_numeric_props)
if assert_false(ticket1.is_exception()):
return
var ticket2 = await socket2.add_matchmaker_async("+properties.region:europe +properties.rank:>=7 +properties.rank:<=9", 2, 8, match_string_props, match_numeric_props)
if assert_false(ticket2.is_exception()):
return
func _on_socket1_message(msg):
if assert_equal(msg.content, JSON.stringify(content)):
return
got_msg = true
check_end()
func _on_socket1_matchmaker_matched(p_matchmaker_matched):
if assert_equal(JSON.stringify(p_matchmaker_matched.users[0].string_properties), JSON.stringify(match_string_props)):
return
if assert_equal(JSON.stringify(p_matchmaker_matched.users[0].numeric_properties), JSON.stringify(match_numeric_props)):
return
if assert_equal(JSON.stringify(p_matchmaker_matched.users[1].string_properties), JSON.stringify(match_string_props)):
return
if assert_equal(JSON.stringify(p_matchmaker_matched.users[1].numeric_properties), JSON.stringify(match_numeric_props)):
return
got_match = true
check_end()
func _process(_delta):
assert_time(60)
func check_end():
if got_match and got_msg:
done()
+98
View File
@@ -0,0 +1,98 @@
extends "res://base_test.gd"
var content = {"My": "message"}
var match_props = {"region": "europe"}
var got_msg = false
var got_match = false
var socket1 : NakamaSocket = null
var socket2 : NakamaSocket = null
func setup():
var client = Nakama.create_client(Config.SERVER_KEY, Config.HOST, Config.PORT, Config.SCHEME)
var session1 = await client.authenticate_custom_async("MyIdentifier")
if assert_cond(session1.is_valid()):
return
var session2 = await client.authenticate_custom_async("MyIdentifier2")
if assert_cond(session2.is_valid()):
return
socket1 = Nakama.create_socket_from(client)
socket1.received_party_close.connect(self._on_party_close)
socket1.received_party_data.connect(self._on_party_data)
socket1.received_party_join_request.connect(self._on_party_join_request)
var conn = await socket1.connect_async(session1)
# Check that connection succeded
if assert_false(conn.is_exception()):
return
var party = await socket1.create_party_async(false, 2)
if assert_false(party.is_exception()):
return
#done()
socket2 = Nakama.create_socket_from(client)
socket2.received_party.connect(self._on_party)
socket2.received_party_close.connect(self._on_party_close)
socket2.received_party_join_request.connect(self._on_party_join_request)
socket2.received_party_leader.connect(self._on_party_leader)
socket2.received_party_presence.connect(self._on_party_presence)
var conn2 = await socket2.connect_async(session2)
# Check that connection succeded
if assert_false(conn2.is_exception()):
return
var join = await socket2.join_party_async(party.party_id)
if assert_false(join.is_exception()):
return
func _on_party_join_request(party_join_request : NakamaRTAPI.PartyJoinRequest):
prints("_on_party_join_request", party_join_request)
var requests : NakamaRTAPI.PartyJoinRequest = await socket1.list_party_join_requests_async(party_join_request.party_id)
if assert_false(requests.is_exception()):
return
if assert_cond(requests.presences.size() == 1):
return
await socket1.accept_party_member_async(party_join_request.party_id, party_join_request.presences[0])
await socket1.promote_party_member(party_join_request.party_id, party_join_request.presences[0])
func _on_party(party):
prints("_on_party", party)
func _on_party_close(party_close):
prints("_on_party_close", party_close)
func _on_party_data(data : NakamaRTAPI.PartyData):
prints("_on_party_data", data)
var left = await socket1.leave_party_async(data.party_id)
if assert_false(left.is_exception()):
return
func _on_party_leader(party_leader : NakamaRTAPI.PartyLeader):
prints("_on_party_leader", party_leader)
var ticket = await socket2.add_matchmaker_party_async(party_leader.party_id)
if assert_false(ticket.is_exception()):
return
_on_party_ticket(ticket)
func _on_party_ticket(ticket : NakamaRTAPI.PartyMatchmakerTicket):
prints("_on_party_ticket", ticket)
var removed = await socket2.remove_matchmaker_party_async(ticket.party_id, ticket.ticket)
if assert_false(removed.is_exception()):
return
var sent = await socket2.send_party_data_async(ticket.party_id, 1, "asd")
if assert_false(sent.is_exception()):
return
func _on_party_presence(party_presence : NakamaRTAPI.PartyPresenceEvent):
prints("_on_party_presence", party_presence)
var left = party_presence.leaves.size() == 1
if left:
var closed = await socket2.close_party_async(party_presence.party_id)
if assert_false(closed.is_exception()):
return
done()
+27
View File
@@ -0,0 +1,27 @@
extends "res://base_test.gd"
var _connected = false
func setup():
var client = Nakama.create_client(Config.SERVER_KEY, Config.HOST, Config.PORT, Config.SCHEME)
var session = await client.authenticate_custom_async("MyIdentifier")
if assert_cond(session.is_valid()):
return
var socket = Nakama.create_socket_from(client)
socket.connected.connect(self._on_socket_connected)
var conn = await socket.connect_async(session)
# Check that connection succeded
if assert_false(conn.is_exception()):
return
# Check that signal has been called
if assert_cond(_connected):
return
done()
func _on_socket_connected():
_connected = true
func _process(_delta):
assert_time(3)
+72
View File
@@ -0,0 +1,72 @@
extends "res://base_test.gd"
const COLLECTION = "ACollection"
const K1 = "Question"
const K2 = "Answer"
const V1 = {"question": "Ultimate question"}
const V2 = {"answer": "42"}
func setup():
var username = str(randi_range(1000, 100000))
var client = Nakama.create_client(Config.SERVER_KEY, Config.HOST, Config.PORT, Config.SCHEME)
var session = await client.authenticate_custom_async("MyIdentifier")
if assert_cond(session.is_valid()):
return
var objs = [
NakamaWriteStorageObject.new(COLLECTION, K1, 1, 1, to_json(V1), ""),
NakamaWriteStorageObject.new(COLLECTION, K2, 1, 1, to_json(V2), "")
]
var write : NakamaAPI.ApiStorageObjectAcks = await client.write_storage_objects_async(session, objs)
if assert_false(write.is_exception()):
return
var objs_ids = []
for a in write.acks:
#var obj : NakamaAPI.ApiStorageObjectAck = a
objs_ids.append(NakamaStorageObjectId.new(a.collection, a.key, a.user_id, a.version))
var read : NakamaAPI.ApiStorageObjects = await client.read_storage_objects_async(session, objs_ids)
if assert_false(read.is_exception()):
return
if assert_equal(read.objects.size(), 2):
return
if assert_equal(read.objects[0].collection, COLLECTION):
return
if assert_cond(read.objects[0].key in [K1, K2]):
return
if assert_cond(to_json(parse_json(read.objects[0].value)) in [to_json(V1), to_json(V2)]):
return
# Delete one
var del = await client.delete_storage_objects_async(session, [objs_ids[0]])
if assert_false(del.is_exception()):
return
# Confirm that one was deleted
var read2 : NakamaAPI.ApiStorageObjects = await client.read_storage_objects_async(session, objs_ids)
if assert_false(read2.is_exception()):
return
if assert_equal(read2.objects.size(), 1):
return
if assert_equal(read2.objects[0].collection, COLLECTION):
return
if assert_equal(read2.objects[0].key, objs_ids[1].key):
return
if assert_cond(to_json(parse_json(read2.objects[0].value)) in [to_json(V1), to_json(V2)]):
return
done()
return
func _process(_delta):
assert_time(3)
func to_json(value) -> String:
return JSON.stringify(value)
func parse_json(value):
var json = JSON.new()
if json.parse(value) != OK:
return null
return json.get_data()
+22
View File
@@ -0,0 +1,22 @@
extends Node
var HOST = "127.0.0.1"
var PORT = 7350
var SCHEME = "http"
var SERVER_KEY = "defaultkey"
func _ready():
var f = FileAccess.open("res://settings.json", FileAccess.READ)
if not f:
return
var json = JSON.new()
var error = json.parse(f.get_as_text())
var parsed = json.get_data()
if error != OK or typeof(parsed) != TYPE_DICTIONARY:
return
for k in parsed:
match k:
"HOST": HOST = parsed[k]
"PORT": PORT = parsed[k]
"SCHEME": SCHEME = parsed[k]
"SERVER_KEY": SERVER_KEY = parsed[k]
Binary file not shown.