diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..4ad3435 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,4 @@ + +[codespell] +skip = assets/**,addons/**,LICENSES/** +ignore-words-list = lod,LOD diff --git a/.env b/.env new file mode 100644 index 0000000..10abf5c --- /dev/null +++ b/.env @@ -0,0 +1,11 @@ +# Variables used by the Justfile + +# Godot + +GODOT_VERSION=4.2.1 + +# Game + +GAME_NAME=Greeter +GAME_VERSION=0.1.0 +GAME_ITCHIO_KEY=greeter diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4ac4a23 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf +*.gd linguist-language=GDScript +*.hdr binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7d73cef --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + time: '00:00' + timezone: UTC + open-pull-requests-limit: 10 + commit-message: + prefix: "chore" + include: "scope" + labels: + - "dependabot" + - "dependencies" diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..720f78c --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,35 @@ +{ + "regexManagers": [ + { + "fileMatch": ["^plug\\.gd$"], + "matchStrings": [ + "\\s+plug\\(\"(?.*?)\",\\ \\{\\s*\"commit\":\\ \"(?)(?.*?)\"" + ], + "depNameTemplate": "{{{gitUrl}}}", + "packageNameTemplate": "https://github.com/{{{gitUrl}}}", + "versioningTemplate": "git", + "datasourceTemplate": "git-refs" + }, + { + "fileMatch": ["^plug\\.gd$"], + "matchStrings": [ + "\\s+plug\\(\"(?.*?)\",\\ \\{\\s*\"tag\":\\ \"(?)(?.*?)\"" + ], + "depNameTemplate": "{{{gitUrl}}}", + "packageNameTemplate": "https://github.com/{{{gitUrl}}}", + "versioningTemplate": "git", + "datasourceTemplate": "git-tags" + }, + { + "fileMatch": ["^.env$"], + "matchStrings": [ + "GODOT_VERSION=(?.*?)\\n" + ], + "depNameTemplate": "godotengine/godot", + "packageNameTemplate": "https://github.com/godotengine/godot", + "versioningTemplate": "loose", + "extractVersionTemplate": "^(?.*)-stable$", + "datasourceTemplate": "git-tags" + } + ] +} diff --git a/.github/workflows/changelog_verifier.yml b/.github/workflows/changelog_verifier.yml new file mode 100644 index 0000000..3582306 --- /dev/null +++ b/.github/workflows/changelog_verifier.yml @@ -0,0 +1,15 @@ +name: Changelog Verifier + +on: + pull_request: + types: [opened, edited, review_requested, synchronize, reopened, ready_for_review, labeled, unlabeled] + +jobs: + # Enforces the update of a changelog file on every pull request + verify-changelog: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dangoslen/changelog-enforcer@v3 + with: + skipLabels: "autocut, skip-changelog" diff --git a/.github/workflows/dependabot_pr.yml b/.github/workflows/dependabot_pr.yml new file mode 100644 index 0000000..4b9f082 --- /dev/null +++ b/.github/workflows/dependabot_pr.yml @@ -0,0 +1,30 @@ +name: Dependabot PR actions + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +jobs: + dependabot: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Update the changelog + uses: dangoslen/dependabot-changelog-helper@v3 + with: + version: 'Unreleased' + + - name: Commit the changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "chore(changelog): automatic update to add dependabot deps bump" + branch: ${{ github.head_ref }} + commit_user_name: dependabot[bot] + commit_user_email: support@github.com diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 0000000..d1bbb40 --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,19 @@ +name: Link Checker +on: + push: + +jobs: + linkchecker: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: lychee Link Checker + id: lychee + uses: lycheeverse/lychee-action@v1 + with: + args: --accept=200,403,429 "**/*.html" "**/*.md" "**/*.txt" "**/*.json" --exclude "file:///github/workspace/*" --exclude-path ".github/renovate.json" --exclude-mail + fail: true + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/lint_pr_title.yml b/.github/workflows/lint_pr_title.yml new file mode 100644 index 0000000..405408f --- /dev/null +++ b/.github/workflows/lint_pr_title.yml @@ -0,0 +1,34 @@ +name: Lint PR Title + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + +permissions: + pull-requests: read + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + scopes: | + addons + assets + scripts + scenes + dependabot + workflows + readme + changelog + deps + requireScope: false + validateSingleCommit: true + validateSingleCommitMatchesPrTitle: true diff --git a/.github/workflows/release-packaging.yml b/.github/workflows/release-packaging.yml new file mode 100644 index 0000000..46368bb --- /dev/null +++ b/.github/workflows/release-packaging.yml @@ -0,0 +1,107 @@ +name: Release Packaging + +on: + push: + workflow_dispatch: + +env: + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + +jobs: + check: + runs-on: ubuntu-22.04 + timeout-minutes: 30 + + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v1 + + - name: Load dotenv + run: just ci-load-dotenv + + - name: Check + run: just fmt + + - name: Ensure version is equal to tag + if: startsWith(github.ref, 'refs/tags/') + run: | + [ "${{ env.game_version }}" == "${{ env.BRANCH_NAME }}" ] || exit 2 + + build: + runs-on: ubuntu-22.04 + timeout-minutes: 30 + needs: [check] + + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v1 + + - name: Load dotenv + run: just ci-load-dotenv + + - name: Export + run: just export + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.game_name }}-v${{ env.game_version }} + path: dist/* + retention-days: 1 + + deploy: + runs-on: ubuntu-22.04 + timeout-minutes: 30 + needs: [check] + + if: github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v1 + + - name: Load dotenv + run: just ci-load-dotenv + + - name: Export + run: just export-web + + # Installing rsync is needed in order to deploy to GitHub Pages. Without it, the build will fail. + - name: Install rsync + run: | + sudo apt-get update && sudo apt-get install -y rsync + + - name: Deploy to GitHub Pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages + folder: build/web + + publish: + runs-on: ubuntu-22.04 + timeout-minutes: 30 + needs: [build] + + if: startsWith(github.ref, 'refs/tags/') + + steps: + - uses: actions/checkout@v4 + - uses: extractions/setup-just@v1 + + - name: Load dotenv + run: just ci-load-dotenv + + - name: Download artifact + uses: dawidd6/action-download-artifact@v3 + with: + workflow: ${{ github.event.workflow_run.workflow_id }} + name: ${{ env.game_name }}-v${{ env.game_version }} + path: dist/ + skip_unpack: false + + - name: Publish + run: just publish + env: + BUTLER_API_KEY: ${{ secrets.BUTLER_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be48c14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Godot-specific ignores +.godot/ +.import/ +gfxrecon_capture_* + +# Imported translations (automatically generated from CSV files) +*.translation + +# Mono-specific ignores +.mono/ +data_*/ + +# gd-plug +.plugged/ +addons/* +!addons/gd-plug/ + +# Python-specific ignores +venv/ + +# Export output +dist/ +build/ +override.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..288dad0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,72 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: fix-byte-order-marker # Prevents weird UTF-8 encoding edge cases + - id: check-case-conflict # Check if case-insensitive filesystems would bork + - id: check-docstring-first # Check for if docstring was misplaced + - id: check-executables-have-shebangs + - id: check-json # Checks for valid json + - id: check-merge-conflict # Checks strings that look like a committed merge conflict + - id: check-xml # Checks for valid xml + - id: check-yaml # Checks for valid yaml + - id: end-of-file-fixer # Checks for ending with a newline + - id: mixed-line-ending # Consistent LF or CRLF + - id: trailing-whitespace # No trailing whitespace +- repo: https://github.com/fsfe/reuse-tool + rev: v2.0.0 + hooks: + - id: reuse +- repo: https://github.com/codespell-project/codespell + rev: v2.2.5 + hooks: + - id: codespell +- repo: local + hooks: + - id: lower-case-only + name: lower case only + entry: filenames must be lower-case or lower_case only + language: fail + files: '[^a-z0-9._/-]' + exclude: | + (?x)^( + .reuse/| + LICENSES/| + public/| + Justfile| + CONTRIBUTING.md| + CHANGELOG.md| + CREDITS.md| + LICENSE.md| + README.md + ) + - id: check-gdscript + name: check gdscript + entry: gdformat + language: system + files: \.gd$ + exclude: | + (?x)^( + addons/| + plug.gd + ) + - id: check-shaders + name: check shaders + entry: clang-format + args: + - --style=llvm + - -Werror + - -i + language: system + files: \.gdshader$ + exclude: ^addons/ + - id: lint-gdscript + name: lint gdscript + entry: gdlint + language: system + files: \.gd$ + exclude: | + (?x)^( + addons/| + plug.gd + ) diff --git a/.reuse/REUSE-compliant.svg b/.reuse/REUSE-compliant.svg new file mode 100644 index 0000000..5b2a2f4 --- /dev/null +++ b/.reuse/REUSE-compliant.svg @@ -0,0 +1 @@ +REUSE: compliantREUSEcompliant diff --git a/.reuse/dep5 b/.reuse/dep5 new file mode 100644 index 0000000..825c07e --- /dev/null +++ b/.reuse/dep5 @@ -0,0 +1,30 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: Greeter +Upstream-Contact: Florian Vazelle +Source: https://github.com/MechanicalFlower/game-template + +Files: * +Copyright: 2023 Florian Vazelle +License: MIT + +# Addons + +Files: addons/gd-plug/* +Copyright: 2021 Tan Jian Ping +License: MIT +Source: https://github.com/imjp94/gd-plug + +Files: addons/debug_menu/* +Copyright: 2023-present Hugo Locurcio and contributors +License: MIT +Source: https://github.com/godot-extended-libraries/godot-debug-menu + +Files: addons/UniversalFade/* +Copyright: 2019 Tomek +License: MIT +Source: https://github.com/KoBeWi/Godot-Universal-Fade + +Files: addons/EasyMenus/* +Copyright: 2022 Savo Vuksan +License: MIT +Source: https://github.com/SavoVuksan/EasyMenus diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0b11476 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# CHANGELOG +Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) + +## [Unreleased] +### Added +- Add web deploy ([#8](https://github.com/MechanicalFlower/godot-template/pull/8)) +- Automating Godot updates with renovate ([#12](https://github.com/MechanicalFlower/godot-template/pull/12)) +### Changed +- Use Justfile as command runner ([#7](https://github.com/MechanicalFlower/godot-template/pull/7)) +### Deprecated +### Removed +### Fixed +### Security +### Dependencies + +[Unreleased]: https://github.com/MechanicalFlower/godot-template/compare/0.1.0...HEAD +- Bump `actions/upload-artifact` from 3 to 4 ([#18](https://github.com/MechanicalFlower/godot-template/pull/18)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a9750b1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing + +We welcome contributions to our open source Godot + game project! There are many ways you can help, + including reporting bugs, improving documentation, + and contributing code. + +## Code of Conduct + +We value the participation of every member of our + community and want to ensure that everyone + has an enjoyable and fulfilling experience. As + such, we have adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as + our code of conduct. By participating in this + project, you agree to abide by its terms. + +## Contributing Code + +To contribute code to the project, follow these steps: + +1. Fork the repository to your own GitHub account. +2. Clone the repository to your local machine. +3. Create a new branch for your changes. +4. Make your changes and commit them to your local repository. +5. Push your changes to your forked repository on GitHub. +6. Create a pull request from your forked repository to the original repository. + +Please note that all code contributions should + pass the continuous integration (CI) checks + that are set up for the project. These checks + ensure that the code is well-formatted and + that tests are passing. + + +## Reporting Bugs + +If you find a bug in the project, please report + it by creating an issue in the repository's issue + tracker. Be sure to include as much information + as possible, including the steps to reproduce + the bug and any relevant error messages. + +## Improving Documentation + +If you would like to improve the documentation + for the project, you can do so by submitting a + pull request with your changes. Please follow + the same process as for contributing code, and + make sure that your changes are properly formatted + and well-written. + +## Questions and Feedback + +If you have any questions or feedback about the + project, don't hesitate to reach out! You can + create an issue in the repository's issue tracker, + or contact us directly through our website or + social media channels. + +Thank you for considering contributing to our + open source Godot game project! We appreciate + your help and look forward to working with you. diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..6264fb6 --- /dev/null +++ b/Justfile @@ -0,0 +1,222 @@ +#!/usr/bin/env -S just --justfile + +set dotenv-load := true + +export PIP_REQUIRE_VIRTUALENV := "true" + +# === Aliases === + +[private] +alias g := godot + +[private] +alias e := editor + +# === Variables === + +# Global directories +# To make the Godot binaries available for other projects +home_dir := env_var('HOME') +main_dir := home_dir / ".mkflower" +cache_dir := main_dir / "cache" +bin_dir := main_dir / "bin" + +# Local directories +build_dir := justfile_directory() / "build" +dist_dir := justfile_directory() / "dist" + +# Godot variables +godot_version := env_var('GODOT_VERSION') +godot_platform := if arch() == "x86" { + "linux.x86_32" +} else { + if arch() == "x86_64" { + "linux.x86_64" + } else { + if arch() == "arm" { + "linux.arm32" + } else { + if arch() == "aarch64" { + "linux.arm64" + } else { "" } + } + } +} +godot_filename := "Godot_v" + godot_version + "-stable_" + godot_platform +godot_template := "Godot_v" + godot_version + "-stable_export_templates.tpz" +godot_bin := bin_dir / godot_filename +godot_editor_data_dir := "~/.local/share/godot/" + +# Game variables +game_name := env_var('GAME_NAME') +game_version := env_var('GAME_VERSION') +game_itchio_key := env_var_or_default('GAME_ITCHIO_KEY', "") + +# Build info +datetime := `date '+%Y%m%d'` +short_version := replace_regex(game_version, "([0-9]+).([0-9]+).[0-9]+", "$1.$2") +build_date := `date +'%Y/%m/%d'` +commit_hash := `git log --pretty=format:"%H" -1` + +# Python virtualenv +venv_dir := justfile_directory() / "venv" + +# Butler binary +butler_bin := bin_dir / "butler" +butler_platform := if arch() == "x86" { "linux-386" } else { if arch() == "x86_64" { "linux-amd64" } else{ "" } } + +# === Commands === + +# Display all commands +@default: + echo "OS: {{ os() }} - ARCH: {{ arch() }}\n" + just --list + +# Create directories +[private] +@makedirs: + mkdir -p {{ cache_dir }} {{ bin_dir }} {{ build_dir }} {{ dist_dir }} + +# Python virtualenv wrapper +[private] +@venv *ARGS: + [ ! -d {{ venv_dir }} ] && python3 -m venv {{ venv_dir }} || true + . {{ venv_dir }}/bin/activate && {{ ARGS }} + +# Download Godot +[private] +install-godot: + #!/usr/bin/env sh + if [ ! -e {{ godot_bin }} ] + then + curl -X GET "https://downloads.tuxfamily.org/godotengine/{{ godot_version }}/{{ godot_filename }}.zip" --output {{ cache_dir }}/{{ godot_filename }}.zip + unzip {{ cache_dir }}/{{ godot_filename }}.zip -d {{ cache_dir }} + cp {{ cache_dir }}/{{ godot_filename }} {{ godot_bin }} + fi + +# Download Godot export templates +[private] +install-templates: + #!/usr/bin/env sh + if [ ! -d {{ godot_editor_data_dir }}/export_templates/{{ godot_version }}.stable ] + then + curl -X GET "https://downloads.tuxfamily.org/godotengine/{{ godot_version }}/{{ godot_template }}" --output {{ cache_dir }}/{{ godot_template }} + unzip {{ cache_dir }}/{{ godot_template }} -d {{ cache_dir }} + mkdir -p {{ godot_editor_data_dir }}/export_templates/{{ godot_version }}.stable + cp {{ cache_dir }}/templates/* {{ godot_editor_data_dir }}/export_templates/{{ godot_version }}.stable + fi + +# Download game plugins +install-addons: + [ -f plug.gd ] && just godot --headless --script plug.gd install || true + +# Workaround from https://github.com/godotengine/godot/pull/68461 +# Import game resources +import-resources: + just godot --headless --export-pack null /dev/null + # timeout 60 just godot --editor || true + # just godot --headless --quit --editor + +# Updates the game version for export +@bump-version: + echo "Update version in the presets.cfg" + sed -i "s,application/file_version=.*$,application/file_version=\"{{ game_version }}.{{ datetime }}\",g" ./export_presets.cfg + sed -i "s,application/product_version=.*$,application/product_version=\"{{ game_version }}.{{ datetime }}\",g" ./export_presets.cfg + sed -i "s,application/version=.*$,application/version=\"{{ game_version }}\",g" ./export_presets.cfg + sed -i "s,application/short_version=.*$,application/short_version=\"{{ short_version }}\",g" ./export_presets.cfg + + echo "Create the override.cfg" + touch override.cfg + echo '[build_info]\npackage/version="{{ game_version }}"\npackage/build_date="{{ build_date }}"\nsource/commit="{{ commit_hash }}"' > override.cfg + +# Godot binary wrapper +@godot *ARGS: makedirs install-godot install-templates + {{ godot_bin }} {{ ARGS }} + +# Open the Godot editor +editor: + just godot --editor + +# Run files formatters +fmt: + just venv pip install pre-commit==3.3.3 reuse==2.1.0 gdtoolkit==4.* + just venv pre-commit run -a + +# Export game on Windows +export-windows: bump-version install-addons import-resources + mkdir -p {{ build_dir }}/windows + just godot --export-release '"Windows Desktop"' --headless {{ build_dir }}/windows/{{ game_name }}.exe + (cd {{ build_dir }}/windows && zip {{ game_name }}-windows-v{{ game_version }}.zip -r .) + mv {{ build_dir }}/windows/{{ game_name }}-windows-v{{ game_version }}.zip {{ dist_dir }}/{{ game_name }}-windows-v{{ game_version }}.zip + rm -rf {{ build_dir }}/windows + +# Export game on MacOS +export-mac: bump-version install-addons import-resources + just godot --export-release "macOS" --headless {{ dist_dir }}/{{ game_name }}-mac-v{{ game_version }}.zip + +# Export game on Linux +export-linux: bump-version install-addons import-resources + mkdir -p {{ build_dir }}/linux + just godot --export-release "Linux/X11" --headless {{ build_dir }}/linux/{{ game_name }}.x86_64 + (cd {{ build_dir }}/linux && zip {{ game_name }}-linux-v{{ game_version }}.zip -r .) + mv {{ build_dir }}/linux/{{ game_name }}-linux-v{{ game_version }}.zip {{ dist_dir }}/{{ game_name }}-linux-v{{ game_version }}.zip + rm -rf {{ build_dir }}/linux + +# Export game for the web +export-web: bump-version install-addons import-resources + mkdir -p {{ build_dir }}/web + just godot --export-release "Web" --headless {{ build_dir }}/web/index.html + +# Export on all platform +export: export-windows export-mac export-linux + +# Remove cache and binaries created by this Justfile +[private] +clean-mkflower: + rm -rf {{ main_dir }} + rm -rf {{ venv_dir }} + +# Remove files created during the export +clean-export: + rm -rf {{ build_dir }} {{ dist_dir }} + +# Remove files created by Godot +clean-resources: + rm -rf .godot + +# Remove game plugins +clean-addons: + rm -rf .plugged + [ -f plug.gd ] && find addons/ -type d -not -name 'addons' -not -name 'gd-plug' -exec rm -rf {} \; || true + +# Remove any unnecessary files +clean: clean-export clean-resources clean-addons + +# Add some variables to Github env +ci-load-dotenv: + echo "godot_version={{ godot_version }}" >> $GITHUB_ENV + echo "game_name={{ game_name }}" >> $GITHUB_ENV + echo "game_version={{ game_version }}" >> $GITHUB_ENV + +# Download Butler +[private] +install-butler: makedirs + #!/usr/bin/env sh + if [ ! -e {{ butler_bin }} ] + then + curl -L -X GET "https://broth.itch.ovh/butler/{{ butler_platform }}/LATEST/archive/default" --output {{ cache_dir }}/butler.zip + unzip {{ cache_dir }}/butler.zip -d {{ cache_dir }} + mv {{ cache_dir }}/butler {{ butler_bin }} + chmod +x {{ butler_bin }} + fi + +# Bulter wrapper +@butler *ARGS: install-butler + {{ butler_bin }} {{ ARGS }} + +# Upload the game on Github and Itch.io +publish: + gh release create "{{ game_version }}" --title="v{{ game_version }}" --generate-notes {{ dist_dir }}/* + just butler push {{ dist_dir }}/{{ game_name }}-windows-v{{ game_version }}.zip mechanical-flower/{{ game_itchio_key }}:windows --userversion {{ game_version }} + just butler push {{ dist_dir }}/{{ game_name }}-mac-v{{ game_version }}.zip mechanical-flower/{{ game_itchio_key }}:mac --userversion {{ game_version }} + just butler push {{ dist_dir }}/{{ game_name }}-linux-v{{ game_version }}.zip mechanical-flower/{{ game_itchio_key }}:linux --userversion {{ game_version }} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..8b19eaa --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +Copyright © 2022-present Florian Vazelle + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Footer diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..af33ae0 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ + +
+ +# 📝 Greeter + +![Godot Badge](https://img.shields.io/badge/godot-4.1-blue?logo=Godot-Engine&logoColor=white) +![license](https://img.shields.io/badge/license-MIT-green?logo=open-source-initiative&logoColor=white) +![reuse](./.reuse/REUSE-compliant.svg) + +A template to create new [Godot Engine](https://godotengine.org/) project. + +
+ +## Features + +- Clean separation of assets, resources, scenes, scripts and shaders code +- Continuous integration via [GitHub Actions](https://help.github.com/en/actions/) +- Code formatting enforced by [gdformat](https://github.com/Scony/godot-gdscript-toolkit) for gdscript code, and [clang-format](https://clang.llvm.org/docs/ClangFormat.html) for shaders, via [pre-commit](https://github.com/pre-commit/pre-commit) +- Keep track of licenses and attribution by following the [reuse specification](https://reuse.software/spec/) + + +## Usage + +### Adjust the template to your needs + +- Use this repo [as a template](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template). +- Replace all occurrences of "Greeter" with the name of your project +- Replace files with your own +- Happy coding! + +Eventually, you can remove any unused files, such as irrelevant github workflows for your project. +Feel free to replace the License with one suited for your project. + +## Contributing + +![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg) + +We welcome community contributions to this project. + +Please read our [Contributor Guide](CONTRIBUTING.md) for more information on how to get started. diff --git a/addons/gd-plug/plug.gd b/addons/gd-plug/plug.gd new file mode 100644 index 0000000..7ebf0ef --- /dev/null +++ b/addons/gd-plug/plug.gd @@ -0,0 +1,1015 @@ +@tool +extends SceneTree + +signal updated(plugin) + +const VERSION = "0.2.5" +const DEFAULT_PLUGIN_URL = "https://git::@github.com/%s.git" +const DEFAULT_PLUG_DIR = "res://.plugged" +const DEFAULT_CONFIG_PATH = DEFAULT_PLUG_DIR + "/index.cfg" +const DEFAULT_USER_PLUG_SCRIPT_PATH = "res://plug.gd" +const DEFAULT_BASE_PLUG_SCRIPT_PATH = "res://addons/gd-plug/plug.gd" + +const ENV_PRODUCTION = "production" +const ENV_TEST = "test" +const ENV_FORCE = "force" +const ENV_KEEP_IMPORT_FILE = "keep_import_file" +const ENV_KEEP_IMPORT_RESOURCE_FILE = "keep_import_resource_file" + +const MSG_PLUG_START_ASSERTION = "_plug_start() must be called first" + +var project_dir +var installation_config = ConfigFile.new() +var logger = _Logger.new() + +var _installed_plugins +var _plugged_plugins = {} + +var _threads = [] +var _mutex = Mutex.new() +var _start_time = 0 +var threadpool = _ThreadPool.new(logger) + + +func _init(): + threadpool.connect("all_thread_finished", request_quit) + project_dir = DirAccess.open("res://") + +func _initialize(): + var args = OS.get_cmdline_args() + # Trim unwanted args passed to godot executable + for arg in Array(args): + args.remove_at(0) + if "plug.gd" in arg: + break + + for arg in args: + # NOTE: "--key" or "-key" will always be consumed by godot executable, see https://github.com/godotengine/godot/issues/8721 + var key = arg.to_lower() + match key: + "detail": + logger.log_format = _Logger.DEFAULT_LOG_FORMAT_DETAIL + "debug", "d": + logger.log_level = _Logger.LogLevel.DEBUG + "quiet", "q", "silent": + logger.log_level = _Logger.LogLevel.NONE + "production": + OS.set_environment(ENV_PRODUCTION, "true") + "test": + OS.set_environment(ENV_TEST, "true") + "force": + OS.set_environment(ENV_FORCE, "true") + "keep-import-file": + OS.set_environment(ENV_KEEP_IMPORT_FILE, "true") + "keep-import-resource-file": + OS.set_environment(ENV_KEEP_IMPORT_RESOURCE_FILE, "true") + + logger.debug("cmdline_args: %s" % args) + _start_time = Time.get_ticks_msec() + _plug_start() + if args.size() > 0: + _plugging() + match args[0]: + "init": + _plug_init() + "install", "update": + _plug_install() + "uninstall": + _plug_uninstall() + "clean": + _plug_clean() + "upgrade": + _plug_upgrade() + "status": + _plug_status() + "version": + logger.info(VERSION) + _: + logger.error("Unknown command %s" % args[0]) + # NOTE: Do no put anything after this line except request_quit(), as _plug_*() may call request_quit() + request_quit() + +func _process(delta): + threadpool.process(delta) + +func _finalize(): + _plug_end() + threadpool.stop() + logger.info("Finished, elapsed %.3fs" % ((Time.get_ticks_msec() - _start_time) / 1000.0)) + +func _on_updated(plugin): + pass + +func _plugging(): + pass + +func request_quit(exit_code=-1): + if threadpool.is_all_thread_finished() and threadpool.is_all_task_finished(): + quit(exit_code) + return true + logger.debug("Request quit declined, threadpool is still running") + return false + +# Index installed plugins, or create directory "plugged" if not exists +func _plug_start(): + logger.debug("Plug start") + if not project_dir.dir_exists(DEFAULT_PLUG_DIR): + if project_dir.make_dir(ProjectSettings.globalize_path(DEFAULT_PLUG_DIR)) == OK: + logger.debug("Make dir %s for plugin installation") + if installation_config.load(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Installation config loaded") + else: + logger.debug("Installation config not found") + _installed_plugins = installation_config.get_value("plugin", "installed", {}) + +# Install plugin or uninstall plugin if unlisted +func _plug_end(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + var test = !OS.get_environment(ENV_TEST).is_empty() + if not test: + installation_config.set_value("plugin", "installed", _installed_plugins) + if installation_config.save(DEFAULT_CONFIG_PATH) == OK: + logger.debug("Plugged config saved") + else: + logger.error("Failed to save plugged config") + else: + logger.warn("Skipped saving of plugged config in test mode") + _installed_plugins = null + +func _plug_init(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + logger.info("Init gd-plug...") + if FileAccess.file_exists(DEFAULT_USER_PLUG_SCRIPT_PATH): + logger.warn("%s already exists!" % DEFAULT_USER_PLUG_SCRIPT_PATH) + else: + var file = FileAccess.open(DEFAULT_USER_PLUG_SCRIPT_PATH, FileAccess.WRITE) + file.store_string(INIT_PLUG_SCRIPT) + file.close() + logger.info("Created %s" % DEFAULT_USER_PLUG_SCRIPT_PATH) + +func _plug_install(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installing...") + for plugin in _plugged_plugins.values(): + var installed = plugin.name in _installed_plugins + if installed: + var installed_plugin = get_installed_plugin(plugin.name) + if (installed_plugin.dev or plugin.dev) and OS.get_environment(ENV_PRODUCTION): + logger.info("Remove dev plugin for production: %s" % plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin)) + else: + threadpool.enqueue_task(update_plugin.bind(plugin)) + else: + threadpool.enqueue_task(install_plugin.bind(plugin)) + + var removed_plugins = [] + for plugin in _installed_plugins.values(): + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + if removed_plugins: + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + for plugin in removed_plugins: + threadpool.enqueue_task(uninstall_plugin.bind(plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_uninstall(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Uninstalling...") + for plugin in _installed_plugins.values(): + var installed_plugin = get_installed_plugin(plugin.name) + threadpool.enqueue_task(uninstall_plugin.bind(installed_plugin), Thread.PRIORITY_LOW) + threadpool.active = true + +func _plug_clean(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Cleaning...") + var plugged_dir = DirAccess.open(DEFAULT_PLUG_DIR) + plugged_dir.include_hidden = true + plugged_dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plugged_dir.get_next() + while not file.is_empty(): + if plugged_dir.current_is_dir(): + if not (file in _installed_plugins): + logger.info("Remove %s" % file) + threadpool.enqueue_task(directory_delete_recursively.bind(plugged_dir.get_current_dir() + "/" + file)) + file = plugged_dir.get_next() + plugged_dir.list_dir_end() + threadpool.active = true + +func _plug_upgrade(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Upgrading gd-plug...") + plug("imjp94/gd-plug") + var gd_plug = _plugged_plugins["gd-plug"] + OS.set_environment(ENV_FORCE, "true") # Required to overwrite res://addons/gd-plug/plug.gd + threadpool.enqueue_task(install_plugin.bind(gd_plug)) + threadpool.disconnect("all_thread_finished", request_quit) + if not threadpool.is_all_thread_finished(): + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + logger.debug("All installation finished! Ready to uninstall removed plugins...") + threadpool.connect("all_thread_finished", request_quit) + threadpool.enqueue_task(directory_delete_recursively.bind(gd_plug.plug_dir)) + threadpool.active = true + +func _plug_status(): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + threadpool.active = false + logger.info("Installed %d plugin%s" % [_installed_plugins.size(), "s" if _installed_plugins.size() > 1 else ""]) + var new_plugins = _plugged_plugins.duplicate() + var has_checking_plugin = false + var removed_plugins = [] + for plugin in _installed_plugins.values(): + logger.info("- {name} - {url}".format(plugin)) + new_plugins.erase(plugin.name) + var removed = not (plugin.name in _plugged_plugins) + if removed: + removed_plugins.append(plugin) + else: + threadpool.enqueue_task(check_plugin.bind(_plugged_plugins[plugin.name])) + has_checking_plugin = true + if has_checking_plugin: + logger.info("\n", true) + threadpool.disconnect("all_thread_finished", request_quit) + threadpool.active = true + await threadpool.all_thread_finished + threadpool.active = false + threadpool.connect("all_thread_finished", request_quit) + logger.debug("Finished checking plugins, ready to proceed") + if new_plugins: + logger.info("\nPlugged %d plugin%s" % [new_plugins.size(), "s" if new_plugins.size() > 1 else ""]) + for plugin in new_plugins.values(): + var is_new = not (plugin.name in _installed_plugins) + if is_new: + logger.info("- {name} - {url}".format(plugin)) + if removed_plugins: + logger.info("\nUnplugged %d plugin%s" % [removed_plugins.size(), "s" if removed_plugins.size() > 1 else ""]) + for plugin in removed_plugins: + logger.info("- %s removed" % plugin.name) + var plug_directory = DirAccess.open(DEFAULT_PLUG_DIR) + var orphan_dirs = [] + if plug_directory.get_open_error() == OK: + plug_directory.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file = plug_directory.get_next() + while not file.is_empty(): + if plug_directory.current_is_dir(): + if not (file in _installed_plugins): + orphan_dirs.append(file) + file = plug_directory.get_next() + plug_directory.list_dir_end() + if orphan_dirs: + logger.info("\nOrphan directory, %d found in %s, execute \"clean\" command to remove" % [orphan_dirs.size(), DEFAULT_PLUG_DIR]) + for dir in orphan_dirs: + logger.info("- %s" % dir) + threadpool.active = true + + if has_checking_plugin: + request_quit() + +# Index & validate plugin +func plug(repo, args={}): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + repo = repo.strip_edges() + var plugin_name = get_plugin_name_from_repo(repo) + if plugin_name in _plugged_plugins: + logger.info("Plugin already plugged: %s" % plugin_name) + return + var plugin = {} + plugin.name = plugin_name + plugin.url = "" + if ":" in repo: + plugin.url = repo + elif repo.find("/") == repo.rfind("/"): + plugin.url = DEFAULT_PLUGIN_URL % repo + else: + logger.error("Invalid repo: %s" % repo) + plugin.plug_dir = DEFAULT_PLUG_DIR + "/" + plugin.name + + var is_valid = true + plugin.include = args.get("include", []) + is_valid = is_valid and validate_var_type(plugin, "include", TYPE_ARRAY, "Array") + plugin.exclude = args.get("exclude", []) + is_valid = is_valid and validate_var_type(plugin, "exclude", TYPE_ARRAY, "Array") + plugin.branch = args.get("branch", "") + is_valid = is_valid and validate_var_type(plugin, "branch", TYPE_STRING, "String") + plugin.tag = args.get("tag", "") + is_valid = is_valid and validate_var_type(plugin, "tag", TYPE_STRING, "String") + plugin.commit = args.get("commit", "") + is_valid = is_valid and validate_var_type(plugin, "commit", TYPE_STRING, "String") + if not plugin.commit.is_empty(): + var is_valid_commit = plugin.commit.length() == 40 + if not is_valid_commit: + logger.error("Expected full length 40 digits commit-hash string, given %s" % plugin.commit) + is_valid = is_valid and is_valid_commit + plugin.dev = args.get("dev", false) + is_valid = is_valid and validate_var_type(plugin, "dev", TYPE_BOOL, "Boolean") + plugin.on_updated = args.get("on_updated", "") + is_valid = is_valid and validate_var_type(plugin, "on_updated", TYPE_STRING, "String") + plugin.install_root = args.get("install_root", "") + is_valid = is_valid and validate_var_type(plugin, "install_root", TYPE_STRING, "String") + + if is_valid: + _plugged_plugins[plugin.name] = plugin + logger.debug("Plug: %s" % plugin) + else: + logger.error("Failed to plug %s, validation error" % plugin.name) + +func install_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var can_install = OS.get_environment(ENV_PRODUCTION).is_empty() if plugin.dev else true + if can_install: + logger.info("Installing plugin %s..." % plugin.name) + var result = is_plugin_downloaded(plugin) + if result != OK: + result = downlaod(plugin) + else: + logger.info("Plugin already downloaded") + + if result == OK: + install(plugin) + else: + logger.error("Failed to install plugin %s with error code %d" % [plugin.name, result]) + +func uninstall_plugin(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + logger.info("Uninstalling plugin %s..." % plugin.name) + uninstall(plugin) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + +func update_plugin(plugin, checking=false): + if not (plugin.name in _installed_plugins): + logger.info("%s new plugin" % plugin.name) + return true + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + var installed_plugin = get_installed_plugin(plugin.name) + var changes = compare_plugins(plugin, installed_plugin) + var should_clone = false + var should_pull = false + var should_reinstall = false + + if plugin.tag or plugin.commit: + for rev in ["tag", "commit"]: + var freeze_at = plugin[rev] + if freeze_at: + logger.info("%s frozen at %s \"%s\"" % [plugin.name, rev, freeze_at]) + break + else: + var ahead_behind = [] + if git.fetch("origin " + plugin.branch if plugin.branch else "origin").exit == OK: + ahead_behind = git.get_commit_comparison("HEAD", "origin/" + plugin.branch if plugin.branch else "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + if is_commit_behind: + logger.info("%s %d commits behind, update required" % [plugin.name, ahead_behind[1]]) + should_pull = true + else: + logger.info("%s up to date" % plugin.name) + + if changes: + logger.info("%s changed %s" % [plugin.name, changes]) + should_reinstall = true + if "url" in changes or "branch" in changes or "tag" in changes or "commit" in changes: + logger.info("%s repository setting changed, update required" % plugin.name) + should_clone = true + + if not checking: + if should_clone: + logger.info("%s cloning from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + uninstall(get_installed_plugin(plugin.name)) + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + if downlaod(plugin) == OK: + install(plugin) + elif should_pull: + logger.info("%s pulling updates from %s..." % [plugin.name, plugin.url]) + uninstall(get_installed_plugin(plugin.name)) + if git.pull().exit == OK: + install(plugin) + elif should_reinstall: + logger.info("%s reinstalling..." % plugin.name) + uninstall(get_installed_plugin(plugin.name)) + install(plugin) + +func check_plugin(plugin): + update_plugin(plugin, true) + +func downlaod(plugin): + logger.info("Downloading %s from %s..." % [plugin.name, plugin.url]) + var test = !OS.get_environment(ENV_TEST).is_empty() + var global_dest_dir = ProjectSettings.globalize_path(plugin.plug_dir) + if project_dir.dir_exists(plugin.plug_dir): + directory_delete_recursively(plugin.plug_dir) + project_dir.make_dir(plugin.plug_dir) + var result = _GitExecutable.new(global_dest_dir, logger).clone(plugin.url, global_dest_dir, {"branch": plugin.branch, "tag": plugin.tag, "commit": plugin.commit}) + if result.exit == OK: + logger.info("Successfully download %s" % [plugin.name]) + else: + logger.info("Failed to download %s" % plugin.name) + # Make sure plug_dir is clean when failed + directory_delete_recursively(plugin.plug_dir, {"exclude": [DEFAULT_CONFIG_PATH], "test": test}) + project_dir.remove(plugin.plug_dir) # Remove empty directory + return result.exit + +func install(plugin): + var include = plugin.get("include", []) + if include.is_empty(): # Auto include "addons/" folder if not explicitly specified + include = ["addons/"] + if OS.get_environment(ENV_FORCE).is_empty() and OS.get_environment(ENV_TEST).is_empty(): + var is_exists = false + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": true, "silent_test": true}) + for dest_file in dest_files: + if project_dir.file_exists(dest_file): + logger.warn("%s attempting to overwrite file %s" % [plugin.name, dest_file]) + is_exists = true + if is_exists: + logger.warn("Installation of %s terminated to avoid overwriting user files, you may disable safe mode with command \"force\"" % plugin.name) + return ERR_ALREADY_EXISTS + + logger.info("Installing files for %s..." % plugin.name) + var test = !OS.get_environment(ENV_TEST).is_empty() + var dest_files = directory_copy_recursively(plugin.plug_dir, "res://" + plugin.install_root, {"include": include, "exclude": plugin.exclude, "test": test}) + plugin.dest_files = dest_files + logger.info("Installed %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "", plugin.name]) + if plugin.name != "gd-plug": + set_installed_plugin(plugin) + if plugin.on_updated: + if has_method(plugin.on_updated): + logger.info("Execute post-update function for %s" % plugin.name) + _on_updated(plugin) + call(plugin.on_updated, plugin.duplicate()) + emit_signal("updated", plugin) + return OK + +func uninstall(plugin): + var test = !OS.get_environment(ENV_TEST).is_empty() + var keep_import_file = !OS.get_environment(ENV_KEEP_IMPORT_FILE).is_empty() + var keep_import_resource_file = !OS.get_environment(ENV_KEEP_IMPORT_RESOURCE_FILE).is_empty() + var dest_files = plugin.get("dest_files", []) + logger.info("Uninstalling %d file%s for %s..." % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + directory_remove_batch(dest_files, {"test": test, "keep_import_file": keep_import_file, "keep_import_resource_file": keep_import_resource_file}) + logger.info("Uninstalled %d file%s for %s" % [dest_files.size(), "s" if dest_files.size() > 1 else "",plugin.name]) + remove_installed_plugin(plugin.name) + +func is_plugin_downloaded(plugin): + if not project_dir.dir_exists(plugin.plug_dir + "/.git"): + return + + var git = _GitExecutable.new(ProjectSettings.globalize_path(plugin.plug_dir), logger) + return git.is_up_to_date(plugin) + +# Get installed plugin, thread safe +func get_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var installed_plugin = _installed_plugins[plugin_name] + _mutex.unlock() + return installed_plugin + +# Set installed plugin, thread safe +func set_installed_plugin(plugin): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + _installed_plugins[plugin.name] = plugin + _mutex.unlock() + +# Remove installed plugin, thread safe +func remove_installed_plugin(plugin_name): + assert(_installed_plugins != null, MSG_PLUG_START_ASSERTION) + _mutex.lock() + var result = _installed_plugins.erase(plugin_name) + _mutex.unlock() + return result + +func directory_copy_recursively(from, to, args={}): + var include = args.get("include", []) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(from) + dir.include_hidden = true + var dest_files = [] + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + var dest = to + ("/" if to != "res://" else "") + file_name + + if dir.current_is_dir(): + dest_files += directory_copy_recursively(source, dest, args) + else: + for include_key in include: + if include_key in source: + var is_excluded = false + for exclude_key in exclude: + if exclude_key in source: + is_excluded = true + break + if not is_excluded: + if test: + if not silent_test: logger.warn("[TEST] Writing to %s" % dest) + else: + dir.make_dir_recursive(to) + if dir.copy(source, dest) == OK: + logger.debug("Copy from %s to %s" % [source, dest]) + dest_files.append(dest) + break + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % from) + + return dest_files + +func directory_delete_recursively(dir_path, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var exclude = args.get("exclude", []) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dir = DirAccess.open(dir_path) + dir.include_hidden = true + if dir.get_open_error() == OK: + dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var file_name = dir.get_next() + while not file_name.is_empty(): + var source = dir.get_current_dir() + ("/" if dir.get_current_dir() != "res://" else "") + file_name + + if dir.current_is_dir(): + var sub_dir = directory_delete_recursively(source, args) + if remove_empty_directory: + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if source.get_file() == ".git": + var empty_dir_path = ProjectSettings.globalize_path(source) + var exit = FAILED + match OS.get_name(): + "Windows": + empty_dir_path = "\"%s\"" % empty_dir_path + empty_dir_path = empty_dir_path.replace("/", "\\") + var cmd = "rd /s /q %s" % empty_dir_path + exit = OS.execute("cmd", ["/C", cmd]) + "X11", "OSX", "Server": + empty_dir_path = "\'%s\'" % empty_dir_path + var cmd = "rm -rf %s" % empty_dir_path + exit = OS.execute("bash", ["-c", cmd]) + # Hacks to remove .git, as git pack files stop it from being removed + # See https://stackoverflow.com/questions/1213430/how-to-fully-delete-a-git-repository-created-with-init + if exit == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + logger.debug("Failed to remove empty directory: %s" % sub_dir.get_current_dir()) + else: + if dir.remove(sub_dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % sub_dir.get_current_dir()) + else: + var excluded = false + for exclude_key in exclude: + if source in exclude_key: + excluded = true + break + if not excluded: + if test: + if not silent_test: logger.warn("[TEST] Remove file: %s" % source) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: %s" % source) + file_name = dir.get_next() + dir.list_dir_end() + else: + logger.error("Failed to access path: %s" % dir_path) + + if remove_empty_directory: + dir.remove(dir.get_current_dir()) + + return dir + +func directory_remove_batch(files, args={}): + var remove_empty_directory = args.get("remove_empty_directory", true) + var keep_import_file = args.get("keep_import_file", false) + var keep_import_resource_file = args.get("keep_import_resource_file", false) + var test = args.get("test", false) + var silent_test = args.get("silent_test", false) + var dirs = {} + for file in files: + var file_dir = file.get_base_dir() + var file_name =file.get_file() + var dir = dirs.get(file_dir) + + if not dir: + dir = DirAccess.open(file_dir) + dirs[file_dir] = dir + + if file.ends_with(".import"): + if not keep_import_file: + _remove_import_file(dir, file, keep_import_resource_file, test, silent_test) + else: + if test: + if not silent_test: logger.warn("[TEST] Remove file: " + file) + else: + if dir.remove(file_name) == OK: + logger.debug("Remove file: " + file) + if not keep_import_file: + _remove_import_file(dir, file + ".import", keep_import_resource_file, test, silent_test) + + for dir in dirs.values(): + var slash_count = dir.get_current_dir().count("/") - 2 # Deduct 2 slash from "res://" + if test: + if not silent_test: logger.warn("[TEST] Remove empty directory: %s" % dir.get_current_dir()) + else: + if dir.remove(dir.get_current_dir()) == OK: + logger.debug("Remove empty directory: %s" % dir.get_current_dir()) + # Dumb method to clean empty ancestor directories + logger.debug("Removing emoty ancestor directory for %s..." % dir.get_current_dir()) + var current_dir = dir.get_current_dir() + for i in slash_count: + current_dir = current_dir.get_base_dir() + var d = DirAccess.open(current_dir) + if d.get_open_error() == OK: + if test: + if not silent_test: logger.warn("[TEST] Remove empty ancestor directory: %s" % d.get_current_dir()) + else: + if d.remove(d.get_current_dir()) == OK: + logger.debug("Remove empty ancestor directory: %s" % d.get_current_dir()) + +func _remove_import_file(dir, file, keep_import_resource_file=false, test=false, silent_test=false): + if not dir.file_exists(file): + return + + if not keep_import_resource_file: + var import_config = ConfigFile.new() + if import_config.load(file) == OK: + var metadata = import_config.get_value("remap", "metadata", {}) + var imported_formats = metadata.get("imported_formats", []) + if imported_formats: + for format in imported_formats: + _remove_import_resource_file(dir, import_config, "." + format, test) + else: + _remove_import_resource_file(dir, import_config, "", test) + if test: + if not silent_test: logger.warn("[TEST] Remove import file: " + file) + else: + if dir.remove(file) == OK: + logger.debug("Remove import file: " + file) + else: + # TODO: Sometimes Directory.remove() unable to remove random .import file and return error code 1(Generic Error) + # Maybe enforce the removal from shell? + logger.warn("Failed to remove import file: " + file) + +func _remove_import_resource_file(dir, import_config, import_format="", test=false): + var import_resource_file = import_config.get_value("remap", "path" + import_format, "") + var checksum_file = import_resource_file.trim_suffix("." + import_resource_file.get_extension()) + ".md5" if import_resource_file else "" + if import_resource_file: + if dir.file_exists(import_resource_file): + if test: + logger.info("[IMPORT] Remove import resource file: " + import_resource_file) + else: + if dir.remove(import_resource_file) == OK: + logger.debug("Remove import resource file: " + import_resource_file) + if checksum_file: + checksum_file = checksum_file.replace(import_format, "") + if dir.file_exists(checksum_file): + if test: + logger.info("[IMPORT] Remove import checksum file: " + checksum_file) + else: + if dir.remove(checksum_file) == OK: + logger.debug("Remove import checksum file: " + checksum_file) + +func compare_plugins(p1, p2): + var changed_keys = [] + for key in p1.keys(): + var v1 = p1[key] + var v2 = p2[key] + if v1 != v2: + changed_keys.append(key) + return changed_keys + +func get_plugin_name_from_repo(repo): + repo = repo.replace(".git", "").trim_suffix("/") + return repo.get_file() + +func validate_var_type(obj, var_name, type, type_string): + var value = obj.get(var_name) + var is_valid = typeof(value) == type + if not is_valid: + logger.error("Expected variable \"%s\" to be %s, given %s" % [var_name, type_string, value]) + return is_valid + +const INIT_PLUG_SCRIPT = \ +"""extends "res://addons/gd-plug/plug.gd" + +func _plugging(): + # Declare plugins with plug(repo, args) + # For example, clone from github repo("user/repo_name") + # plug("imjp94/gd-YAFSM") # By default, gd-plug will only install anything from "addons/" directory + # Or you can explicitly specify which file/directory to include + # plug("imjp94/gd-YAFSM", {"include": ["addons/"]}) # By default, gd-plug will only install anything from "addons/" directory + pass +""" + +class _GitExecutable extends RefCounted: + var cwd = "" + var logger + + func _init(p_cwd, p_logger): + cwd = p_cwd + logger = p_logger + + func _execute(command, output=[], read_stderr=false): + var cmd = "cd '%s' && %s" % [cwd, command] + # NOTE: OS.execute() seems to ignore read_stderr + var exit = FAILED + match OS.get_name(): + "Windows": + cmd = cmd.replace("\'", "\"") # cmd doesn't accept single-quotes + cmd = cmd if read_stderr else "%s 2> nul" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("cmd", ["/C", cmd], output, read_stderr) + "macOS", "Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD": + cmd if read_stderr else "%s 2>/dev/null" % cmd + logger.debug("Execute \"%s\"" % cmd) + exit = OS.execute("bash", ["-c", cmd], output, read_stderr) + var unhandled_os: + logger.error("Unexpected OS: %s" % unhandled_os) + logger.debug("Execution ended(code:%d): %s" % [exit, output]) + return exit + + func init(): + logger.debug("Initializing git at %s..." % cwd) + var output = [] + var exit = _execute("git init", output) + logger.debug("Successfully init" if exit == OK else "Failed to init") + return {"exit": exit, "output": output} + + func clone(src, dest, args={}): + logger.debug("Cloning from %s to %s..." % [src, dest]) + var output = [] + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + var command = "git clone --depth=1 --progress '%s' '%s'" % [src, dest] + if branch or tag: + command = "git clone --depth=1 --single-branch --branch %s '%s' '%s'" % [branch if branch else tag, src, dest] + elif commit: + return clone_commit(src, dest, commit) + var exit = _execute(command, output) + logger.debug("Successfully cloned from %s" % src if exit == OK else "Failed to clone from %s" % src) + return {"exit": exit, "output": output} + + func clone_commit(src, dest, commit): + var output = [] + if commit.length() < 40: + logger.error("Expected full length 40 digits commit-hash to clone specific commit, given {%s}" % commit) + return {"exit": FAILED, "output": output} + + logger.debug("Cloning from %s to %s @ %s..." % [src, dest, commit]) + var result = init() + if result.exit == OK: + result = remote_add("origin", src) + if result.exit == OK: + result = fetch("%s %s" % ["origin", commit]) + if result.exit == OK: + result = reset("--hard", "FETCH_HEAD") + return result + + func fetch(rm="--all"): + logger.debug("Fetching %s..." % rm.replace("--", "")) + var output = [] + var exit = _execute("git fetch %s" % rm, output) + logger.debug("Successfully fetched" if exit == OK else "Failed to fetch") + return {"exit": exit, "output": output} + + func pull(): + logger.debug("Pulling...") + var output = [] + var exit = _execute("git pull --rebase", output) + logger.debug("Successfully pulled" if exit == OK else "Failed to pull") + return {"exit": exit, "output": output} + + func remote_add(name, src): + logger.debug("Adding remote %s@%s..." % [name, src]) + var output = [] + var exit = _execute("git remote add %s '%s'" % [name, src], output) + logger.debug("Successfully added remote" if exit == OK else "Failed to add remote") + return {"exit": exit, "output": output} + + func reset(mode, to): + logger.debug("Resetting %s %s..." % [mode, to]) + var output = [] + var exit = _execute("git reset %s %s" % [mode, to], output) + logger.debug("Successfully reset" if exit == OK else "Failed to reset") + return {"exit": exit, "output": output} + + func get_commit_comparison(branch_a, branch_b): + var output = [] + var exit = _execute("git rev-list --count --left-right %s...%s" % [branch_a, branch_b], output) + var raw_ahead_behind = output[0].split("\t") + var ahead_behind = [] + for msg in raw_ahead_behind: + ahead_behind.append(msg.to_int()) + return ahead_behind if exit == OK else [] + + func get_current_branch(): + var output = [] + var exit = _execute("git rev-parse --abbrev-ref HEAD", output) + return output[0] if exit == OK else "" + + func get_current_tag(): + var output = [] + var exit = _execute("git describe --tags --exact-match", output) + return output[0] if exit == OK else "" + + func get_current_commit(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return output[0] if exit == OK else "" + + func is_detached_head(): + var output = [] + var exit = _execute("git rev-parse --short HEAD", output) + return (!!output[0]) if exit == OK else true + + func is_up_to_date(args={}): + if fetch().exit == OK: + var branch = args.get("branch", "") + var tag = args.get("tag", "") + var commit = args.get("commit", "") + + if branch: + if branch == get_current_branch(): + return FAILED if is_detached_head() else OK + elif tag: + if tag == get_current_tag(): + return OK + elif commit: + if commit == get_current_commit(): + return OK + + var ahead_behind = get_commit_comparison("HEAD", "origin") + var is_commit_behind = !!ahead_behind[1] if ahead_behind.size() == 2 else false + return FAILED if is_commit_behind else OK + return FAILED + +class _ThreadPool extends RefCounted: + signal all_thread_finished() + + var active = true + + var _threads = [] + var _finished_threads = [] + var _mutex = Mutex.new() + var _tasks = [] + var logger + + func _init(p_logger): + logger = p_logger + _threads.resize(OS.get_processor_count()) + + func _execute_task(task): + var thread = _get_thread() + var can_execute = thread + if can_execute: + task.thread = weakref(thread) + var callable = task.get("callable") + thread.start(_execute.bind(task), task.priority) + logger.debug("Execute task %s.%s() " % [callable.get_object(), callable.get_method()]) + return can_execute + + func _execute(args): + var callable = args.get("callable") + callable.call() + _mutex.lock() + var thread = args.thread.get_ref() + _threads[_threads.find(thread)] = null + _finished_threads.append(thread) + var all_finished = is_all_thread_finished() + _mutex.unlock() + + logger.debug("Execution finished %s.%s() " % [callable.get_object(), callable.get_method()]) + if all_finished: + logger.debug("All thread finished") + emit_signal("all_thread_finished") + + func _flush_tasks(): + if _tasks.size() == 0: + return + + var executed = true + while executed: + var task = _tasks.pop_front() + if task != null: + executed = _execute_task(task) + if not executed: + _tasks.push_front(task) + else: + executed = false + + func _flush_threads(): + for i in _finished_threads.size(): + var thread = _finished_threads.pop_front() + if not thread.is_alive(): + thread.wait_to_finish() + + func enqueue_task(callable, priority=1): + enqueue({"callable": callable, "priority": priority}) + + func enqueue(task): + var can_execute = false + if active: + can_execute = _execute_task(task) + if not can_execute: + _tasks.append(task) + + func process(delta): + if active: + _flush_tasks() + _flush_threads() + + func stop(): + _tasks.clear() + _flush_threads() + + func _get_thread(): + var thread + for i in OS.get_processor_count(): + var t = _threads[i] + if t: + if not t.is_started(): + thread = t + break + else: + thread = Thread.new() + _threads[i] = thread + break + return thread + + func is_all_thread_finished(): + for i in _threads.size(): + if _threads[i]: + return false + return true + + func is_all_task_finished(): + for i in _tasks.size(): + if _tasks[i]: + return false + return true + +class _Logger extends RefCounted: + enum LogLevel { + ALL, DEBUG, INFO, WARN, ERROR, NONE + } + const DEFAULT_LOG_FORMAT_DETAIL = "[{time}] [{level}] {msg}" + const DEFAULT_LOG_FORMAT_NORMAL = "{msg}" + + var log_level = LogLevel.INFO + var log_format = DEFAULT_LOG_FORMAT_NORMAL + var log_time_format = "{year}/{month}/{day} {hour}:{minute}:{second}" + + func debug(msg, raw=false): + _log(LogLevel.DEBUG, msg, raw) + + func info(msg, raw=false): + _log(LogLevel.INFO, msg, raw) + + func warn(msg, raw=false): + _log(LogLevel.WARN, msg, raw) + + func error(msg, raw=false): + _log(LogLevel.ERROR, msg, raw) + + func _log(level, msg, raw=false): + if log_level <= level: + if raw: + printraw(format_log(level, msg)) + else: + print(format_log(level, msg)) + + func format_log(level, msg): + return log_format.format({ + "time": log_time_format.format(get_formatted_datatime()), + "level": LogLevel.keys()[level], + "msg": msg + }) + + func get_formatted_datatime(): + var datetime = Time.get_datetime_dict_from_system() + datetime.year = "%04d" % datetime.year + datetime.month = "%02d" % datetime.month + datetime.day = "%02d" % datetime.day + datetime.hour = "%02d" % datetime.hour + datetime.minute = "%02d" % datetime.minute + datetime.second = "%02d" % datetime.second + return datetime diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..c98fbb6 Binary files /dev/null and b/assets/icon.png differ diff --git a/assets/icon.png.import b/assets/icon.png.import new file mode 100644 index 0000000..c36940f --- /dev/null +++ b/assets/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2lg07u76njan" +path="res://.godot/imported/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/icon.png" +dest_files=["res://.godot/imported/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +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=1 diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..9d1ab18 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,248 @@ +[preset.0] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=false +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +binary_format/architecture="x86_64" +codesign/enable=false +codesign/timestamp=true +codesign/timestamp_server_url="" +codesign/digest_algorithm=1 +codesign/description="" +codesign/custom_options=PackedStringArray() +application/modify_resources=true +application/icon="" +application/console_wrapper_icon="" +application/icon_interpolation=4 +application/file_version="0.1.0.20231103" +application/product_version="0.1.0.20231103" +application/company_name="Mechanical Flower" +application/product_name="Marble" +application/file_description="" +application/copyright="2023-present Mechanical Flower" +application/trademarks="" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' +$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' +$trigger = New-ScheduledTaskTrigger -Once -At 00:00 +$settings = New-ScheduledTaskSettingsSet +$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings +Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true +Start-ScheduledTask -TaskName godot_remote_debug +while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 } +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue" +ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue +Remove-Item -Recurse -Force '{temp_dir}'" +debug/export_console_script=1 + +[preset.1] + +name="Web" +platform="Web" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) +include_coi_service_worker=true +iframe_breakout="Disabled" + +[preset.2] + +name="macOS" +platform="macOS" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.2.options] + +export/distribution_type=1 +binary_format/architecture="universal" +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +application/icon="" +application/icon_interpolation=4 +application/bundle_identifier="io.itch.MechanicalFlower" +application/signature="" +application/app_category="Games" +application/short_version="0.1" +application/version="0.1.0" +application/copyright="2023-present Mechanical Flower" +application/copyright_localized={} +application/min_macos_version="10.12" +display/high_res=true +xcode/platform_build="14C18" +xcode/sdk_version="13.1" +xcode/sdk_build="22C55" +xcode/sdk_name="macosx13.1" +xcode/xcode_version="1420" +xcode/xcode_build="14C18" +codesign/codesign=1 +codesign/installer_identity="" +codesign/apple_team_id="" +codesign/identity="" +codesign/entitlements/custom_file="" +codesign/entitlements/allow_jit_code_execution=false +codesign/entitlements/allow_unsigned_executable_memory=false +codesign/entitlements/allow_dyld_environment_variables=false +codesign/entitlements/disable_library_validation=false +codesign/entitlements/audio_input=false +codesign/entitlements/camera=false +codesign/entitlements/location=false +codesign/entitlements/address_book=false +codesign/entitlements/calendars=false +codesign/entitlements/photos_library=false +codesign/entitlements/apple_events=false +codesign/entitlements/debugging=false +codesign/entitlements/app_sandbox/enabled=false +codesign/entitlements/app_sandbox/network_server=false +codesign/entitlements/app_sandbox/network_client=false +codesign/entitlements/app_sandbox/device_usb=false +codesign/entitlements/app_sandbox/device_bluetooth=false +codesign/entitlements/app_sandbox/files_downloads=0 +codesign/entitlements/app_sandbox/files_pictures=0 +codesign/entitlements/app_sandbox/files_music=0 +codesign/entitlements/app_sandbox/files_movies=0 +codesign/entitlements/app_sandbox/helper_executables=[] +codesign/custom_options=PackedStringArray() +notarization/notarization=0 +privacy/microphone_usage_description="" +privacy/microphone_usage_description_localized={} +privacy/camera_usage_description="" +privacy/camera_usage_description_localized={} +privacy/location_usage_description="" +privacy/location_usage_description_localized={} +privacy/address_book_usage_description="" +privacy/address_book_usage_description_localized={} +privacy/calendar_usage_description="" +privacy/calendar_usage_description_localized={} +privacy/photos_library_usage_description="" +privacy/photos_library_usage_description_localized={} +privacy/desktop_folder_usage_description="" +privacy/desktop_folder_usage_description_localized={} +privacy/documents_folder_usage_description="" +privacy/documents_folder_usage_description_localized={} +privacy/downloads_folder_usage_description="" +privacy/downloads_folder_usage_description_localized={} +privacy/network_volumes_usage_description="" +privacy/network_volumes_usage_description_localized={} +privacy/removable_volumes_usage_description="" +privacy/removable_volumes_usage_description_localized={} +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/usr/bin/env bash +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash +kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\") +rm -rf \"{temp_dir}\"" +debug/export_console_script=1 +notarization/apple_team_id="" + +[preset.3] + +name="Linux/X11" +platform="Linux/X11" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.3.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=false +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +binary_format/architecture="x86_64" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/usr/bin/env bash +export DISPLAY=:0 +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash +kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\") +rm -rf \"{temp_dir}\"" +debug/export_console_script=1 diff --git a/plug.gd b/plug.gd new file mode 100644 index 0000000..87c2841 --- /dev/null +++ b/plug.gd @@ -0,0 +1,10 @@ +extends "res://addons/gd-plug/plug.gd" + + +func _plugging(): + plug("godot-extended-libraries/godot-debug-menu", {"commit": "9d36ea23661d095198ff7fcfff2715172f73c983"}) + plug("KoBeWi/Godot-Universal-Fade", {"commit": "f091514bba652880f81c5bc8809e0ee4498988ea"}) + plug("nisovin/godot-coi-serviceworker", {"commit": "de1be2989eda4c7d77a08b8c56cd94c769181c4e"}) + + # Patched version + plug("florianvazelle/EasyMenus", {"commit": "3b8602985191f6a128808068d250b5b336d05379"}) diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..fc857c1 --- /dev/null +++ b/project.godot @@ -0,0 +1,38 @@ +; 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 + +[application] + +config/name="Greeter" +run/main_scene="res://scenes/menu.tscn" +config/features=PackedStringArray("4.1") +config/icon="res://assets/icon.png" + +[audio] + +buses/default_bus_layout="res://resources/default_bus_layout.tres" + +[autoload] + +GlobalSignal="*res://scripts/framework/global_signal.gd" +DebugMenu="*res://addons/debug_menu/debug_menu.tscn" +MenuTemplateManager="*res://addons/EasyMenus/Nodes/menu_template_manager.tscn" + +[editor_plugins] + +enabled=PackedStringArray("res://addons/debug_menu/plugin.cfg","res://addons/coi_serviceworker/plugin.cfg") + +[filesystem] + +import/blender/enabled=false + +[rendering] + +environment/default_environment="res://resources/default_env.tres" diff --git a/public/.gdignore b/public/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/public/.gitignore b/public/.gitignore new file mode 100644 index 0000000..e4fdbdb --- /dev/null +++ b/public/.gitignore @@ -0,0 +1 @@ +*.import diff --git a/public/publishing/store/github.webp b/public/publishing/store/github.webp new file mode 100644 index 0000000..43a9803 Binary files /dev/null and b/public/publishing/store/github.webp differ diff --git a/public/publishing/store/itchio.webp b/public/publishing/store/itchio.webp new file mode 100644 index 0000000..935d13a Binary files /dev/null and b/public/publishing/store/itchio.webp differ diff --git a/resources/default_bus_layout.tres b/resources/default_bus_layout.tres new file mode 100644 index 0000000..777e824 --- /dev/null +++ b/resources/default_bus_layout.tres @@ -0,0 +1,16 @@ +[gd_resource type="AudioBusLayout" format=3 uid="uid://cud5nfjh3hljh"] + +[resource] +bus/0/volume_db = 0.0672607 +bus/1/name = &"Music" +bus/1/solo = false +bus/1/mute = false +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = &"Master" +bus/2/name = &"SFX" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = 0.0 +bus/2/send = &"Master" diff --git a/resources/default_env.tres b/resources/default_env.tres new file mode 100644 index 0000000..20207a4 --- /dev/null +++ b/resources/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/resources/materials/circle_pixel_material.tres b/resources/materials/circle_pixel_material.tres new file mode 100644 index 0000000..2a4ce05 --- /dev/null +++ b/resources/materials/circle_pixel_material.tres @@ -0,0 +1,8 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://cdco3232rqf0y"] + +[ext_resource type="Shader" path="res://shaders/circle_pixel.gdshader" id="1_v1eqq"] + +[resource] +shader = ExtResource("1_v1eqq") +shader_parameter/amount_x = 128.0 +shader_parameter/amount_y = 128.0 diff --git a/scenes/main.tscn b/scenes/main.tscn new file mode 100644 index 0000000..2c2c97c --- /dev/null +++ b/scenes/main.tscn @@ -0,0 +1,30 @@ +[gd_scene load_steps=3 format=3 uid="uid://d11wogcjxnncv"] + +[ext_resource type="Script" path="res://scripts/greeter.gd" id="1"] +[ext_resource type="Material" uid="uid://cdco3232rqf0y" path="res://resources/materials/circle_pixel_material.tres" id="1_1rx2b"] + +[node name="Main" type="Node"] + +[node name="ViewportContainer" type="SubViewportContainer" parent="."] +material = ExtResource("1_1rx2b") +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +stretch = true + +[node name="Viewport" type="SubViewport" parent="ViewportContainer"] +transparent_bg = true +handle_input_locally = false +size = Vector2i(1152, 648) +render_target_update_mode = 4 + +[node name="Camera" type="Camera3D" parent="ViewportContainer/Viewport"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5) + +[node name="Greeter" type="Label3D" parent="ViewportContainer/Viewport"] +pixel_size = 0.05 +text = "Hello John!" +script = ExtResource("1") +player_name = "John" diff --git a/scenes/menu.tscn b/scenes/menu.tscn new file mode 100644 index 0000000..3120fbf --- /dev/null +++ b/scenes/menu.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=3 format=3 uid="uid://jw34guuhfr2h"] + +[ext_resource type="PackedScene" uid="uid://dq6tvhqcy2aps" path="res://addons/EasyMenus/Scenes/main_menu.tscn" id="1_hg7xw"] +[ext_resource type="Script" path="res://scripts/menu.gd" id="1_lk1h2"] + +[node name="Menu" type="Node"] +script = ExtResource("1_lk1h2") + +[node name="EasyMenu" parent="." instance=ExtResource("1_hg7xw")] +unique_name_in_owner = true diff --git a/scripts/framework/global_signal.gd b/scripts/framework/global_signal.gd new file mode 100644 index 0000000..d235deb --- /dev/null +++ b/scripts/framework/global_signal.gd @@ -0,0 +1,21 @@ +extends Node + +var _signals: Dictionary = {} + + +func add_listener(signal_name: StringName, callable: Callable): + if not _signals.has(signal_name): + _signals[signal_name] = [] + + _signals[signal_name].append(callable) + + +func trigger_signal(signal_name: StringName, arguments: Array = []): + if not _signals.has(signal_name): + printerr("Unknown signal: '" + signal_name + "'") + return + + for callable in _signals[signal_name]: + callable = callable as Callable # cast + if callable.is_valid(): + callable.callv(arguments) diff --git a/scripts/framework/helper.gd b/scripts/framework/helper.gd new file mode 100644 index 0000000..b1e554c --- /dev/null +++ b/scripts/framework/helper.gd @@ -0,0 +1,24 @@ +extends Node3D + +@export var fast_close := true + + +func _ready() -> void: + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + + if !OS.is_debug_build(): + fast_close = false + + set_process_input(fast_close) + + +func _input(event: InputEvent) -> void: + if event.is_action_pressed(&"ui_cancel"): + get_tree().quit() # Quits the game + + if event.is_action_pressed(&"change_mouse_input"): + match Input.get_mouse_mode(): + Input.MOUSE_MODE_CAPTURED: + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + Input.MOUSE_MODE_VISIBLE: + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) diff --git a/scripts/greeter.gd b/scripts/greeter.gd new file mode 100644 index 0000000..1247ec1 --- /dev/null +++ b/scripts/greeter.gd @@ -0,0 +1,14 @@ +@tool + +class_name Greeter + +extends Label3D + +@export var player_name: String + + +func _ready() -> void: + await get_tree().process_frame + await Fade.fade_in(1, Color.BLACK, "Diamond", false, false).finished + + set_text("Hello " + player_name + "!") diff --git a/scripts/menu.gd b/scripts/menu.gd new file mode 100644 index 0000000..2aa861a --- /dev/null +++ b/scripts/menu.gd @@ -0,0 +1,14 @@ +extends Node + +@onready var easy_menu: Control = get_node(^"%EasyMenu") +@onready var title: Label = get_node(^"%EasyMenu/Content/Title") + + +func _ready(): + title.set_text(&"Greeter") + easy_menu.connect(&"start_game_pressed", _on_Menu_start_game_pressed) + + +func _on_Menu_start_game_pressed(): + await Fade.fade_out(1, Color.BLACK, "Diamond", false, false).finished + get_tree().change_scene_to_file("res://scenes/main.tscn") diff --git a/shaders/circle_pixel.gdshader b/shaders/circle_pixel.gdshader new file mode 100644 index 0000000..25bcff9 --- /dev/null +++ b/shaders/circle_pixel.gdshader @@ -0,0 +1,21 @@ +shader_type canvas_item; + +uniform float amount_x : hint_range(0, 128) = 8; +uniform float amount_y : hint_range(0, 128) = 8; + +void fragment() { + vec2 pos = UV; + pos *= vec2(amount_x, amount_y); + pos = ceil(pos); + pos /= vec2(amount_x, amount_y); + vec2 cellpos = pos - (0.5 / vec2(amount_x, amount_y)); + + pos -= UV; + pos *= vec2(amount_x, amount_y); + pos = vec2(1.0) - pos; + + float dist = distance(pos, vec2(0.5)); + + vec4 c = texture(TEXTURE, cellpos); + COLOR = c * step(0.0, (0.5 * c.a) - dist); +}