Compare commits

..

No commits in common. "master" and "5.7.0" have entirely different histories.

1794 changed files with 191270 additions and 417132 deletions

33
.clang-format Normal file
View file

@ -0,0 +1,33 @@
BasedOnStyle: LLVM
IndentWidth: 4
UseTab: Always
TabWidth: 4
BreakBeforeBraces: Custom
Standard: Cpp11
BraceWrapping:
AfterClass: true
AfterControlStatement: false
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterStruct: true
AfterUnion: true
BeforeCatch: false
BeforeElse: false
FixNamespaceComments: false
AllowShortIfStatementsOnASingleLine: false
IndentCaseLabels: false
AccessModifierOffset: -4
ColumnLimit: 90
AllowShortFunctionsOnASingleLine: InlineOnly
SortIncludes: false
IncludeCategories:
- Regex: '^".*'
Priority: 2
- Regex: '^<.*'
Priority: 1
AlignAfterOpenBracket: DontAlign
ContinuationIndentWidth: 8
ConstructorInitializerIndentWidth: 8
BreakConstructorInitializers: AfterColon
AlwaysBreakTemplateDeclarations: Yes

View file

@ -1,4 +1,4 @@
Checks: '-*,modernize-use-emplace,modernize-avoid-bind,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,performance-*,-performance-avoid-endl,performance-inefficient-string-concatenation'
Checks: '-*,modernize-use-emplace,modernize-avoid-bind,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,performance-*'
WarningsAsErrors: '-*,modernize-use-emplace,performance-type-promotion-in-math-fn,performance-faster-string-find,performance-implicit-cast-in-loop'
CheckOptions:
- key: performance-unnecessary-value-param.AllowedTypes

11
.editorconfig Normal file → Executable file
View file

@ -1,16 +1,9 @@
[*]
end_of_line = lf
[*.{cpp,h,lua,txt,glsl,c,cmake,java,gradle}]
charset = utf-8
[*.{cpp,h,lua,txt,glsl,md,c,cmake,java,gradle}]
charset = utf8
indent_size = 4
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

6
.gitattributes vendored
View file

@ -1,8 +1,2 @@
# Forces all files which git considers text files to use LF line endings
* text=auto eol=lf
*.cpp diff=cpp
*.h diff=cpp
*.gltf binary
*.x binary

View file

@ -14,31 +14,28 @@ Contributions are welcome! Here's how you can help:
[clone](https://help.github.com/articles/cloning-a-repository/) your fork.
2. Before you start coding, consider opening an
[issue on Github](https://github.com/luanti-org/luanti/issues) to discuss the
[issue at Github](https://github.com/minetest/minetest/issues) to discuss the
suitability and implementation of your intended contribution with the core
developers.
Any Pull Request that isn't a bug fix and isn't covered by
[the roadmap](../doc/direction.md) will be closed within a month unless it
[the roadmap](../doc/direction.md) will be closed within a week unless it
receives a concept approval from a Core Developer. For this reason, it is
recommended that you open an issue for any such pull requests before doing
the work, to avoid disappointment.
You may also benefit from discussing on our IRC development channel
[#luanti-dev](http://www.luanti.org/irc/). Note that a proper IRC client
[#minetest-dev](http://www.minetest.net/irc/). Note that a proper IRC client
is required to speak on this channel.
3. Start coding!
- Refer to the
[Lua API](https://github.com/luanti-org/luanti/blob/master/doc/lua_api.md),
[Developer Wiki](https://dev.luanti.org/) and other
[documentation](https://github.com/luanti-org/luanti/tree/master/doc).
- Follow the [C/C++](https://dev.luanti.org/Code_style_guidelines) and
[Lua](https://dev.luanti.org/Lua_code_style_guidelines) code style guidelines.
[Lua API](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt),
[Developer Wiki](http://dev.minetest.net/Main_Page) and other
[documentation](https://github.com/minetest/minetest/tree/master/doc).
- Follow the [C/C++](http://dev.minetest.net/Code_style_guidelines) and
[Lua](http://dev.minetest.net/Lua_code_style_guidelines) code style guidelines.
- Check your code works as expected and document any changes to the Lua API.
- To avoid conflicting changes between contributions, do not do the following manually. They will be done before each release.
- Run `updatepo.sh` or update `luanti.po{,t}` even if your code adds new translatable strings.
- Update `minetest.conf.example` and `settings_translation_file.cpp` even if your code adds new core settings.
4. Commit & [push](https://help.github.com/articles/pushing-to-a-remote/) your changes to a new branch (not `master`, one change per branch)
- Commit messages should:
@ -53,7 +50,7 @@ Contributions are welcome! Here's how you can help:
- The following lines should describe the commit, starting a new line for each point.
5. Once you are happy with your changes, submit a pull request.
- Open the [pull-request form](https://github.com/luanti-org/luanti/pull/new/master).
- Open the [pull-request form](https://github.com/minetest/minetest/pull/new/master).
- Add a description explaining what you've done (or if it's a
work-in-progress - what you need to do).
- Make sure to fill out the pull request template.
@ -64,26 +61,40 @@ Contributions are welcome! Here's how you can help:
picture of the project.
2. It works.
3. It follows the code style for
[C/C++](https://dev.luanti.org/Code_style_guidelines) or
[Lua](https://dev.luanti.org/Lua_code_style_guidelines).
[C/C++](http://dev.minetest.net/Code_style_guidelines) or
[Lua](http://dev.minetest.net/Lua_code_style_guidelines).
4. The code's interfaces are well designed, regardless of other aspects that
might need more work in the future.
5. It uses protocols and formats which include the required compatibility.
### Important note about automated GitHub checks
When you submit a pull request, GitHub automatically runs checks on the Minetest
Engine combined with your changes. One of these checks is called 'cpp lint /
clang format', which checks code formatting. Because formatting for readability
requires human judgement this check often fails and often makes unsuitable
formatting requests which make code readability worse.
If this check fails, look at the details to check for any clear mistakes and
correct those. However, you should not apply everything ClangFormat requests.
Ignore requests that make code readability worse and any other clearly
unsuitable requests. Discuss in the pull request with a core developer about how
to progress.
## Issues
If you experience an issue, we would like to know the details - especially when
a stable release is on the way.
1. Do a quick search on GitHub to check if the issue has already been reported.
2. Is it an issue with the Luanti *engine*? If not, report it
[elsewhere](http://www.luanti.org/development/#reporting-issues).
3. [Open an issue](https://github.com/luanti-org/luanti/issues/new) and describe
2. Is it an issue with the Minetest *engine*? If not, report it
[elsewhere](http://www.minetest.net/development/#reporting-issues).
3. [Open an issue](https://github.com/minetest/minetest/issues/new) and describe
the issue you are having - you could include:
- Error logs (check the bottom of the `debug.txt` file).
- Screenshots.
- Ways you have tried to solve the issue, and whether they worked or not.
- Your Luanti version and the content (games, mods or texture packs) you have installed.
- Your Minetest version and the content (games, mods or texture packs) you have installed.
- Your platform (e.g. Windows 10 or Ubuntu 15.04 x64).
After reporting you should aim to answer questions or clarifications as this
@ -99,28 +110,28 @@ possible.
## Translations
The core translations of Luanti are performed using Weblate. You can access
The core translations of Minetest are performed using Weblate. You can access
the project page with a list of current languages
[here](https://hosted.weblate.org/projects/minetest/minetest/).
Builtin (the component which contains things like server messages, chat command
descriptions, privilege descriptions) is translated separately; it needs to be
translated by editing a `.tr` text file. See
[Translation](https://dev.luanti.org/Translation) for more information.
[Translation](https://dev.minetest.net/Translation) for more information.
## Donations
If you'd like to monetarily support Luanti development, you can find donation
methods on [our website](http://www.luanti.org/development/#donate).
If you'd like to monetarily support Minetest development, you can find donation
methods on [our website](http://www.minetest.net/development/#donate).
# Maintaining
* This is a concise version of the
[Rules & Guidelines](https://dev.luanti.org/engine-dev-process/) on the developer wiki.*
[Rules & Guidelines](http://dev.minetest.net/Category:Rules_and_Guidelines) on the developer wiki.*
These notes are for those who have push access Luanti (core developers / maintainers).
These notes are for those who have push access Minetest (core developers / maintainers).
- See the [project organisation](https://dev.luanti.org/Organisation) for the people involved.
- See the [project organisation](http://dev.minetest.net/Organisation) for the people involved.
## Concept approvals and roadmaps
@ -159,14 +170,14 @@ Submit a :+1: (+1) or "Looks good" comment to show you believe the pull-request
- The title should follow the commit guidelines (title starts with a capital letter, present tense, descriptive).
- Don't modify history older than 10 minutes.
- Use rebase, not merge to get linear history:
- `curl -Ls https://github.com/luanti-org/luanti/pull/1.patch | git am`
- `curl https://github.com/minetest/minetest/pull/1.patch | git am`
## Reviewing issues and feature requests
- If an issue does not get a response from its author within 1 month (when requiring more details), it can be closed.
- When an issue is a duplicate, refer to the first ones and close the later ones.
- Tag issues with the appropriate [labels](https://github.com/luanti-org/luanti/labels) for devices, platforms etc.
- Tag issues with the appropriate [labels](https://github.com/minetest/minetest/labels) for devices, platforms etc.
## Releasing a new version
*Refer to [dev.luanti.org/Releasing_Luanti](https://dev.luanti.org/Releasing_Luanti)*
*Refer to [dev.minetest.net/Releasing_Minetest](http://dev.minetest.net/Releasing_Minetest)*

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: Unconfirmed bug
assignees: ''
---
##### Minetest version
<!--
Paste Minetest version between quotes below.
If you are on a devel version, please add git commit hash.
You can use `minetest --version` to find it.
-->
```
```
##### OS / Hardware
<!-- General information about your hardware and operating system -->
Operating system:
CPU:
<!-- For graphical issues only -->
GPU model:
OpenGL version:
##### Summary
<!-- Describe your problem here -->
##### Steps to reproduce
<!-- Explain how the problem has happened, providing a minimal test (i.e. a code snippet reduced to the bone) where possible -->

View file

@ -1,79 +0,0 @@
name: Bug report
description: Create a report to help us improve
labels: ["Unconfirmed bug"]
body:
- type: markdown
attributes:
value: |
Please note the following:
1. **Please update Luanti to the latest stable or dev version** before submitting bug reports. Make sure the bug is still reproducible on the latest version.
2. This page is for reporting the bugs of **the engine itself**. For bugs in a particular game, please [search for the game in the ContentDB](https://content.luanti.org/packages/?type=game) and submit a bug report in their issue trackers.
* For example, you can submit issues about the Minetest Game [in its own repository](https://github.com/luanti-org/minetest_game/issues).
3. Please provide as many details as possible for us to spot the problem quicker.
- type: textarea
attributes:
label: Luanti version
description: |
Paste the Luanti version below.
If you are on a dev version, please also indicate the git commit hash.
Refer to the "About" tab of the menu or run `luanti --version` on the command line.
placeholder: |
Example:
Luanti 5.10.0-3ad6aee9b (Linux)
Using LuaJIT 2.1.1727870382
Built by GCC 14.2
Running on Linux/6.11.5 x86_64
BUILD_TYPE=Release
RUN_IN_PLACE=1
USE_CURL=1
USE_GETTEXT=1
USE_SOUND=1
STATIC_SHAREDIR="."
STATIC_LOCALEDIR="locale"
render: "true"
validations:
required: true
- type: input
attributes:
label: Operating system and version
description: It is recommended to upgrade your operating system to see if the problem persists.
placeholder: "Example: Ubuntu 22.04"
validations:
required: true
- type: input
attributes:
label: CPU model
description: Usually found in OS/system settings.
placeholder: "Example: Intel Core i5-2410M"
validations:
required: false
- type: markdown
attributes:
value: The GPU model and renderer can be omitted if the bug is not a graphical issue.
- type: input
attributes:
label: GPU model
description: Usually found in OS/system settings.
placeholder: "Example: NVIDIA GeForce GTX 1660"
validations:
required: false
- type: input
attributes:
label: Active renderer
description: You can find this in the "About" tab in the main menu.
placeholder: "Example: ES 3.2 / ogles2 / X11"
validations:
required: false
- type: textarea
attributes:
label: Summary
description: Describe your problem here.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Explain how the problem has happened, providing a minimal test (e.g. a minimized code snippet) where possible.
validations:
required: true

View file

@ -1,8 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Submit issues about Minetest Game
url: https://github.com/luanti-org/minetest_game/issues/new
about: Only submit issues of the engine in this repository's issue tracker. Submit those of Minetest Game in its own issue tracker.
- name: Search for issue trackers of third-party games
url: https://content.luanti.org/packages/?type=game
about: For issues of third-party games, search for the game in the ContentDB and then submit an issue in their issue tracker.

View file

@ -0,0 +1,25 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: Feature request
assignees: ''
---
## Problem
A clear and concise description of what the problem is.
ie: Why is this needed?
Ex. I'm always frustrated when [...]
## Solutions
A clear and concise description of what you want to happen.
## Alternatives
A clear and concise description of any alternative solutions or features you've considered.
## Additional context
Add any other context or screenshots about the feature request here.

View file

@ -1,39 +0,0 @@
name: Feature request
description: Suggest an idea for this project
labels: ["Feature request"]
body:
- type: markdown
attributes:
value: |
Please note the following:
1. Only submit a feature request if the feature does not exist on the latest dev version.
2. This page is for suggesting changes to **the engine itself**. To suggest changes to games, please [search for the game in the ContentDB](https://content.luanti.org/packages/?type=game) and submit a feature request in their issue trackers.
- type: textarea
attributes:
label: Problem
description: |
A clear and concise description of the problem, i.e. "Why is this needed?"
Example: I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Solutions
description: |
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Alternatives
description: |
A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: |
Add any other context or screenshots about the feature request here.
validations:
required: false

View file

@ -3,7 +3,7 @@ Add compact, short information about your PR for easier understanding:
- Goal of the PR
- How does the PR work?
- Does it resolve any reported issue?
- Does this relate to a goal in [the roadmap](https://github.com/luanti-org/luanti/blob/master/doc/direction.md)?
- Does this relate to a goal in [the roadmap](../doc/direction.md)?
- If not a bug fix, why is this PR needed? What usecases does it solve?
## To do

6
.github/SECURITY.md vendored
View file

@ -3,7 +3,7 @@
## Supported Versions
We only support the latest stable version for security issues.
See the [releases page](https://github.com/luanti-org/luanti/releases).
See the [releases page](https://github.com/minetest/minetest/releases).
## Reporting a Vulnerability
@ -11,10 +11,10 @@ We ask that you report vulnerabilities privately, by contacting a core developer
to give us time to fix them. You can do that by emailing one of the following addresses:
* celeron55@gmail.com
* rw@rubenwardy.com
* rubenwardy@minetest.net
Depending on severity, we will either create a private issue for the vulnerability
and release a patch version of Luanti, or give you permission to file the issue publicly.
and release a patch version of Minetest, or give you permission to file the issue publicly.
For more information on the justification of this policy, see
[Responsible Disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure).

View file

@ -8,11 +8,6 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- 'android/**'
- '.github/workflows/android.yml'
pull_request:
@ -21,56 +16,37 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- 'android/**'
- '.github/workflows/android.yml'
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends gettext
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Build AAB with Gradle
# We build an AAB as well for uploading to the the Play Store.
run: cd android; ./gradlew bundlerelease
- name: Build APKs with Gradle
# "assemblerelease" is very fast after "bundlerelease".
sudo apt-get install -y --no-install-recommends gettext openjdk-11-jdk-headless
- name: Build with Gradle
run: cd android; ./gradlew assemblerelease
- name: Save AAB artifact
uses: actions/upload-artifact@v4
with:
name: Luanti-release.aab
path: android/app/build/outputs/bundle/release/app-release.aab
- name: Save armeabi artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: Luanti-armeabi-v7a.apk
name: Minetest-armeabi-v7a.apk
path: android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk
- name: Save arm64 artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: Luanti-arm64-v8a.apk
name: Minetest-arm64-v8a.apk
path: android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
- name: Save x86 artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: Luanti-x86.apk
name: Minetest-x86.apk
path: android/app/build/outputs/apk/release/app-x86-release-unsigned.apk
- name: Save x86_64 artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: Luanti-x86_64.apk
name: Minetest-x86_64.apk
path: android/app/build/outputs/apk/release/app-x86_64-release-unsigned.apk

276
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,276 @@
name: build
# build on c/cpp changes or workflow changes
on:
push:
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/buildbot/**'
- 'util/ci/**'
- '.github/workflows/**.yml'
- 'Dockerfile'
- '.dockerignore'
pull_request:
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/buildbot/**'
- 'util/ci/**'
- '.github/workflows/**.yml'
- 'Dockerfile'
- '.dockerignore'
env:
MINETEST_POSTGRESQL_CONNECT_STRING: 'host=localhost user=minetest password=minetest dbname=minetest'
jobs:
# Older gcc version (should be close to our minimum supported version)
gcc_5:
runs-on: ubuntu-18.04
if: "false" # FIXME
steps:
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps g++-5
- name: Build
run: |
./util/ci/build.sh
env:
CC: gcc-5
CXX: g++-5
- name: Test
run: |
./bin/minetest --run-unittests
# Current gcc version
gcc_12:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps g++-12 libluajit-5.1-dev
- name: Build
run: |
./util/ci/build.sh
env:
CC: gcc-12
CXX: g++-12
- name: Test
run: |
./bin/minetest --run-unittests
# Older clang version (should be close to our minimum supported version)
clang_3_9:
runs-on: ubuntu-18.04
if: "false" # FIXME
steps:
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-3.9 valgrind
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang-3.9
CXX: clang++-3.9
- name: Unittest
run: |
./bin/minetest --run-unittests
- name: Valgrind
run: |
valgrind --leak-check=full --leak-check-heuristics=all --undef-value-errors=no --error-exitcode=9 ./bin/minetest --run-unittests
# Current clang version
clang_14:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-14 gdb
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang-14
CXX: clang++-14
- name: Test
run: |
./bin/minetest --run-unittests
- name: Integration test + devtest
run: |
./util/test_multiplayer.sh
# Build with prometheus-cpp (server-only)
clang_9_prometheus:
name: "clang_9 (PROMETHEUS=1)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-9
- name: Build prometheus-cpp
run: |
./util/ci/build_prometheus_cpp.sh
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang-9
CXX: clang++-9
CMAKE_FLAGS: "-DENABLE_PROMETHEUS=1 -DBUILD_CLIENT=0"
- name: Test
run: |
./bin/minetestserver --run-unittests
docker:
name: "Docker image"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Build docker image
run: |
docker build . -t minetest:latest
docker run --rm minetest:latest /usr/local/bin/minetestserver --version
win32:
name: "MinGW cross-compiler (32-bit)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Install compiler
run: |
sudo apt-get update && sudo apt-get install -y gettext
wget http://minetest.kitsunemimi.pw/mingw-w64-i686_11.2.0_ubuntu20.04.tar.xz -O mingw.tar.xz
sudo tar -xaf mingw.tar.xz -C /usr
- name: Build
run: |
EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin32.sh winbuild
env:
NO_MINETEST_GAME: 1
NO_PACKAGE: 1
win64:
name: "MinGW cross-compiler (64-bit)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Install compiler
run: |
sudo apt-get update && sudo apt-get install -y gettext
wget http://minetest.kitsunemimi.pw/mingw-w64-x86_64_11.2.0_ubuntu20.04.tar.xz -O mingw.tar.xz
sudo tar -xaf mingw.tar.xz -C /usr
- name: Build
run: |
EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin64.sh winbuild
env:
NO_MINETEST_GAME: 1
NO_PACKAGE: 1
msvc:
name: VS 2019 ${{ matrix.config.arch }}-${{ matrix.type }}
runs-on: windows-2019
env:
VCPKG_VERSION: 5cf60186a241e84e8232641ee973395d4fde90e1
# 2022.02
vcpkg_packages: zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp opengl-registry
strategy:
fail-fast: false
matrix:
config:
- {
arch: x86,
generator: "-G'Visual Studio 16 2019' -A Win32",
vcpkg_triplet: x86-windows
}
- {
arch: x64,
generator: "-G'Visual Studio 16 2019' -A x64",
vcpkg_triplet: x64-windows
}
type: [portable]
# type: [portable, installer]
# The installer type is working, but disabled, to save runner jobs.
# Enable it, when working on the installer.
steps:
- uses: actions/checkout@v3
- name: Checkout IrrlichtMt
run: |
$ref = @(Get-Content misc\irrlichtmt_tag.txt)
git clone https://github.com/minetest/irrlicht lib\irrlichtmt --depth 1 -b $ref[0]
- name: Restore from cache and run vcpkg
uses: lukka/run-vcpkg@v7
with:
vcpkgArguments: ${{env.vcpkg_packages}}
vcpkgDirectory: '${{ github.workspace }}\vcpkg'
appendedCacheKey: ${{ matrix.config.vcpkg_triplet }}
vcpkgGitCommitId: ${{ env.VCPKG_VERSION }}
vcpkgTriplet: ${{ matrix.config.vcpkg_triplet }}
- name: Minetest CMake
run: |
cmake ${{matrix.config.generator}} `
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" `
-DCMAKE_BUILD_TYPE=Release `
-DENABLE_POSTGRESQL=OFF `
-DRUN_IN_PLACE=${{ contains(matrix.type, 'portable') }} .
- name: Build Minetest
run: cmake --build . --config Release
- name: CPack
run: |
If ($env:TYPE -eq "installer")
{
cpack -G WIX -B "$env:GITHUB_WORKSPACE\Package"
}
ElseIf($env:TYPE -eq "portable")
{
cpack -G ZIP -B "$env:GITHUB_WORKSPACE\Package"
}
env:
TYPE: ${{matrix.type}}
- name: Package Clean
run: rm -r $env:GITHUB_WORKSPACE\Package\_CPack_Packages
- uses: actions/upload-artifact@v3
with:
name: msvc-${{ matrix.config.arch }}-${{ matrix.type }}
path: .\Package\

View file

@ -8,8 +8,6 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/ci/**'
@ -20,25 +18,37 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/ci/**'
- '.github/workflows/**.yml'
env:
CLANG_TIDY: clang-tidy-15
jobs:
# clang_format:
# runs-on: ubuntu-20.04
# steps:
# - uses: actions/checkout@v3
# - name: Install clang-format
# run: |
# sudo apt-get update
# sudo apt-get install -y clang-format-9
#
# - name: Run clang-format
# run: |
# source ./util/ci/clang-format.sh
# check_format
# env:
# CLANG_FORMAT: clang-format-9
clang_tidy:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps $CLANG_TIDY
install_linux_deps clang-tidy-9
- name: Run clang-tidy
run: |

View file

@ -1,98 +0,0 @@
---
name: docker_image
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images
# https://docs.docker.com/build/ci/github-actions/multi-platform
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
on:
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ "*" ]
pull_request:
# Build docker image on pull requests. (but do not publish)
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/ci/**'
- 'misc/irrlichtmt_tag.txt'
- 'Dockerfile'
- '.dockerignore'
- '.github/workflows/docker_image.yml'
workflow_dispatch:
inputs:
use_cache:
description: "Use build cache"
required: true
type: boolean
default: true
env:
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3.0.0
# Login against the Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.5.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
org.opencontainers.image.title=Luanti
org.opencontainers.image.vendor=Luanti
org.opencontainers.image.licenses=LGPL-2.1-only
# Build and push Docker image
# https://github.com/docker/build-push-action
# No arm support for now. Require cross-compilation support in Dockerfile to not use QEMU.
- name: Build and push Docker image
uses: docker/build-push-action@v5.1.0
with:
context: .
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }}
- name: Test Docker Image
run: |
docker run --rm $(cut -d, -f1 <<<"$DOCKER_METADATA_OUTPUT_TAGS") luantiserver --version
shell: bash

View file

@ -1,159 +0,0 @@
name: linux
# build on c/cpp changes or workflow changes
on:
push:
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- 'util/ci/**'
- '.github/workflows/linux.yml'
pull_request:
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- 'util/ci/**'
- '.github/workflows/linux.yml'
env:
MINETEST_POSTGRESQL_CONNECT_STRING: 'host=localhost user=minetest password=minetest dbname=minetest'
jobs:
# Older gcc version (should be close to our minimum supported version)
gcc_7:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps g++-7
- name: Build
run: |
./util/ci/build.sh
env:
CC: gcc-7
CXX: g++-7
# Test fallback SHA implementations
CMAKE_FLAGS: '-DENABLE_OPENSSL=0'
- name: Test
run: |
./bin/luanti --run-unittests
# Current gcc version
gcc_14:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps g++-14 libluajit-5.1-dev
- name: Build
run: |
./util/ci/build.sh
env:
CC: gcc-14
CXX: g++-14
- name: Test
run: |
mkdir nowrite
chmod a-w nowrite
cd nowrite
../bin/luanti --run-unittests
# Older clang version (should be close to our minimum supported version)
clang_7:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-7 llvm-7
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang-7
CXX: clang++-7
CMAKE_FLAGS: '-DCMAKE_C_FLAGS="-fsanitize=address" -DCMAKE_CXX_FLAGS="-fsanitize=address"'
- name: Unittest
run: |
./bin/luanti --run-unittests
# Do this here because we have ASan and error paths are sensitive to dangling pointers
- name: Test error cases
run: |
./util/test_error_cases.sh
# Current clang version
clang_18:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-18 lldb
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang-18
CXX: clang++-18
- name: Test
run: |
./bin/luanti --run-unittests
- name: Integration test + devtest
run: |
./util/test_multiplayer.sh
# Build with prometheus-cpp (server-only)
clang_11_prometheus:
name: "clang_11 (PROMETHEUS=1)"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-11
- name: Build prometheus-cpp
run: ./util/ci/build_prometheus_cpp.sh
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang-11
CXX: clang++-11
CMAKE_FLAGS: "-DENABLE_PROMETHEUS=1 -DBUILD_CLIENT=0 -DENABLE_CURSES=0"
- name: Test
run: |
./bin/luantiserver --run-unittests

View file

@ -14,43 +14,47 @@ on:
- '.github/workflows/**.yml'
jobs:
# Note that the integration tests are also run in build.yml, but only when C++ code is changed.
# Note that the integration tests are also run build.yml, but only when C++ code is changed.
integration_tests:
name: "Compile and run multiplayer tests"
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang gdb libluajit-5.1-dev
install_linux_deps clang-10 gdb libluajit-5.1-dev
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang
CXX: clang++
CMAKE_FLAGS: "-DENABLE_GETTEXT=0 -DBUILD_SERVER=0 -DBUILD_UNITTESTS=0"
CC: clang-10
CXX: clang++-10
CMAKE_FLAGS: "-DENABLE_GETTEXT=0 -DBUILD_SERVER=0"
- name: Integration test + devtest
run: |
serverconf="profiler.load=true" ./util/test_multiplayer.sh
./util/test_multiplayer.sh
luacheck:
name: "Builtin Luacheck and Unit Tests"
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: leafo/gh-actions-lua@v10
- uses: actions/checkout@v3
- uses: leafo/gh-actions-lua@v9
with:
luaVersion: "5.1.5"
- uses: leafo/gh-actions-luarocks@v4.3.0
- uses: leafo/gh-actions-luarocks@v4
- name: Install LuaJIT
run: ./util/ci/build_luajit.sh
run: |
cd $HOME
git clone https://github.com/LuaJIT/LuaJIT/
cd LuaJIT
make -j$(nproc)
- name: Install luarocks tools
run: |

View file

@ -1,48 +0,0 @@
name: lua_api_deploy
permissions:
contents: read
pages: write
id-token: write
on:
push:
paths:
- '.github/workflows/lua_api_deploy.yml'
- 'doc/lua_api.md'
- 'doc/mkdocs/'
branches:
- master
jobs:
build:
if: github.repository == 'luanti-org/luanti'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install mkdocs
run: |
pip install -U -r doc/mkdocs/requirements.txt
- name: Build documentation
run: |
cd doc/mkdocs/
./build.sh
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'public/'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -8,12 +8,8 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- 'irr/**.mm' # Objective-C(++)
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- '.github/workflows/macos.yml'
pull_request:
paths:
@ -21,20 +17,20 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- 'irr/**.mm' # Objective-C(++)
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- '.github/workflows/macos.yml'
env:
MINETEST_GAME_REPO: https://github.com/minetest/minetest_game.git
MINETEST_GAME_BRANCH: master
MINETEST_GAME_NAME: minetest_game
jobs:
build-intel-macos:
# use lowest possible macOS running on x86_64 supported by brew to support more users
runs-on: macos-13
build:
runs-on: macos-11
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Install deps
run: |
source ./util/ci/common.sh
@ -42,6 +38,8 @@ jobs:
- name: Build
run: |
git clone -b $MINETEST_GAME_BRANCH $MINETEST_GAME_REPO games/$MINETEST_GAME_NAME
git clone https://github.com/minetest/irrlicht lib/irrlichtmt --depth 1 -b $(cat misc/irrlichtmt_tag.txt)
mkdir build
cd build
cmake .. \
@ -50,64 +48,21 @@ jobs:
-DCMAKE_INSTALL_PREFIX=../build/macos/ \
-DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE \
-DINSTALL_DEVTEST=TRUE
cmake --build . -j$(sysctl -n hw.logicalcpu)
make -j2
make install
- name: Test
run: |
./build/macos/luanti.app/Contents/MacOS/luanti --run-unittests
./build/macos/minetest.app/Contents/MacOS/minetest --run-unittests
# Zipping the built .app preserves permissions on the contained files,
# which the GitHub artifact pipeline would otherwise strip away.
- name: CPack
run: |
cd build
rm -rf macos
cmake .. -DINSTALL_DEVTEST=FALSE
cpack -G ZIP -B macos
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: luanti-macos
name: minetest-macos
path: ./build/macos/*.zip
build-arm-macos-xcode:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source ./util/ci/common.sh
install_macos_deps
# brew jsoncpp do not include libjsoncpp.a, and if installed header conflict caused build failure
brew uninstall jsoncpp
- name: Build with Cmake
run: |
mkdir build
cd build
cmake .. \
-DCMAKE_OSX_DEPLOYMENT_TARGET=14 \
-DCMAKE_FIND_FRAMEWORK=LAST \
-DCMAKE_INSTALL_PREFIX=../build/macos/ \
-DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE \
-DENABLE_SYSTEM_JSONCPP=OFF
cmake --build . -j$(sysctl -n hw.logicalcpu)
make install
- name: Build and Archive with Xcode
run: |
mkdir build_xcode
cd build_xcode
../util/ci/build_xcode.sh
- name: Tests
run: |
mkdir -p "${HOME}/Library/Application Support/minetest/games/"
ln -s "${PWD}/games/devtest" "${HOME}/Library/Application Support/minetest/games/"
./build/macos/luanti.app/Contents/MacOS/luanti --run-unittests
./build_xcode/luanti.xcarchive/Products/Applications/luanti.app/Contents/MacOS/luanti --run-unittests
- name: Diff Resources
run: |
diff -rd ./build/macos/luanti.app/Contents/Resources ./build_xcode/build/Release/luanti.app/Contents/Resources || exit 1
diff -rd ./build/macos/luanti.app/Contents/Resources ./build_xcode/luanti.xcarchive/Products/Applications/luanti.app/Contents/Resources || exit 1

View file

@ -1,26 +0,0 @@
name: png_file_checks
# Check whether all png files are in a valid format
on:
push:
paths:
- '**.png'
- '.github/workflows/**.yml'
pull_request:
paths:
- '**.png'
- '.github/workflows/**.yml'
jobs:
png_optimized:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
sudo apt-get update
sudo apt install -y optipng
- name: Check whether all png files are optimized
run: |
./util/ci/check_png_optimized.sh

View file

@ -1,118 +0,0 @@
name: whitespace_checks
# Check whitespaces of the following file types
# Not checked: .lua, .yml, .properties, .conf, .java, .py, .svg, .gradle, .xml, ...
# (luacheck already checks .lua files)
on:
push:
paths:
- '**.txt'
- '**.md'
- '**.[ch]'
- '**.cpp'
- '**.hpp'
- '**.sh'
- '**.cmake'
- '**.glsl'
- '**.lua'
pull_request:
paths:
- '**.txt'
- '**.md'
- '**.[ch]'
- '**.cpp'
- '**.hpp'
- '**.sh'
- '**.cmake'
- '**.glsl'
- '**.lua'
jobs:
trailing_whitespaces:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Line endings are already ensured by .gitattributes
- name: Check trailing whitespaces
run: |
if git ls-files |\
grep -E '\.txt$|\.md$|\.[ch]$|\.cpp$|\.hpp$|\.sh$|\.cmake$|\.glsl$' |\
xargs grep -n '\s$';\
then\
echo -e "\033[0;31mFound trailing whitespace";\
(exit 1);\
else\
(exit 0);\
fi
indent_spaces:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Line endings are already ensured by .gitattributes
# Multiple multline comments in one line is not supported by this check
# and there is no reason to use them
# So lines like: "/* */ /*" or "*/ a = 5; /*" will result in error
- name: Check for unsupported multiline comments
run: |
if git ls-files |\
grep -E '^src/.*\.cpp$|^src/.*\.[ch]$' |\
xargs grep -n '\*/.*/\*';\
then
echo -e "\033[0;31mUnsupported combination of multiline comments. New multiline comment should begin on new line.";\
(exit 1);\
else\
(exit 0);\
fi
if git ls-files |\
grep -E '\.lua$' |\
xargs grep -n -e '\]\].*--\[\[';\
then
echo -e "\033[0;31mUnsupported combination of multiline comments. New multiline comment should begin on new line.";\
(exit 1);\
else\
(exit 0);\
fi
# This prepare files for final check
# See python script ./util/ci/indent_tab_preprocess.py for details.
- name: Preprocess files
run: |
git ls-files |\
grep -E '^src/.*\.cpp$|^src/.*\.[ch]$' |\
xargs -L 1 -P $(($(nproc) + 1)) \
python3 ./util/ci/indent_tab_preprocess.py "/*" "*/"
git ls-files |\
grep -E '\.lua$' |\
xargs -L 1 -P $(($(nproc) + 1)) \
python3 ./util/ci/indent_tab_preprocess.py "--[[" "]]"
# Check for bad indent.
# This runs over preprocessed files.
# If there is any remaining space on line beginning or after tab,
# error is generated
- name: Check indent spaces
run: |
if git ls-files |\
grep -E '^src/.*\.cpp$|^src/.*\.[ch]$|\.lua' |\
xargs grep -n -P '^\t*[ ]';\
then\
echo -e "\033[0;31mFound incorrect indent whitespaces";\
(exit 1);\
else\
(exit 0);\
fi
tabs_lua_api_files:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Some files should not contain tabs
- name: Check tabs in Lua API files
run: |
if grep -n $'\t' doc/lua_api.md doc/client_lua_api.md;\
then\
echo -e "\033[0;31mFound tab in markdown file";\
(exit 1);\
else\
(exit 0);\
fi

View file

@ -1,146 +0,0 @@
name: windows
# build on c/cpp changes or workflow changes
on:
push:
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- 'util/buildbot/**'
- 'misc/*.manifest'
- '.github/workflows/windows.yml'
pull_request:
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'po/**.po'
- 'util/buildbot/**'
- 'misc/*.manifest'
- '.github/workflows/windows.yml'
jobs:
mingw:
name: "MinGW cross-compiler (${{ matrix.bits }}-bit)"
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
bits: [32, 64]
steps:
- uses: actions/checkout@v4
- name: Install compiler
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y --no-install-recommends gettext wine wine${{ matrix.bits }}
sudo ./util/buildbot/download_toolchain.sh /usr
- name: Build
run: |
EXISTING_MINETEST_DIR=$PWD \
./util/buildbot/buildwin${{ matrix.bits }}.sh B
# Check that the resulting binary can run (DLLs etc.)
- name: Runtime test
run: |
dest=$(mktemp -d)
unzip -q -d "$dest" B/build/*.zip
cd "$dest"/luanti-*-win*
wine bin/luanti.exe --version
- uses: actions/upload-artifact@v4
with:
name: "mingw${{ matrix.bits }}"
path: B/build/*.zip
if-no-files-found: error
msvc:
name: VS 2019 ${{ matrix.config.arch }}-${{ matrix.type }}
runs-on: windows-2019
env:
VCPKG_VERSION: 01f602195983451bc83e72f4214af2cbc495aa94
# 2024.05.24
vcpkg_packages: zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp sdl2
strategy:
fail-fast: false
matrix:
config:
- {
arch: x86,
generator: "-G'Visual Studio 16 2019' -A Win32",
vcpkg_triplet: x86-windows
}
- {
arch: x64,
generator: "-G'Visual Studio 16 2019' -A x64",
vcpkg_triplet: x64-windows
}
type: [portable]
# type: [portable, installer]
# The installer type is working, but disabled, to save runner jobs.
# Enable it, when working on the installer.
steps:
- uses: actions/checkout@v4
- name: Restore from cache and run vcpkg
uses: lukka/run-vcpkg@v7
with:
vcpkgArguments: ${{env.vcpkg_packages}}
vcpkgDirectory: '${{ github.workspace }}\vcpkg'
appendedCacheKey: ${{ matrix.config.vcpkg_triplet }}
vcpkgGitCommitId: ${{ env.VCPKG_VERSION }}
vcpkgTriplet: ${{ matrix.config.vcpkg_triplet }}
- name: CMake
run: |
cmake ${{matrix.config.generator}} `
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" `
-DCMAKE_BUILD_TYPE=Release `
-DENABLE_POSTGRESQL=OFF `
-DENABLE_LUAJIT=TRUE `
-DREQUIRE_LUAJIT=TRUE `
-DRUN_IN_PLACE=${{ contains(matrix.type, 'portable') }} .
- name: Build
run: cmake --build . --config Release
- name: Unittests
# need this workaround for stdout to work
run: |
$proc = start .\bin\Release\luanti.exe --run-unittests -NoNewWindow -Wait -PassThru
exit $proc.ExitCode
continue-on-error: true # FIXME!!
- name: CPack
run: |
If ($env:TYPE -eq "installer")
{
cpack -G WIX -B "$env:GITHUB_WORKSPACE\Package"
}
ElseIf($env:TYPE -eq "portable")
{
cpack -G ZIP -B "$env:GITHUB_WORKSPACE\Package"
}
rm -r $env:GITHUB_WORKSPACE\Package\_CPack_Packages
env:
TYPE: ${{matrix.type}}
- uses: actions/upload-artifact@v4
with:
name: msvc-${{ matrix.config.arch }}-${{ matrix.type }}
path: .\Package\
if-no-files-found: error

35
.gitignore vendored
View file

@ -1,8 +1,5 @@
## Editors and development environments
*~
.cmake
CMakeUserPresets.json
Testing/*
*.swp
*.bak*
*.orig
@ -26,36 +23,23 @@ tags
!tags/
gtags.files
.idea
.qtcreator/
# Codelite
*.project
# Visual Studio Code & plugins
.vscode/*
!.vscode/extensions.json
.vscode/
build/.cmake/
# Fleet
.fleet
# Gradle
.gradle
# Clang
.cache
# AppImage
*.AppImage
*.zsync
appimage-build
AppDir
# Direnv
.direnv/
# Nix
/result
## Files related to Luanti development cycle
## Files related to Minetest development cycle
/*.patch
*.diff
# GNU Patch reject file
*.rej
## Non-static Luanti directories or symlinks to these
## Non-static Minetest directories or symlinks to these
/bin/
/games/*
!/games/devtest/
@ -73,14 +57,13 @@ AppDir
/clientmods/*
!/clientmods/preview/
/client/mod_storage/
/mod_data
## Configuration/log files
minetest.conf
debug.txt
debug.txt.1
## Other files generated by Luanti
## Other files generated by Minetest
screenshot_*.png
testbm.txt
@ -102,26 +85,24 @@ cmake_install.cmake
CMakeCache.txt
CPackConfig.cmake
CPackSourceConfig.cmake
src/test_config.h
src/cmake_config.h
src/cmake_config_githash.h
src/unittest/test_world/world.mt
games/devtest/mods/testnodes/textures/testnodes_generated_*.png
/locale/
.directory
*.cbp
*.layout
*.o
*.a
*.dump
*.dmp
*.ninja
.ninja*
*.gch
*.iml
test_config.h
cmake-build-debug/
cmake-build-minsizerel/
cmake-build-release/
cmake-build-relwithdebinfo/
cmake-build-default/
cmake_config.h
cmake_config_githash.h
CMakeDoxy*
@ -133,7 +114,7 @@ compile_commands.json
*.sln
.vs/
# Old irrlichtmt. Still ignored to make bisecting easier.
# Optional user provided library folder
lib/irrlichtmt
# Generated mod storage database

View file

@ -3,14 +3,131 @@
# https://gitlab.com/minetest/minetest
# Pipelines URL: https://gitlab.com/minetest/minetest/pipelines
stages:
- build
- package
- deploy
variables:
MINETEST_GAME_REPO: "https://github.com/minetest/minetest_game.git"
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
.build_template:
stage: build
before_script:
- apt-get update
- DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential gettext git cmake libpng-dev libjpeg-dev libxi-dev libgl1-mesa-dev libsqlite3-dev libleveldb-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev libzstd-dev libluajit-5.1-dev
script:
- git clone https://github.com/minetest/irrlicht lib/irrlichtmt --depth 1 -b $(cat misc/irrlichtmt_tag.txt)
- mkdir build && cd build
- cmake -DCMAKE_INSTALL_PREFIX=../artifact/minetest/usr/ -DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE ..
- make -j $(($(nproc) + 1))
- make install
artifacts:
when: on_success
expire_in: 1h
paths:
- artifact/*
##
## Ubuntu (prerequisite for AppImage build)
##
build:ubuntu-20.04:
extends: .build_template
image: ubuntu:focal
##
## MinGW for Windows
##
.generic_win_template:
image: ubuntu:focal
before_script:
- apt-get update
- DEBIAN_FRONTEND=noninteractive apt-get install -y wget xz-utils unzip git cmake gettext
- wget -nv http://minetest.kitsunemimi.pw/mingw-w64-${WIN_ARCH}_11.2.0_ubuntu20.04.tar.xz -O mingw.tar.xz
- tar -xaf mingw.tar.xz -C /usr
.build_win_template:
extends: .generic_win_template
stage: build
artifacts:
expire_in: 90 day
paths:
- minetest-*-win*/*
build:win32:
extends: .build_win_template
script:
- EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin32.sh build
- unzip -q build/build/*.zip
variables:
WIN_ARCH: "i686"
build:win64:
extends: .build_win_template
script:
- EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin64.sh build
- unzip -q build/build/*.zip
variables:
WIN_ARCH: "x86_64"
##
## Docker
##
package:docker:
stage: package
image: docker:stable
services:
- docker:dind
before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
script:
- ./util/ci/docker.sh
##
## Gitlab Pages (Lua API documentation)
##
pages:
stage: deploy
image: python:3.8
before_script:
- pip install -U -r doc/mkdocs/requirements.txt
script:
- ./misc/make_redirects.sh
- cd doc/mkdocs && ./build.sh
artifacts:
paths:
- public
only:
- master
##
## AppImage
##
package:appimage-client:
stage: package
image: appimagecrafters/appimage-builder
needs:
- build:ubuntu-20.04
before_script:
- apt-get update
- apt-get install -y git
# Collect files
- mkdir AppDir
- cp -a artifact/minetest/usr/ AppDir/usr/
- cp -a clientmods AppDir/usr/share/minetest
- git clone $MINETEST_GAME_REPO AppDir/usr/share/minetest/games/minetest_game
- rm -rf AppDir/usr/share/minetest/games/minetest_game/.git
# Remove PrefersNonDefaultGPU property due to validation errors
- sed -i '/PrefersNonDefaultGPU/d' AppDir/usr/share/applications/net.minetest.minetest.desktop
script:
- export VERSION=$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA
- appimage-builder --skip-test --recipe misc/AppImageBuilder.yml
artifacts:
expire_in: 90 day
paths:
- ./*.AppImage

View file

@ -10,19 +10,16 @@ ignore = {
read_globals = {
"ItemStack",
"INIT",
"PLATFORM",
"DIR_DELIM",
"dump", "dump2",
"fgettext", "fgettext_ne",
"vector",
"VoxelArea",
"VoxelManip",
"profiler",
"Settings",
"PerlinNoise", "PerlinNoiseMap",
string = {fields = {"split", "trim"}},
table = {fields = {"copy", "getn", "indexof", "keyof", "insert_all"}},
table = {fields = {"copy", "getn", "indexof", "insert_all"}},
math = {fields = {"hypot", "round"}},
}
@ -39,12 +36,6 @@ files["builtin/client/register.lua"] = {
}
}
files["builtin/common/math.lua"] = {
globals = {
"math",
},
}
files["builtin/common/misc_helpers.lua"] = {
globals = {
"dump", "dump2", "table", "math", "string",
@ -54,7 +45,7 @@ files["builtin/common/misc_helpers.lua"] = {
}
files["builtin/common/vector.lua"] = {
globals = { "vector", "math" },
globals = { "vector" },
}
files["builtin/game/voxelarea.lua"] = {
@ -76,6 +67,10 @@ files["builtin/mainmenu"] = {
globals = {
"gamedata",
},
read_globals = {
"PLATFORM",
},
}
files["builtin/common/tests"] = {

View file

@ -72,4 +72,3 @@ Lars Müller <appgurulars@gmx.de>
Lars Müller <appgurulars@gmx.de> <34514239+appgurueu@users.noreply.github.com>
ROllerozxa <rollerozxa@voxelmanip.se>
ROllerozxa <rollerozxa@voxelmanip.se> <temporaryemail4meh+github@gmail.com>
Ælla Chiana Moskopp <erle@dieweltistgarnichtso.net> <nils@dieweltistgarnichtso.net>

View file

@ -1,5 +0,0 @@
{
"recommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

View file

@ -1,22 +1,29 @@
cmake_minimum_required(VERSION 3.12)
cmake_minimum_required(VERSION 3.5)
# Set policies up to 3.9 since we want to enable the IPO option
if(${CMAKE_VERSION} VERSION_LESS 3.9)
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
else()
cmake_policy(VERSION 3.9)
endif()
# This can be read from ${PROJECT_NAME} after project() is called
project(luanti)
set(PROJECT_NAME_CAPITALIZED "Luanti")
project(minetest)
set(PROJECT_NAME_CAPITALIZED "Minetest")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
set(GCC_MINIMUM_VERSION "7.5")
set(CLANG_MINIMUM_VERSION "7.0.1")
set(GCC_MINIMUM_VERSION "5.1")
set(CLANG_MINIMUM_VERSION "3.5")
# You should not need to edit these manually, use util/bump_version.sh
set(VERSION_MAJOR 5)
set(VERSION_MINOR 12)
set(VERSION_MINOR 7)
set(VERSION_PATCH 0)
set(VERSION_EXTRA "" CACHE STRING "Stuff to append to version string")
# Change to false for releases
set(DEVELOPMENT_BUILD TRUE)
set(DEVELOPMENT_BUILD FALSE)
set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
if(VERSION_EXTRA)
@ -30,37 +37,10 @@ if (CMAKE_BUILD_TYPE STREQUAL Debug)
set(VERSION_STRING "${VERSION_STRING}-debug")
endif()
message(STATUS "*** Will build version ${VERSION_STRING} ***")
# Configuration options
set(BUILD_CLIENT TRUE CACHE BOOL "Build client")
set(BUILD_SERVER FALSE CACHE BOOL "Build server")
set(BUILD_UNITTESTS TRUE CACHE BOOL "Build unittests")
set(BUILD_BENCHMARKS FALSE CACHE BOOL "Build benchmarks")
set(BUILD_DOCUMENTATION TRUE CACHE BOOL "Build documentation")
set(DEFAULT_ENABLE_LTO TRUE)
# by default don't enable on Debug builds to get faster builds
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DEFAULT_ENABLE_LTO FALSE)
endif()
#### LTO testing list ####
# - Linux: seems to work always
# - win32/msvc: works
# - win32/gcc: fails to link
# - win32/clang: works
# - macOS on x86: seems to be fine
# - macOS on ARM: crashes, see <https://github.com/luanti-org/luanti/issues/14397>
# Note: since CMake has no easy architecture detection disabling for Mac entirely
#### ####
if((WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR APPLE)
set(DEFAULT_ENABLE_LTO FALSE)
endif()
set(ENABLE_LTO ${DEFAULT_ENABLE_LTO} CACHE BOOL "Use Link Time Optimization")
set(BUILD_WITH_TRACY FALSE CACHE BOOL
"Fetch and build with the Tracy profiler client")
set(FETCH_TRACY_GIT_TAG "master" CACHE STRING
"Git tag for fetching Tracy client. Match with your server (gui) version")
set(DEFAULT_RUN_IN_PLACE FALSE)
if(WIN32)
set(DEFAULT_RUN_IN_PLACE TRUE)
@ -68,13 +48,11 @@ endif()
set(RUN_IN_PLACE ${DEFAULT_RUN_IN_PLACE} CACHE BOOL
"Run directly in source directory structure")
message(STATUS "*** Will build version ${VERSION_STRING} ***")
message(STATUS "BUILD_CLIENT: " ${BUILD_CLIENT})
message(STATUS "BUILD_SERVER: " ${BUILD_SERVER})
message(STATUS "BUILD_UNITTESTS: " ${BUILD_UNITTESTS})
message(STATUS "BUILD_BENCHMARKS: " ${BUILD_BENCHMARKS})
message(STATUS "BUILD_DOCUMENTATION: " ${BUILD_DOCUMENTATION})
message(STATUS "RUN_IN_PLACE: " ${RUN_IN_PLACE})
set(BUILD_CLIENT TRUE CACHE BOOL "Build client")
set(BUILD_SERVER FALSE CACHE BOOL "Build server")
set(BUILD_UNITTESTS TRUE CACHE BOOL "Build unittests")
set(BUILD_BENCHMARKS FALSE CACHE BOOL "Build benchmarks")
set(WARN_ALL TRUE CACHE BOOL "Enable -Wall for Release build")
@ -89,17 +67,27 @@ set(ENABLE_UPDATE_CHECKER (NOT ${DEVELOPMENT_BUILD}) CACHE BOOL
# Included stuff
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/")
# Load default options for Android
if(ANDROID)
cmake_minimum_required(VERSION 3.20)
include(AndroidLibs)
endif()
set(IRRLICHTMT_BUILD_DIR "" CACHE PATH "Path to IrrlichtMt build directory.")
if(NOT "${IRRLICHTMT_BUILD_DIR}" STREQUAL "")
find_package(IrrlichtMt QUIET
PATHS "${IRRLICHTMT_BUILD_DIR}"
NO_DEFAULT_PATH
)
if(TRUE)
message(STATUS "Using imported IrrlichtMt at subdirectory 'irr'")
if(NOT TARGET IrrlichtMt::IrrlichtMt)
# find_package() searches certain subdirectories. ${PATH}/cmake is not
# the only one, but it is the one where IrrlichtMt is supposed to export
# IrrlichtMtConfig.cmake
message(FATAL_ERROR "Could not find IrrlichtMtConfig.cmake in ${IRRLICHTMT_BUILD_DIR}/cmake.")
endif()
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/irrlichtmt")
message(STATUS "Using user-provided IrrlichtMt at subdirectory 'lib/irrlichtmt'")
if(BUILD_CLIENT)
add_subdirectory(irr EXCLUDE_FROM_ALL)
# tell IrrlichtMt to create a static library
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared library" FORCE)
add_subdirectory(lib/irrlichtmt EXCLUDE_FROM_ALL)
unset(BUILD_SHARED_LIBS CACHE)
if(NOT TARGET IrrlichtMt)
message(FATAL_ERROR "IrrlichtMt project is missing a CMake target?!")
@ -107,22 +95,45 @@ if(TRUE)
else()
add_library(IrrlichtMt::IrrlichtMt INTERFACE IMPORTED)
set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/irr/include")
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/lib/irrlichtmt/include")
endif()
else()
find_package(IrrlichtMt QUIET)
if(NOT TARGET IrrlichtMt::IrrlichtMt)
string(CONCAT explanation_msg
"The Minetest team has forked Irrlicht to make their own customizations. "
"It can be found here: https://github.com/minetest/irrlicht\n"
"For example use: git clone --depth=1 https://github.com/minetest/irrlicht lib/irrlichtmt\n")
if(BUILD_CLIENT)
message(FATAL_ERROR "IrrlichtMt is required to build the client, but it was not found.\n${explanation_msg}")
endif()
include(MinetestFindIrrlichtHeaders)
if(NOT IRRLICHT_INCLUDE_DIR)
message(FATAL_ERROR "IrrlichtMt headers are required to build the server, but none found.\n${explanation_msg}")
endif()
message(STATUS "Found IrrlichtMt headers: ${IRRLICHT_INCLUDE_DIR}")
add_library(IrrlichtMt::IrrlichtMt INTERFACE IMPORTED)
# Note that we can't use target_include_directories() since that doesn't work for IMPORTED targets before CMake 3.11
set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${IRRLICHT_INCLUDE_DIR}")
endif()
endif()
if (ENABLE_LTO OR CMAKE_INTERPROCEDURAL_OPTIMIZATION)
include(CheckIPOSupported)
check_ipo_supported(RESULT lto_supported OUTPUT lto_output)
if(lto_supported)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
message(STATUS "LTO/IPO is enabled")
else()
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION FALSE)
message(STATUS "LTO/IPO was requested but is not supported by the compiler: ${lto_output}")
if(BUILD_CLIENT AND TARGET IrrlichtMt::IrrlichtMt)
# retrieve version somehow
if(NOT IrrlichtMt_VERSION)
get_target_property(IrrlichtMt_VERSION IrrlichtMt VERSION)
endif()
message(STATUS "Found IrrlichtMt ${IrrlichtMt_VERSION}")
set(TARGET_VER_S 1.9.0mt10)
string(REPLACE "mt" "." TARGET_VER ${TARGET_VER_S})
if(IrrlichtMt_VERSION VERSION_LESS ${TARGET_VER})
message(FATAL_ERROR "At least IrrlichtMt ${TARGET_VER_S} is required to build")
elseif(NOT DEVELOPMENT_BUILD AND IrrlichtMt_VERSION VERSION_GREATER ${TARGET_VER})
message(FATAL_ERROR "IrrlichtMt ${TARGET_VER_S} is required to build")
endif()
else()
message(STATUS "LTO/IPO is not enabled")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
@ -161,7 +172,7 @@ elseif(UNIX) # Linux, BSD etc
set(EXAMPLE_CONF_DIR ".")
set(MANDIR "unix/man")
set(XDG_APPS_DIR "unix/applications")
set(METAINFODIR "unix/metainfo")
set(APPDATADIR "unix/metainfo")
set(ICONDIR "unix/icons")
set(LOCALEDIR "locale")
else()
@ -172,7 +183,7 @@ elseif(UNIX) # Linux, BSD etc
set(MANDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}")
set(EXAMPLE_CONF_DIR ${DOCDIR})
set(XDG_APPS_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/applications")
set(METAINFODIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/metainfo")
set(APPDATADIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/metainfo")
set(ICONDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/icons")
set(LOCALEDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LOCALEDIR}")
endif()
@ -236,6 +247,9 @@ if(RUN_IN_PLACE)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/textures/texture_packs_here.txt" DESTINATION "${SHAREDIR}/textures")
endif()
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/games/minetest_game" DESTINATION "${SHAREDIR}/games/"
COMPONENT "SUBGAME_MINETEST_GAME" OPTIONAL PATTERN ".git*" EXCLUDE )
set(INSTALL_DEVTEST FALSE CACHE BOOL "Install Development Test")
if(INSTALL_DEVTEST)
@ -253,47 +267,40 @@ if(BUILD_CLIENT)
endif()
install(FILES "README.md" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/lua_api.md" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/client_lua_api.md" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/menu_lua_api.md" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/texture_packs.md" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/world_format.md" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/lua_api.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/client_lua_api.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/menu_lua_api.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/texture_packs.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "doc/world_format.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs")
install(FILES "minetest.conf.example" DESTINATION "${EXAMPLE_CONF_DIR}")
if(UNIX AND NOT APPLE)
install(FILES "doc/luanti.6" "doc/luantiserver.6" DESTINATION "${MANDIR}/man6")
install(FILES "doc/minetest.6" "doc/minetestserver.6" DESTINATION "${MANDIR}/man6")
install(FILES "misc/net.minetest.minetest.desktop" DESTINATION "${XDG_APPS_DIR}")
install(FILES "misc/net.minetest.minetest.metainfo.xml" DESTINATION "${METAINFODIR}")
install(FILES "misc/luanti.svg" DESTINATION "${ICONDIR}/hicolor/scalable/apps")
install(FILES "misc/luanti-xorg-icon-128.png"
install(FILES "misc/net.minetest.minetest.appdata.xml" DESTINATION "${APPDATADIR}")
install(FILES "misc/minetest.svg" DESTINATION "${ICONDIR}/hicolor/scalable/apps")
install(FILES "misc/minetest-xorg-icon-128.png"
DESTINATION "${ICONDIR}/hicolor/128x128/apps"
RENAME "luanti.png")
RENAME "minetest.png")
endif()
if(APPLE)
install(FILES "misc/luanti-icon.icns" DESTINATION "${SHAREDIR}")
install(FILES "${CMAKE_BINARY_DIR}/Info.plist" DESTINATION "${BUNDLE_PATH}/Contents")
endif()
if(CMAKE_GENERATOR STREQUAL "Xcode")
set(client_RESOURCES "${CMAKE_SOURCE_DIR}/misc/luanti-icon.icns")
install(FILES "misc/minetest-icon.icns" DESTINATION "${SHAREDIR}")
install(FILES "misc/Info.plist" DESTINATION "${BUNDLE_PATH}/Contents")
endif()
# Library pack
find_package(GMP REQUIRED)
find_package(Json 1.0.0 REQUIRED)
find_package(Json REQUIRED)
find_package(Lua REQUIRED)
if(NOT USE_LUAJIT)
add_subdirectory(lib/bitop)
endif()
add_subdirectory(lib/sha256)
if(BUILD_UNITTESTS OR BUILD_BENCHMARKS)
if(BUILD_BENCHMARKS)
add_subdirectory(lib/catch2)
endif()
add_subdirectory(lib/tiniergltf)
# Subdirectories
# Be sure to add all relevant definitions above this
add_subdirectory(src)
@ -311,7 +318,17 @@ include(CPackComponent)
cpack_add_component(Docs
DISPLAY_NAME "Documentation"
DESCRIPTION "Documentation about ${PROJECT_NAME_CAPITALIZED} and ${PROJECT_NAME_CAPITALIZED} modding"
DESCRIPTION "Documentation about Minetest and Minetest modding"
)
cpack_add_component(SUBGAME_MINETEST_GAME
DISPLAY_NAME "Minetest Game"
DESCRIPTION "The default game bundled in the Minetest engine. Mainly used as a modding base."
GROUP "Games"
)
cpack_add_component_group(Subgames
DESCRIPTION "Games for the Minetest engine."
)
if(WIN32)
@ -335,7 +352,7 @@ if(WIN32)
set(CPACK_CREATE_DESKTOP_LINKS ${PROJECT_NAME})
set(CPACK_PACKAGING_INSTALL_PREFIX "/${PROJECT_NAME_CAPITALIZED}")
set(CPACK_WIX_PRODUCT_ICON "${CMAKE_CURRENT_SOURCE_DIR}/misc/luanti-icon.ico")
set(CPACK_WIX_PRODUCT_ICON "${CMAKE_CURRENT_SOURCE_DIR}/misc/minetest-icon.ico")
# Supported languages can be found at
# http://wixtoolset.org/documentation/manual/v3/wixui/wixui_localization.html
#set(CPACK_WIX_CULTURES "ar-SA,bg-BG,ca-ES,hr-HR,cs-CZ,da-DK,nl-NL,en-US,et-EE,fi-FI,fr-FR,de-DE")
@ -367,31 +384,13 @@ include(CPack)
# Add a target to generate API documentation with Doxygen
if(BUILD_DOCUMENTATION)
find_package(Doxygen)
if(DOXYGEN_FOUND)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile.in
${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile @ONLY)
add_custom_target(doc
${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc
COMMENT "Generating API documentation with Doxygen" VERBATIM
)
endif()
endif()
# Fetch Tracy
if(BUILD_WITH_TRACY)
include(FetchContent)
message(STATUS "Fetching Tracy (${FETCH_TRACY_GIT_TAG})...")
FetchContent_Declare(
tracy
GIT_REPOSITORY https://github.com/wolfpld/tracy.git
GIT_TAG ${FETCH_TRACY_GIT_TAG}
GIT_SHALLOW TRUE
GIT_PROGRESS TRUE
find_package(Doxygen)
if(DOXYGEN_FOUND)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile.in
${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile @ONLY)
add_custom_target(doc
${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc
COMMENT "Generating API documentation with Doxygen" VERBATIM
)
FetchContent_MakeAvailable(tracy)
message(STATUS "Fetching Tracy - done")
endif()

View file

@ -1,41 +0,0 @@
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 12
},
"configurePresets": [
{
"name": "Debug",
"displayName": "Debug",
"description": "Debug preset with debug symbols and no optimizations",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "Release",
"displayName": "Release",
"description": "Release preset with optimizations and no debug symbols",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "RelWithDebInfo",
"displayName": "RelWithDebInfo",
"description": "Release with debug symbols",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "MinSizeRel",
"displayName": "MinSizeRel",
"description": "Release with minimal code size",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "MinSizeRel"
}
}
]
}

1
CNAME
View file

@ -1 +0,0 @@
api.luanti.org

View file

@ -1,6 +1,9 @@
ARG DOCKER_IMAGE=alpine:3.19
FROM $DOCKER_IMAGE AS dev
ARG DOCKER_IMAGE=alpine:3.16
FROM $DOCKER_IMAGE AS builder
ENV MINETEST_GAME_VERSION master
ENV IRRLICHT_VERSION master
ENV SPATIALINDEX_VERSION 1.9.3
ENV LUAJIT_VERSION v2.1
RUN apk add --no-cache git build-base cmake curl-dev zlib-dev zstd-dev \
@ -8,7 +11,7 @@ RUN apk add --no-cache git build-base cmake curl-dev zlib-dev zstd-dev \
gmp-dev jsoncpp-dev ninja ca-certificates
WORKDIR /usr/src/
RUN git clone --recursive https://github.com/jupp0r/prometheus-cpp && \
RUN git clone --recursive https://github.com/jupp0r/prometheus-cpp/ && \
cd prometheus-cpp && \
cmake -B build \
-DCMAKE_INSTALL_PREFIX=/usr/local \
@ -18,7 +21,7 @@ RUN git clone --recursive https://github.com/jupp0r/prometheus-cpp && \
cmake --build build && \
cmake --install build && \
cd /usr/src/ && \
git clone --recursive https://github.com/libspatialindex/libspatialindex && \
git clone --recursive https://github.com/libspatialindex/libspatialindex -b ${SPATIALINDEX_VERSION} && \
cd libspatialindex && \
cmake -B build \
-DCMAKE_INSTALL_PREFIX=/usr/local && \
@ -27,38 +30,40 @@ RUN git clone --recursive https://github.com/jupp0r/prometheus-cpp && \
cd /usr/src/ && \
git clone --recursive https://luajit.org/git/luajit.git -b ${LUAJIT_VERSION} && \
cd luajit && \
make amalg && make install && \
cd /usr/src/
make && make install && \
cd /usr/src/ && \
git clone --depth=1 https://github.com/minetest/irrlicht/ -b ${IRRLICHT_VERSION} && \
cp -r irrlicht/include /usr/include/irrlichtmt
FROM dev as builder
COPY .git /usr/src/minetest/.git
COPY CMakeLists.txt /usr/src/minetest/CMakeLists.txt
COPY README.md /usr/src/minetest/README.md
COPY minetest.conf.example /usr/src/minetest/minetest.conf.example
COPY builtin /usr/src/minetest/builtin
COPY cmake /usr/src/minetest/cmake
COPY doc /usr/src/minetest/doc
COPY fonts /usr/src/minetest/fonts
COPY lib /usr/src/minetest/lib
COPY misc /usr/src/minetest/misc
COPY po /usr/src/minetest/po
COPY src /usr/src/minetest/src
COPY textures /usr/src/minetest/textures
COPY .git /usr/src/luanti/.git
COPY CMakeLists.txt /usr/src/luanti/CMakeLists.txt
COPY README.md /usr/src/luanti/README.md
COPY minetest.conf.example /usr/src/luanti/minetest.conf.example
COPY builtin /usr/src/luanti/builtin
COPY cmake /usr/src/luanti/cmake
COPY doc /usr/src/luanti/doc
COPY fonts /usr/src/luanti/fonts
COPY lib /usr/src/luanti/lib
COPY misc /usr/src/luanti/misc
COPY po /usr/src/luanti/po
COPY src /usr/src/luanti/src
COPY irr /usr/src/luanti/irr
COPY textures /usr/src/luanti/textures
WORKDIR /usr/src/luanti
RUN cmake -B build \
-DCMAKE_INSTALL_PREFIX=/usr/local \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SERVER=TRUE \
-DENABLE_PROMETHEUS=TRUE \
-DBUILD_UNITTESTS=FALSE -DBUILD_BENCHMARKS=FALSE \
-DBUILD_CLIENT=FALSE \
-GNinja && \
cmake --build build && \
cmake --install build
WORKDIR /usr/src/minetest
RUN git clone --depth=1 -b ${MINETEST_GAME_VERSION} https://github.com/minetest/minetest_game.git ./games/minetest_game && \
rm -fr ./games/minetest_game/.git && \
cmake -B build \
-DCMAKE_INSTALL_PREFIX=/usr/local \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SERVER=TRUE \
-DENABLE_PROMETHEUS=TRUE \
-DBUILD_UNITTESTS=FALSE \
-DBUILD_CLIENT=FALSE \
-GNinja && \
cmake --build build && \
cmake --install build
ARG DOCKER_IMAGE=alpine:3.16
FROM $DOCKER_IMAGE AS runtime
RUN apk add --no-cache curl gmp libstdc++ libgcc libpq jsoncpp zstd-libs \
@ -68,9 +73,9 @@ RUN apk add --no-cache curl gmp libstdc++ libgcc libpq jsoncpp zstd-libs \
WORKDIR /var/lib/minetest
COPY --from=builder /usr/local/share/luanti /usr/local/share/luanti
COPY --from=builder /usr/local/bin/luantiserver /usr/local/bin/luantiserver
COPY --from=builder /usr/local/share/doc/luanti/minetest.conf.example /etc/minetest/minetest.conf
COPY --from=builder /usr/local/share/minetest /usr/local/share/minetest
COPY --from=builder /usr/local/bin/minetestserver /usr/local/bin/minetestserver
COPY --from=builder /usr/local/share/doc/minetest/minetest.conf.example /etc/minetest/minetest.conf
COPY --from=builder /usr/local/lib/libspatialindex* /usr/local/lib/
COPY --from=builder /usr/local/lib/libluajit* /usr/local/lib/
USER minetest:minetest
@ -78,5 +83,5 @@ USER minetest:minetest
EXPOSE 30000/udp 30000/tcp
VOLUME /var/lib/minetest/ /etc/minetest/
ENTRYPOINT ["/usr/local/bin/luantiserver"]
ENTRYPOINT ["/usr/local/bin/minetestserver"]
CMD ["--config", "/etc/minetest/minetest.conf"]

View file

@ -1,8 +1,8 @@
License of Luanti textures and sounds
License of Minetest textures and sounds
---------------------------------------
This applies to textures and sounds contained in the main Luanti
This applies to textures and sounds contained in the main Minetest
distribution.
Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)
@ -14,9 +14,6 @@ https://www.apache.org/licenses/LICENSE-2.0.html
Textures by Zughy are under CC BY-SA 4.0
https://creativecommons.org/licenses/by-sa/4.0/
Textures by cx384 are under CC BY-SA 4.0
https://creativecommons.org/licenses/by-sa/4.0/
Media files by DS are under CC BY-SA 4.0
https://creativecommons.org/licenses/by-sa/4.0/
@ -32,6 +29,7 @@ ShadowNinja:
textures/base/pack/smoke_puff.png
paramat:
textures/base/pack/menu_header.png
textures/base/pack/next_icon.png
textures/base/pack/prev_icon.png
textures/base/pack/clear.png
@ -41,10 +39,10 @@ rubenwardy, paramat:
textures/base/pack/start_icon.png
textures/base/pack/end_icon.png
erle:
misc/luanti-icon-24x24.png
misc/luanti-icon.ico
misc/luanti.svg
erlehmann:
misc/minetest-icon-24x24.png
misc/minetest-icon.ico
misc/minetest.svg
textures/base/pack/logo.png
JRottm:
@ -59,56 +57,34 @@ srifqi:
textures/base/pack/minimap_btn.png
Zughy:
textures/base/pack/cdb_add.png
textures/base/pack/cdb_downloading.png
textures/base/pack/cdb_queued.png
textures/base/pack/cdb_update.png
textures/base/pack/cdb_update_cropped.png
textures/base/pack/settings_btn.png
textures/base/pack/settings_info.png
textures/base/pack/settings_reset.png
textures/base/pack/server_url.png
textures/base/pack/server_url_unavailable.png
textures/base/pack/server_view_clients.png
textures/base/pack/server_view_clients_unavailable.png
cx384:
textures/base/pack/server_view_mods.png
textures/base/pack/server_view_mods_unavailable.png
textures/base/pack/cdb_viewonline.png
appgurueu:
textures/base/pack/server_incompatible.png
erle, Warr1024, rollerozxa:
erlehmann, Warr1024, rollerozxa:
textures/base/pack/no_screenshot.png
kilbith:
textures/base/pack/server_favorite.png
textures/base/pack/progress_bar.png
textures/base/pack/progress_bar_bg.png
SmallJoker:
textures/base/pack/cdb_clear.png
textures/base/pack/server_favorite_delete.png (based on server_favorite.png)
DS:
games/devtest/mods/soundstuff/textures/soundstuff_bigfoot.png
games/devtest/mods/soundstuff/textures/soundstuff_jukebox.png
games/devtest/mods/soundstuff/textures/soundstuff_racecar.png
games/devtest/mods/soundstuff/sounds/soundstuff_sinus.ogg
games/devtest/mods/testtools/textures/testtools_branding_iron.png
grorp:
textures/base/pack/exit_btn.png
textures/base/pack/menu_header.png
using the font "undefined medium" (https://undefined-medium.com/),
which is licensed under the SIL Open Font License, Version 1.1
modified by DS
License of Luanti source code
License of Minetest source code
-------------------------------
Luanti
Copyright (C) 2010-2024 celeron55, Perttu Ahola <celeron55@gmail.com>
and contributors (see source file comments and the version control log)
Minetest
Copyright (C) 2010-2018 celeron55, Perttu Ahola <celeron55@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
@ -127,7 +103,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Irrlicht
---------------
This program uses IrrlichtMt, Luanti's fork of
This program uses IrrlichtMt, Minetest's fork of
the Irrlicht Engine. http://irrlicht.sourceforge.net/
The Irrlicht Engine License

386
README.md
View file

@ -1,17 +1,21 @@
<div align="center">
<img src="textures/base/pack/logo.png" width="32%">
<h1>Luanti (formerly Minetest)</h1>
<img src="https://github.com/luanti-org/luanti/workflows/build/badge.svg" alt="Build Status">
<a href="https://hosted.weblate.org/engage/minetest/?utm_source=widget"><img src="https://hosted.weblate.org/widgets/minetest/-/svg-badge.svg" alt="Translation status"></a>
<a href="https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html"><img src="https://img.shields.io/badge/license-LGPLv2.1%2B-blue.svg" alt="License"></a>
</div>
<br><br>
Minetest
========
Luanti is a free open-source voxel game engine with easy modding and game creation.
![Build Status](https://github.com/minetest/minetest/workflows/build/badge.svg)
[![Translation status](https://hosted.weblate.org/widgets/minetest/-/svg-badge.svg)](https://hosted.weblate.org/engage/minetest/?utm_source=widget)
[![License](https://img.shields.io/badge/license-LGPLv2.1%2B-blue.svg)](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html)
Copyright (C) 2010-2025 Perttu Ahola <celeron55@gmail.com>
Minetest is a free open-source voxel game engine with easy modding and game creation.
Copyright (C) 2010-2022 Perttu Ahola <celeron55@gmail.com>
and contributors (see source file comments and the version control log)
In case you downloaded the source code
--------------------------------------
If you downloaded the Minetest Engine source code in which this file is
contained, you probably want to download the [Minetest Game](https://github.com/minetest/minetest_game/)
project too. See its README.txt for more information.
Table of Contents
------------------
@ -27,11 +31,11 @@ Table of Contents
Further documentation
----------------------
- Website: https://www.luanti.org/
- Wiki: https://wiki.luanti.org/
- Forum: https://forum.luanti.org/
- GitHub: https://github.com/luanti-org/luanti/
- [Developer documentation](doc/developing/)
- Website: https://minetest.net/
- Wiki: https://wiki.minetest.net/
- Developer wiki: https://dev.minetest.net/
- Forum: https://forum.minetest.net/
- GitHub: https://github.com/minetest/minetest/
- [doc/](doc/) directory of source distribution
Default controls
@ -47,7 +51,7 @@ Some can be changed in the key config dialog in the settings tab.
| Shift | Sneak/move down |
| Q | Drop itemstack |
| Shift + Q | Drop single item |
| Left mouse button | Dig/punch/use |
| Left mouse button | Dig/punch/take item |
| Right mouse button | Place/use |
| Shift + right mouse button | Build (without using) |
| I | Inventory menu |
@ -108,7 +112,7 @@ Configuration file
------------------
- Default location:
`user/minetest.conf`
- This file is created by closing Luanti for the first time.
- This file is created by closing Minetest for the first time.
- A specific file can be specified on the command line:
`--config <path-to-file>`
- A run-in-place build will look for the configuration file in
@ -120,17 +124,353 @@ Command-line options
Compiling
---------
### Compiling on GNU/Linux
- [Compiling - common information](doc/compiling/README.md)
- [Compiling on GNU/Linux](doc/compiling/linux.md)
- [Compiling on Windows](doc/compiling/windows.md)
- [Compiling on MacOS](doc/compiling/macos.md)
#### Dependencies
| Dependency | Version | Commentary |
|------------|---------|------------|
| GCC | 5.1+ | or Clang 3.5+ |
| CMake | 3.5+ | |
| IrrlichtMt | - | Custom version of Irrlicht, see https://github.com/minetest/irrlicht |
| Freetype | 2.0+ | |
| SQLite3 | 3+ | |
| Zstd | 1.0+ | |
| LuaJIT | 2.0+ | Bundled Lua 5.1 is used if not present |
| GMP | 5.0.0+ | Bundled mini-GMP is used if not present |
| JsonCPP | 1.0.0+ | Bundled JsonCPP is used if not present |
For Debian/Ubuntu users:
sudo apt install g++ make libc6-dev cmake libpng-dev libjpeg-dev libxi-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev libzstd-dev libluajit-5.1-dev
For Fedora users:
sudo dnf install make automake gcc gcc-c++ kernel-devel cmake libcurl-devel openal-soft-devel libpng-devel libjpeg-devel libvorbis-devel libXi-devel libogg-devel freetype-devel mesa-libGL-devel zlib-devel jsoncpp-devel gmp-devel sqlite-devel luajit-devel leveldb-devel ncurses-devel spatialindex-devel libzstd-devel
For Arch users:
sudo pacman -S base-devel libcurl-gnutls cmake libxi libpng sqlite libogg libvorbis openal freetype2 jsoncpp gmp luajit leveldb ncurses zstd
For Alpine users:
sudo apk add build-base cmake libpng-dev jpeg-dev libxi-dev mesa-dev sqlite-dev libogg-dev libvorbis-dev openal-soft-dev curl-dev freetype-dev zlib-dev gmp-dev jsoncpp-dev luajit-dev zstd-dev
#### Download
You can install Git for easily keeping your copy up to date.
If you dont want Git, read below on how to get the source without Git.
This is an example for installing Git on Debian/Ubuntu:
sudo apt install git
For Fedora users:
sudo dnf install git
For Arch users:
sudo pacman -S git
For Alpine users:
sudo apk add git
Download source (this is the URL to the latest of source repository, which might not work at all times) using Git:
git clone --depth 1 https://github.com/minetest/minetest.git
cd minetest
Download Minetest Game (otherwise only the "Development Test" game is available) using Git:
git clone --depth 1 https://github.com/minetest/minetest_game.git games/minetest_game
Download IrrlichtMt to `lib/irrlichtmt`, it will be used to satisfy the IrrlichtMt dependency that way:
git clone --depth 1 https://github.com/minetest/irrlicht.git lib/irrlichtmt
Download source, without using Git:
wget https://github.com/minetest/minetest/archive/master.tar.gz
tar xf master.tar.gz
cd minetest-master
Download Minetest Game, without using Git:
cd games/
wget https://github.com/minetest/minetest_game/archive/master.tar.gz
tar xf master.tar.gz
mv minetest_game-master minetest_game
cd ..
Download IrrlichtMt, without using Git:
cd lib/
wget https://github.com/minetest/irrlicht/archive/master.tar.gz
tar xf master.tar.gz
mv irrlicht-master irrlichtmt
cd ..
#### Build
Build a version that runs directly from the source directory:
cmake . -DRUN_IN_PLACE=TRUE
make -j$(nproc)
Run it:
./bin/minetest
- Use `cmake . -LH` to see all CMake options and their current state.
- If you want to install it system-wide (or are making a distribution package),
you will want to use `-DRUN_IN_PLACE=FALSE`.
- You can build a bare server by specifying `-DBUILD_SERVER=TRUE`.
- You can disable the client build by specifying `-DBUILD_CLIENT=FALSE`.
- You can select between Release and Debug build by `-DCMAKE_BUILD_TYPE=<Debug or Release>`.
- Debug build is slower, but gives much more useful output in a debugger.
- If you build a bare server you don't need to compile IrrlichtMt, just the headers suffice.
- In that case use `-DIRRLICHT_INCLUDE_DIR=/some/where/irrlichtmt/include`.
- Minetest will use the IrrlichtMt package that is found first, given by the following order:
1. Specified `IRRLICHTMT_BUILD_DIR` CMake variable
2. `${PROJECT_SOURCE_DIR}/lib/irrlichtmt` (if existent)
3. Installation of IrrlichtMt in the system-specific library paths
4. For server builds with disabled `BUILD_CLIENT` variable, the headers from `IRRLICHT_INCLUDE_DIR` will be used.
- NOTE: Changing the IrrlichtMt build directory (includes system installs) requires regenerating the CMake cache (`rm CMakeCache.txt`)
### CMake options
General options and their default values:
BUILD_CLIENT=TRUE - Build Minetest client
BUILD_SERVER=FALSE - Build Minetest server
BUILD_UNITTESTS=TRUE - Build unittest sources
BUILD_BENCHMARKS=FALSE - Build benchmark sources
CMAKE_BUILD_TYPE=Release - Type of build (Release vs. Debug)
Release - Release build
Debug - Debug build
SemiDebug - Partially optimized debug build
RelWithDebInfo - Release build with debug information
MinSizeRel - Release build with -Os passed to compiler to make executable as small as possible
ENABLE_CURL=ON - Build with cURL; Enables use of online mod repo, public serverlist and remote media fetching via http
ENABLE_CURSES=ON - Build with (n)curses; Enables a server side terminal (command line option: --terminal)
ENABLE_GETTEXT=ON - Build with Gettext; Allows using translations
ENABLE_GLES=OFF - Enable extra support code for OpenGL ES (requires support by IrrlichtMt)
ENABLE_LEVELDB=ON - Build with LevelDB; Enables use of LevelDB map backend
ENABLE_POSTGRESQL=ON - Build with libpq; Enables use of PostgreSQL map backend (PostgreSQL 9.5 or greater recommended)
ENABLE_REDIS=ON - Build with libhiredis; Enables use of Redis map backend
ENABLE_SPATIAL=ON - Build with LibSpatial; Speeds up AreaStores
ENABLE_SOUND=ON - Build with OpenAL, libogg & libvorbis; in-game sounds
ENABLE_LUAJIT=ON - Build with LuaJIT (much faster than non-JIT Lua)
ENABLE_PROMETHEUS=OFF - Build with Prometheus metrics exporter (listens on tcp/30000 by default)
ENABLE_SYSTEM_GMP=ON - Use GMP from system (much faster than bundled mini-gmp)
ENABLE_SYSTEM_JSONCPP=ON - Use JsonCPP from system
RUN_IN_PLACE=FALSE - Create a portable install (worlds, settings etc. in current directory)
ENABLE_UPDATE_CHECKER=TRUE - Whether to enable update checks by default
INSTALL_DEVTEST=FALSE - Whether the Development Test game should be installed alongside Minetest
USE_GPROF=FALSE - Enable profiling using GProf
VERSION_EXTRA= - Text to append to version (e.g. VERSION_EXTRA=foobar -> Minetest 0.4.9-foobar)
ENABLE_TOUCH=FALSE - Enable Touchscreen support (requires support by IrrlichtMt)
Library specific options:
CURL_DLL - Only if building with cURL on Windows; path to libcurl.dll
CURL_INCLUDE_DIR - Only if building with cURL; directory where curl.h is located
CURL_LIBRARY - Only if building with cURL; path to libcurl.a/libcurl.so/libcurl.lib
EGL_INCLUDE_DIR - Only if building with GLES; directory that contains egl.h
EGL_LIBRARY - Only if building with GLES; path to libEGL.a/libEGL.so
EXTRA_DLL - Only on Windows; optional paths to additional DLLs that should be packaged
FREETYPE_INCLUDE_DIR_freetype2 - Directory that contains files such as ftimage.h
FREETYPE_INCLUDE_DIR_ft2build - Directory that contains ft2build.h
FREETYPE_LIBRARY - Path to libfreetype.a/libfreetype.so/freetype.lib
FREETYPE_DLL - Only on Windows; path to libfreetype-6.dll
GETTEXT_DLL - Only when building with gettext on Windows; paths to libintl + libiconv DLLs
GETTEXT_INCLUDE_DIR - Only when building with gettext; directory that contains libintl.h
GETTEXT_LIBRARY - Optional/platform-dependent with gettext; path to libintl.so/libintl.dll.a
GETTEXT_MSGFMT - Only when building with gettext; path to msgfmt/msgfmt.exe
ICONV_LIBRARY - Optional/platform-dependent; path to libiconv.so/libiconv.dylib
IRRLICHT_DLL - Only on Windows; path to IrrlichtMt.dll
IRRLICHT_INCLUDE_DIR - Directory that contains IrrCompileConfig.h (usable for server build only)
LEVELDB_INCLUDE_DIR - Only when building with LevelDB; directory that contains db.h
LEVELDB_LIBRARY - Only when building with LevelDB; path to libleveldb.a/libleveldb.so/libleveldb.dll.a
LEVELDB_DLL - Only when building with LevelDB on Windows; path to libleveldb.dll
PostgreSQL_INCLUDE_DIR - Only when building with PostgreSQL; directory that contains libpq-fe.h
PostgreSQL_LIBRARY - Only when building with PostgreSQL; path to libpq.a/libpq.so/libpq.lib
REDIS_INCLUDE_DIR - Only when building with Redis; directory that contains hiredis.h
REDIS_LIBRARY - Only when building with Redis; path to libhiredis.a/libhiredis.so
SPATIAL_INCLUDE_DIR - Only when building with LibSpatial; directory that contains spatialindex/SpatialIndex.h
SPATIAL_LIBRARY - Only when building with LibSpatial; path to libspatialindex.so/spatialindex-32.lib
LUA_INCLUDE_DIR - Only if you want to use LuaJIT; directory where luajit.h is located
LUA_LIBRARY - Only if you want to use LuaJIT; path to libluajit.a/libluajit.so
OGG_DLL - Only if building with sound on Windows; path to libogg.dll
OGG_INCLUDE_DIR - Only if building with sound; directory that contains an ogg directory which contains ogg.h
OGG_LIBRARY - Only if building with sound; path to libogg.a/libogg.so/libogg.dll.a
OPENAL_DLL - Only if building with sound on Windows; path to OpenAL32.dll
OPENAL_INCLUDE_DIR - Only if building with sound; directory where al.h is located
OPENAL_LIBRARY - Only if building with sound; path to libopenal.a/libopenal.so/OpenAL32.lib
SQLITE3_INCLUDE_DIR - Directory that contains sqlite3.h
SQLITE3_LIBRARY - Path to libsqlite3.a/libsqlite3.so/sqlite3.lib
VORBISFILE_LIBRARY - Only if building with sound; path to libvorbisfile.a/libvorbisfile.so/libvorbisfile.dll.a
VORBIS_DLL - Only if building with sound on Windows; paths to vorbis DLLs
VORBIS_INCLUDE_DIR - Only if building with sound; directory that contains a directory vorbis with vorbisenc.h inside
VORBIS_LIBRARY - Only if building with sound; path to libvorbis.a/libvorbis.so/libvorbis.dll.a
ZLIB_DLL - Only on Windows; path to zlib1.dll
ZLIB_INCLUDE_DIR - Directory that contains zlib.h
ZLIB_LIBRARY - Path to libz.a/libz.so/zlib.lib
ZSTD_DLL - Only on Windows; path to libzstd.dll
ZSTD_INCLUDE_DIR - Directory that contains zstd.h
ZSTD_LIBRARY - Path to libzstd.a/libzstd.so/ztd.lib
### Compiling on Windows using MSVC
### Requirements
- [Visual Studio 2015 or newer](https://visualstudio.microsoft.com)
- [CMake](https://cmake.org/download/)
- [vcpkg](https://github.com/Microsoft/vcpkg)
- [Git](https://git-scm.com/downloads)
### Compiling and installing the dependencies
It is highly recommended to use vcpkg as package manager.
After you successfully built vcpkg you can easily install the required libraries:
```powershell
vcpkg install zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp opengl-registry --triplet x64-windows
```
- **Don't forget about IrrlichtMt.** The easiest way is to clone it to `lib/irrlichtmt` as described in the Linux section.
- `curl` is optional, but required to read the serverlist, `curl[winssl]` is required to use the content store.
- `openal-soft`, `libvorbis` and `libogg` are optional, but required to use sound.
- `luajit` is optional, it replaces the integrated Lua interpreter with a faster just-in-time interpreter.
- `gmp` and `jsoncpp` are optional, otherwise the bundled versions will be compiled
There are other optional libraries, but they are not tested if they can build and link correctly.
Use `--triplet` to specify the target triplet, e.g. `x64-windows` or `x86-windows`.
### Compile Minetest
#### a) Using the vcpkg toolchain and CMake GUI
1. Start up the CMake GUI
2. Select **Browse Source...** and select DIR/minetest
3. Select **Browse Build...** and select DIR/minetest-build
4. Select **Configure**
5. Choose the right visual Studio version and target platform. It has to match the version of the installed dependencies
6. Choose **Specify toolchain file for cross-compiling**
7. Click **Next**
8. Select the vcpkg toolchain file e.g. `D:/vcpkg/scripts/buildsystems/vcpkg.cmake`
9. Click Finish
10. Wait until cmake have generated the cash file
11. If there are any errors, solve them and hit **Configure**
12. Click **Generate**
13. Click **Open Project**
14. Compile Minetest inside Visual studio.
#### b) Using the vcpkg toolchain and the commandline
Run the following script in PowerShell:
```powershell
cmake . -G"Visual Studio 15 2017 Win64" -DCMAKE_TOOLCHAIN_FILE=D:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_GETTEXT=OFF -DENABLE_CURSES=OFF
cmake --build . --config Release
```
Make sure that the right compiler is selected and the path to the vcpkg toolchain is correct.
### Windows Installer using WiX Toolset
Requirements:
* [Visual Studio 2017](https://visualstudio.microsoft.com/)
* [WiX Toolset](https://wixtoolset.org/)
In the Visual Studio 2017 Installer select **Optional Features -> WiX Toolset**.
Build the binaries as described above, but make sure you unselect `RUN_IN_PLACE`.
Open the generated project file with Visual Studio. Right-click **Package** and choose **Generate**.
It may take some minutes to generate the installer.
### Compiling on MacOS
#### Requirements
- [Homebrew](https://brew.sh/)
- [Git](https://git-scm.com/downloads)
Install dependencies with homebrew:
```
brew install cmake freetype gettext gmp hiredis jpeg jsoncpp leveldb libogg libpng libvorbis luajit zstd
```
#### Download
Download source (this is the URL to the latest of source repository, which might not work at all times) using Git:
```bash
git clone --depth 1 https://github.com/minetest/minetest.git
cd minetest
```
Download Minetest Game (otherwise only the "Development Test" game is available) using Git:
```
git clone --depth 1 https://github.com/minetest/minetest_game.git games/minetest_game
```
Download Minetest's fork of Irrlicht:
```
git clone --depth 1 https://github.com/minetest/irrlicht.git lib/irrlichtmt
```
#### Build
```bash
mkdir build
cd build
cmake .. \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 \
-DCMAKE_FIND_FRAMEWORK=LAST \
-DCMAKE_INSTALL_PREFIX=../build/macos/ \
-DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE
make -j$(sysctl -n hw.logicalcpu)
make install
```
#### Run
```
open ./build/macos/minetest.app
```
Docker
------
We provide Minetest server Docker images using the GitLab mirror registry.
Images are built on each commit and available using the following tag scheme:
* `registry.gitlab.com/minetest/minetest/server:latest` (latest build)
* `registry.gitlab.com/minetest/minetest/server:<branch/tag>` (current branch or current tag)
* `registry.gitlab.com/minetest/minetest/server:<commit-id>` (current commit id)
If you want to test it on a Docker server you can easily run:
sudo docker run registry.gitlab.com/minetest/minetest/server:<docker tag>
If you want to use it in a production environment you should use volumes bound to the Docker host
to persist data and modify the configuration:
sudo docker create -v /home/minetest/data/:/var/lib/minetest/ -v /home/minetest/conf/:/etc/minetest/ registry.gitlab.com/minetest/minetest/server:master
Data will be written to `/home/minetest/data` on the host, and configuration will be read from `/home/minetest/conf/minetest.conf`.
**Note:** If you don't understand the previous commands please read the official Docker documentation before use.
You can also host your Minetest server inside a Kubernetes cluster. See our example implementation in [`misc/kubernetes.yml`](misc/kubernetes.yml).
- [Developing minetestserver with Docker](doc/developing/docker.md)
- [Running a server with Docker](doc/docker_server.md)
Version scheme
--------------

View file

@ -1,17 +0,0 @@
diff --git a/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/android/app/src/main/java/org/libsdl/app/SDLActivity.java
--- a/android/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/android/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -1345,7 +1345,12 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
}
}
- if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
+ if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE ||
+ /*
+ * CUSTOM ADDITION FOR LUANTI
+ * should be upstreamed
+ */
+ (source & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE) {
// on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
// they are ignored here because sending them as mouse input to SDL is messy
if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {

View file

@ -1,18 +1,15 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 33
buildToolsVersion '33.0.2'
ndkVersion "$ndk_version"
defaultConfig {
applicationId 'net.minetest.minetest'
minSdkVersion 21
compileSdk 34
targetSdkVersion 34
targetSdkVersion 33
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
versionCode versionMajor * 1000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild
}
buildFeatures {
buildConfig true
versionCode project.versionCode
}
// load properties
@ -52,15 +49,12 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
namespace 'net.minetest.minetest'
}
task prepareAssets() {
def assetsFolder = "build/assets"
def projRoot = rootDir.parent
// See issue #4638
def unsupportedLanguages = new File("${projRoot}/src/unsupported_language_list.txt").text.readLines()
def gameToCopy = "minetest_game"
doFirst {
logger.lifecycle('Preparing assets at {}', assetsFolder)
@ -79,23 +73,24 @@ task prepareAssets() {
from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders"
}
copy {
from "${projRoot}/irr/media/Shaders" into "${assetsFolder}/client/shaders/Irrlicht"
from "../native/deps/armeabi-v7a/Irrlicht/Shaders" into "${assetsFolder}/client/shaders/Irrlicht"
}
copy {
from "${projRoot}/fonts" include "*.ttf" into "${assetsFolder}/fonts"
}
copy {
from "${projRoot}/textures/base/pack" into "${assetsFolder}/textures/base/pack"
from "${projRoot}/games/${gameToCopy}" into "${assetsFolder}/games/${gameToCopy}"
}
copy {
from "${projRoot}/textures" into "${assetsFolder}/textures"
}
// compile translations
fileTree("${projRoot}/po").include("**/*.po").grep {
it.parentFile.name !in unsupportedLanguages
}.forEach { poFile ->
fileTree("${projRoot}/po").include("**/*.po").forEach { poFile ->
def moPath = "${assetsFolder}/locale/${poFile.parentFile.name}/LC_MESSAGES/"
file(moPath).mkdirs()
exec {
commandLine 'msgfmt', '-o', "${moPath}/luanti.mo", poFile
commandLine 'msgfmt', '-o', "${moPath}/minetest.mo", poFile
}
}
@ -103,7 +98,7 @@ task prepareAssets() {
}
task zipAssets(dependsOn: prepareAssets, type: Zip) {
archiveFileName = "assets.zip"
archiveFileName = "Minetest.zip"
from assetsFolder
destinationDirectory = file("src/main/assets")
}
@ -112,8 +107,16 @@ task prepareAssets() {
preBuild.dependsOn zipAssets
prepareAssets.dependsOn ':native:getDeps'
clean {
delete new File("src/main/assets", "assets.zip")
// Map for the version code that gives each ABI a value.
import com.android.build.OutputFile
def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1]
android.applicationVariants.all { variant ->
variant.outputs.each {
output ->
def abiName = output.getFilter(OutputFile.ABI)
output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode
}
}
dependencies {

View file

@ -1,16 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
package="net.minetest.minetest"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature android:glEsVersion="0x00020000" />
<!--
`android:requestLegacyExternalStorage="true"` is workaround for using `/sdcard`
instead of the `getFilesDir()` patch for assets. Check link below for more information:
https://developer.android.com/training/data-storage/compatibility
-->
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/label"
android:requestLegacyExternalStorage="true"
android:resizeableActivity="false"
tools:ignore="UnusedAttribute">
@ -43,6 +51,9 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="Minetest" />
</activity>
<service

View file

@ -2,8 +2,6 @@
Minetest
Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua>
Copyright (C) 2014-2020 ubulem, Bektur Mambetov <berkut87@gmail.com>
Copyright (C) 2023 srifqi, Muhammad Rifqi Priyo Susanto
<muhammadrifqipriyosusanto@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
@ -31,52 +29,17 @@ import androidx.appcompat.widget.AppCompatEditText;
import java.util.Objects;
public class CustomEditText extends AppCompatEditText {
private int editType = 2; // single line text input as default
private boolean wantsToShowKeyboard = false;
public CustomEditText(Context context) {
super(context);
}
public CustomEditText(Context context, int _editType) {
super(context);
editType = _editType;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
// For multi-line, do not close the dialog after pressing back button
if (editType != 1 && keyCode == KeyEvent.KEYCODE_BACK) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
InputMethodManager mgr = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
Objects.requireNonNull(mgr).hideSoftInputFromWindow(this.getWindowToken(), 0);
}
return false;
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
tryShowKeyboard();
}
public void requestFocusTryShow() {
requestFocus();
wantsToShowKeyboard = true;
tryShowKeyboard();
}
private void tryShowKeyboard() {
if (hasWindowFocus() && wantsToShowKeyboard) {
if (isFocused()) {
CustomEditText that = this;
post(() -> {
final InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(that, 0);
});
}
wantsToShowKeyboard = false;
}
}
}

View file

@ -20,11 +20,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
package net.minetest.minetest;
import org.libsdl.app.SDLActivity;
import android.app.NativeActivity;
import android.content.Intent;
import android.content.ActivityNotFoundException;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
@ -33,85 +32,88 @@ import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;
import android.content.res.Configuration;
import androidx.annotation.Keep;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
import java.io.File;
import java.util.Locale;
import java.util.Objects;
// Native code finds these methods by name (see porting_android.cpp).
// This annotation prevents the minifier/Proguard from mangling them.
@Keep
@SuppressWarnings("unused")
public class GameActivity extends SDLActivity {
@Override
protected String getMainSharedObject() {
return getContext().getApplicationInfo().nativeLibraryDir + "/libluanti.so";
public class GameActivity extends NativeActivity {
static {
System.loadLibrary("c++_shared");
System.loadLibrary("Minetest");
}
@Override
protected String getMainFunction() {
return "SDL_Main";
}
@Override
protected String[] getLibraries() {
return new String[] {
"luanti"
};
}
// Prevent SDL from changing orientation settings since we already set the
// correct orientation in our AndroidManifest.xml
@Override
public void setOrientationBis(int w, int h, boolean resizable, String hint) {}
enum DialogType { TEXT_INPUT, SELECTION_INPUT }
enum DialogState { DIALOG_SHOWN, DIALOG_INPUTTED, DIALOG_CANCELED }
private DialogType lastDialogType = DialogType.TEXT_INPUT;
private DialogState inputDialogState = DialogState.DIALOG_CANCELED;
private int messageReturnCode = -1;
private String messageReturnValue = "";
private int selectionReturnValue = 0;
private native void saveSettings();
public static native void putMessageBoxResult(String text);
@Override
protected void onStop() {
super.onStop();
// Avoid losing setting changes in case the app is onDestroy()ed later.
// Saving stuff in onStop() is recommended in the Android activity
// lifecycle documentation.
saveSettings();
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
public void showTextInputDialog(String hint, String current, int editType) {
runOnUiThread(() -> showTextInputDialogUI(hint, current, editType));
private void makeFullScreen() {
if (Build.VERSION.SDK_INT >= 19)
this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
public void showSelectionInputDialog(String[] optionList, int selectedIdx) {
runOnUiThread(() -> showSelectionInputDialogUI(optionList, selectedIdx));
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus)
makeFullScreen();
}
private void showTextInputDialogUI(String hint, String current, int editType) {
lastDialogType = DialogType.TEXT_INPUT;
inputDialogState = DialogState.DIALOG_SHOWN;
@Override
protected void onResume() {
super.onResume();
makeFullScreen();
}
@Override
public void onBackPressed() {
// Ignore the back press so Minetest can handle it
}
public void showDialog(String acceptButton, String hint, String current, int editType) {
runOnUiThread(() -> showDialogUI(hint, current, editType));
}
private void showDialogUI(String hint, String current, int editType) {
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
LinearLayout container = new LinearLayout(this);
container.setOrientation(LinearLayout.VERTICAL);
builder.setView(container);
AlertDialog alertDialog = builder.create();
CustomEditText editText = new CustomEditText(this, editType);
EditText editText;
// For multi-line, do not close the dialog after pressing back button
if (editType == 1) {
editText = new EditText(this);
} else {
editText = new CustomEditText(this);
}
container.addView(editText);
editText.setMaxLines(8);
editText.requestFocus();
editText.setHint(hint);
editText.setText(current);
final InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
Objects.requireNonNull(imm).toggleSoftInput(InputMethodManager.SHOW_FORCED,
InputMethodManager.HIDE_IMPLICIT_ONLY);
if (editType == 1)
editText.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_FLAG_MULTI_LINE);
@ -120,13 +122,12 @@ public class GameActivity extends SDLActivity {
InputType.TYPE_TEXT_VARIATION_PASSWORD);
else
editText.setInputType(InputType.TYPE_CLASS_TEXT);
editText.setSelection(Objects.requireNonNull(editText.getText()).length());
final InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
editText.setSelection(editText.getText().length());
editText.setOnKeyListener((view, keyCode, event) -> {
// For multi-line, do not submit the text after pressing Enter key
if (keyCode == KeyEvent.KEYCODE_ENTER && editType != 1) {
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
inputDialogState = DialogState.DIALOG_INPUTTED;
messageReturnCode = 0;
messageReturnValue = editText.getText().toString();
alertDialog.dismiss();
return true;
@ -140,55 +141,28 @@ public class GameActivity extends SDLActivity {
doneButton.setText(R.string.ime_dialog_done);
doneButton.setOnClickListener((view -> {
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
inputDialogState = DialogState.DIALOG_INPUTTED;
messageReturnCode = 0;
messageReturnValue = editText.getText().toString();
alertDialog.dismiss();
}));
}
alertDialog.show();
alertDialog.setOnCancelListener(dialog -> {
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
inputDialogState = DialogState.DIALOG_CANCELED;
messageReturnValue = current;
messageReturnCode = -1;
});
alertDialog.show();
editText.requestFocusTryShow();
}
public void showSelectionInputDialogUI(String[] optionList, int selectedIdx) {
lastDialogType = DialogType.SELECTION_INPUT;
inputDialogState = DialogState.DIALOG_SHOWN;
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setSingleChoiceItems(optionList, selectedIdx, (dialog, selection) -> {
inputDialogState = DialogState.DIALOG_INPUTTED;
selectionReturnValue = selection;
dialog.dismiss();
});
builder.setOnCancelListener(dialog -> {
inputDialogState = DialogState.DIALOG_CANCELED;
selectionReturnValue = selectedIdx;
});
AlertDialog alertDialog = builder.create();
alertDialog.show();
public int getDialogState() {
return messageReturnCode;
}
public int getLastDialogType() {
return lastDialogType.ordinal();
}
public int getInputDialogState() {
return inputDialogState.ordinal();
}
public String getDialogMessage() {
inputDialogState = DialogState.DIALOG_CANCELED;
public String getDialogValue() {
messageReturnCode = -1;
return messageReturnValue;
}
public int getDialogSelection() {
inputDialogState = DialogState.DIALOG_CANCELED;
return selectionReturnValue;
}
public float getDensity() {
return getResources().getDisplayMetrics().density;
}
@ -203,11 +177,7 @@ public class GameActivity extends SDLActivity {
public void openURI(String uri) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
try {
startActivity(browserIntent);
} catch (ActivityNotFoundException e) {
runOnUiThread(() -> Toast.makeText(this, R.string.no_web_browser, Toast.LENGTH_SHORT).show());
}
startActivity(browserIntent);
}
public String getUserDataPath() {
@ -235,32 +205,4 @@ public class GameActivity extends SDLActivity {
Intent shareIntent = Intent.createChooser(intent, null);
startActivity(shareIntent);
}
public String getLanguage() {
String langCode = Locale.getDefault().getLanguage();
// getLanguage() still uses old language codes to preserve compatibility.
// List of code changes in ISO 639-2:
// https://www.loc.gov/standards/iso639-2/php/code_changes.php
switch (langCode) {
case "in":
langCode = "id"; // Indonesian
break;
case "iw":
langCode = "he"; // Hebrew
break;
case "ji":
langCode = "yi"; // Yiddish
break;
case "jw":
langCode = "jv"; // Javanese
break;
}
return langCode;
}
public boolean hasPhysicalKeyboard() {
return getContext().getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS;
}
}

View file

@ -20,31 +20,38 @@ with this program; if not, write to the Free Software Foundation, Inc.,
package net.minetest.minetest;
import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static net.minetest.minetest.UnzipService.*;
public class MainActivity extends AppCompatActivity {
public static final String NOTIFICATION_CHANNEL_ID = "Minetest channel";
private final static int versionCode = BuildConfig.VERSION_CODE;
private final static int PERMISSIONS = 1;
private static final String[] REQUIRED_SDK_PERMISSIONS =
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
private static final String SETTINGS = "MinetestSettings";
private static final String TAG_VERSION_CODE = "versionCode";
@ -84,30 +91,67 @@ public class MainActivity extends AppCompatActivity {
}
};
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
IntentFilter filter = new IntentFilter(ACTION_UPDATE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(myReceiver, filter, RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(myReceiver, filter);
}
registerReceiver(myReceiver, filter);
mProgressBar = findViewById(R.id.progressBar);
mTextView = findViewById(R.id.textView);
sharedPreferences = getSharedPreferences(SETTINGS, Context.MODE_PRIVATE);
checkAppVersion();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
checkPermission();
else
checkAppVersion();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
createNotificationChannel();
private void checkPermission() {
final List<String> missingPermissions = new ArrayList<>();
for (final String permission : REQUIRED_SDK_PERMISSIONS) {
final int result = ContextCompat.checkSelfPermission(this, permission);
if (result != PackageManager.PERMISSION_GRANTED)
missingPermissions.add(permission);
}
if (!missingPermissions.isEmpty()) {
final String[] permissions = missingPermissions
.toArray(new String[0]);
ActivityCompat.requestPermissions(this, permissions, PERMISSIONS);
} else {
final int[] grantResults = new int[REQUIRED_SDK_PERMISSIONS.length];
Arrays.fill(grantResults, PackageManager.PERMISSION_GRANTED);
onRequestPermissionsResult(PERMISSIONS, REQUIRED_SDK_PERMISSIONS, grantResults);
}
}
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSIONS) {
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, R.string.not_granted, Toast.LENGTH_LONG).show();
finish();
return;
}
}
checkAppVersion();
}
}
private void checkAppVersion() {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Toast.makeText(this, R.string.no_external_storage, Toast.LENGTH_LONG).show();
finish();
return;
}
if (UnzipService.getIsRunning()) {
mProgressBar.setVisibility(View.VISIBLE);
mProgressBar.setIndeterminate(true);
@ -132,28 +176,6 @@ public class MainActivity extends AppCompatActivity {
startActivity(intent);
}
@RequiresApi(Build.VERSION_CODES.O)
private void createNotificationChannel() {
NotificationManager notifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (notifyManager == null)
return;
NotificationChannel notifyChannel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_LOW
);
notifyChannel.setDescription(getString(R.string.notification_channel_description));
// Configure the notification channel without sound set
notifyChannel.setSound(null, null);
notifyChannel.enableLights(false);
notifyChannel.enableVibration(false);
// It is fine to always create the notification channel because creating a channel
// with the same ID is the same as overriding it (only its name and description).
notifyManager.createNotificationChannel(notifyChannel);
}
@Override
public void onBackPressed() {
// Prevent abrupt interruption when copy game files from assets

View file

@ -22,11 +22,13 @@ package net.minetest.minetest;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
@ -57,11 +59,9 @@ public class UnzipService extends IntentService {
private String failureMessage;
private static boolean isRunning = false;
public static synchronized boolean getIsRunning() {
return isRunning;
}
private static synchronized void setIsRunning(boolean v) {
isRunning = v;
}
@ -73,7 +73,7 @@ public class UnzipService extends IntentService {
@Override
protected void onHandleIntent(Intent intent) {
Notification.Builder notificationBuilder = createNotification();
final File zipFile = new File(getCacheDir(), "assets.zip");
final File zipFile = new File(getCacheDir(), "Minetest.zip");
try {
setIsRunning(true);
File userDataDirectory = Utils.getUserDataDirectory(this);
@ -88,6 +88,7 @@ public class UnzipService extends IntentService {
}
}
migrate(notificationBuilder, userDataDirectory);
unzip(notificationBuilder, zipFile, userDataDirectory);
} catch (IOException e) {
isSuccess = false;
@ -100,13 +101,28 @@ public class UnzipService extends IntentService {
}
}
@NonNull
private Notification.Builder createNotification() {
String name = "net.minetest.minetest";
String channelId = "Minetest channel";
String description = "notifications from Minetest";
Notification.Builder builder;
if (mNotifyManager == null)
mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(this, MainActivity.NOTIFICATION_CHANNEL_ID);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel mChannel = null;
if (mNotifyManager != null)
mChannel = mNotifyManager.getNotificationChannel(channelId);
if (mChannel == null) {
mChannel = new NotificationChannel(channelId, name, importance);
mChannel.setDescription(description);
// Configure the notification channel, NO SOUND
mChannel.setSound(null, null);
mChannel.enableLights(false);
mChannel.enableVibration(false);
mNotifyManager.createNotificationChannel(mChannel);
}
builder = new Notification.Builder(this, channelId);
} else {
builder = new Notification.Builder(this);
}
@ -121,9 +137,9 @@ public class UnzipService extends IntentService {
PendingIntent intent = PendingIntent.getActivity(this, 0,
notificationIntent, pendingIntentFlag);
builder.setContentTitle(getString(R.string.unzip_notification_title))
builder.setContentTitle(getString(R.string.notification_title))
.setSmallIcon(R.mipmap.ic_launcher)
.setContentText(getString(R.string.unzip_notification_description))
.setContentText(getString(R.string.notification_description))
.setContentIntent(intent)
.setOngoing(true)
.setProgress(0, 0, true);
@ -166,9 +182,9 @@ public class UnzipService extends IntentService {
try {
Process p = new ProcessBuilder("/system/bin/mv",
src.getAbsolutePath(), dst.getAbsolutePath()).start();
int exitCode = p.waitFor();
if (exitCode != 0)
throw new IOException("Move failed with exit code " + exitCode);
int exitcode = p.waitFor();
if (exitcode != 0)
throw new IOException("Move failed with exit code " + exitcode);
} catch (InterruptedException e) {
throw new IOException("Move operation interrupted");
}
@ -184,9 +200,46 @@ public class UnzipService extends IntentService {
}
}
private void publishProgress(@Nullable Notification.Builder notificationBuilder, @StringRes int message, int progress) {
/**
* Migrates user data from deprecated external storage to app scoped storage
*/
private void migrate(Notification.Builder notificationBuilder, File newLocation) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return;
}
File oldLocation = new File(Environment.getExternalStorageDirectory(), "Minetest");
if (!oldLocation.isDirectory())
return;
publishProgress(notificationBuilder, R.string.migrating, 0);
if (!newLocation.mkdir()) {
Log.e("UnzipService", "New installation folder cannot be made");
}
String[] dirs = new String[] { "worlds", "games", "mods", "textures", "client" };
for (int i = 0; i < dirs.length; i++) {
publishProgress(notificationBuilder, R.string.migrating, 100 * i / dirs.length);
File dir = new File(oldLocation, dirs[i]), dir2 = new File(newLocation, dirs[i]);
if (dir.isDirectory() && !dir2.isDirectory()) {
moveFileOrDir(dir, dir2);
}
}
for (String filename : new String[] { "minetest.conf" }) {
File file = new File(oldLocation, filename), file2 = new File(newLocation, filename);
if (file.isFile() && !file2.isFile()) {
moveFileOrDir(file, file2);
}
}
if (!recursivelyDeleteDirectory(oldLocation)) {
Log.w("UnzipService", "Old installation files cannot be deleted successfully");
}
}
private void publishProgress(@Nullable Notification.Builder notificationBuilder, @StringRes int message, int progress) {
Intent intentUpdate = new Intent(ACTION_UPDATE);
intentUpdate.setPackage(getPackageName());
intentUpdate.putExtra(ACTION_PROGRESS, progress);
intentUpdate.putExtra(ACTION_PROGRESS_MESSAGE, message);
if (!isSuccess)

View file

@ -38,6 +38,7 @@ public class Utils {
public static boolean isInstallValid(@NonNull Context context) {
File userDataDirectory = getUserDataDirectory(context);
return userDataDirectory.isDirectory() &&
new File(userDataDirectory, "games").isDirectory() &&
new File(userDataDirectory, "builtin").isDirectory() &&
new File(userDataDirectory, "client").isDirectory() &&
new File(userDataDirectory, "textures").isDirectory();

View file

@ -1,22 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int sendFeatureReport(byte[] report);
public int sendOutputReport(byte[] report);
public boolean getFeatureReport(byte[] report);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}

View file

@ -1,650 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.os.*;
//import com.android.internal.util.HexDump;
import java.lang.Runnable;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.UUID;
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
private static final String TAG = "hidapi";
private HIDDeviceManager mManager;
private BluetoothDevice mDevice;
private int mDeviceId;
private BluetoothGatt mGatt;
private boolean mIsRegistered = false;
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
private Handler mHandler;
private static final int TRANSPORT_AUTO = 0;
private static final int TRANSPORT_BREDR = 1;
private static final int TRANSPORT_LE = 2;
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
static public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
static public final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
static class GattOperation {
private enum Operation {
CHR_READ,
CHR_WRITE,
ENABLE_NOTIFICATION
}
Operation mOp;
UUID mUuid;
byte[] mValue;
BluetoothGatt mGatt;
boolean mResult = true;
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
}
public void run() {
// This is executed in main thread
BluetoothGattCharacteristic chr;
switch (mOp) {
case CHR_READ:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case CHR_WRITE:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
chr.setValue(mValue);
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case ENABLE_NOTIFICATION:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
if (chr != null) {
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
int properties = chr.getProperties();
byte[] value;
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
} else {
Log.e(TAG, "Unable to start notifications on input characteristic");
mResult = false;
return;
}
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
mResult = false;
return;
}
mResult = true;
}
}
}
}
public boolean finish() {
return mResult;
}
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return null;
return valveService.getCharacteristic(uuid);
}
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.CHR_READ, uuid);
}
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
}
}
public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
mManager = manager;
mDevice = device;
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
mIsRegistered = false;
mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
mOperations = new LinkedList<GattOperation>();
mHandler = new Handler(Looper.getMainLooper());
mGatt = connectGatt();
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.checkConnectionForChromebookIssue();
// }
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
public String getIdentifier() {
return String.format("SteamController.%s", mDevice.getAddress());
}
public BluetoothGatt getGatt() {
return mGatt;
}
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
private BluetoothGatt connectGatt(boolean managed) {
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
}
private BluetoothGatt connectGatt() {
return connectGatt(false);
}
protected int getConnectionState() {
Context context = mManager.getContext();
if (context == null) {
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
return BluetoothProfile.STATE_DISCONNECTED;
}
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
if (btManager == null) {
// This device doesn't support Bluetooth. We should never be here, because how did
// we instantiate a device to start with?
return BluetoothProfile.STATE_DISCONNECTED;
}
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
}
public void reconnect() {
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
mGatt.disconnect();
mGatt = connectGatt();
}
}
protected void checkConnectionForChromebookIssue() {
if (!mIsChromebook) {
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
// over and over.
return;
}
int connectionState = getConnectionState();
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
if (!mIsConnected) {
// We are in the Bad Chromebook Place. We can force a disconnect
// to try to recover.
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
else if (!isRegistered()) {
if (mGatt.getServices().size() > 0) {
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
probeService(this);
}
else {
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
}
else {
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
return;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
case BluetoothProfile.STATE_CONNECTING:
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
break;
}
final HIDDeviceBLESteamController finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.checkConnectionForChromebookIssue();
}
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
private boolean isRegistered() {
return mIsRegistered;
}
private void setRegistered() {
mIsRegistered = true;
}
private boolean probeService(HIDDeviceBLESteamController controller) {
if (isRegistered()) {
return true;
}
if (!mIsConnected) {
return false;
}
Log.v(TAG, "probeService controller=" + controller);
for (BluetoothGattService service : mGatt.getServices()) {
if (service.getUuid().equals(steamControllerService)) {
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(inputCharacteristic)) {
Log.v(TAG, "Found input characteristic");
// Start notifications
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
enableNotification(chr.getUuid());
}
}
}
return true;
}
}
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
mIsConnected = false;
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private void finishCurrentGattOperation() {
GattOperation op = null;
synchronized (mOperations) {
if (mCurrentOperation != null) {
op = mCurrentOperation;
mCurrentOperation = null;
}
}
if (op != null) {
boolean result = op.finish(); // TODO: Maybe in main thread as well?
// Our operation failed, let's add it back to the beginning of our queue.
if (!result) {
mOperations.addFirst(op);
}
}
executeNextGattOperation();
}
private void executeNextGattOperation() {
synchronized (mOperations) {
if (mCurrentOperation != null)
return;
if (mOperations.isEmpty())
return;
mCurrentOperation = mOperations.removeFirst();
}
// Run in main thread
mHandler.post(new Runnable() {
@Override
public void run() {
synchronized (mOperations) {
if (mCurrentOperation == null) {
Log.e(TAG, "Current operation null in executor?");
return;
}
mCurrentOperation.run();
// now wait for the GATT callback and when it comes, finish this operation
}
}
});
}
private void queueGattOperation(GattOperation op) {
synchronized (mOperations) {
mOperations.add(op);
}
executeNextGattOperation();
}
private void enableNotification(UUID chrUuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
queueGattOperation(op);
}
public void writeCharacteristic(UUID uuid, byte[] value) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
queueGattOperation(op);
}
public void readCharacteristic(UUID uuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
queueGattOperation(op);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////////// BluetoothGattCallback overridden methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
mIsReconnecting = false;
if (newState == 2) {
mIsConnected = true;
// Run directly, without GattOperation
if (!isRegistered()) {
mHandler.post(new Runnable() {
@Override
public void run() {
mGatt.discoverServices();
}
});
}
}
else if (newState == 0) {
mIsConnected = false;
}
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
}
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onServicesDiscovered status=" + status);
if (status == 0) {
if (gatt.getServices().size() == 0) {
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
mIsReconnecting = true;
mIsConnected = false;
gatt.disconnect();
mGatt = connectGatt(false);
}
else {
probeService(this);
}
}
}
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
mManager.HIDDeviceFeatureReport(getId(), characteristic.getValue());
}
finishCurrentGattOperation();
}
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic)) {
// Only register controller with the native side once it has been fully configured
if (!isRegistered()) {
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0);
setRegistered();
}
}
finishCurrentGattOperation();
}
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// Enable this for verbose logging of controller input reports
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) {
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
}
}
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//Log.v(TAG, "onDescriptorRead status=" + status);
}
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
if (chr.getUuid().equals(inputCharacteristic)) {
boolean hasWrittenInputDescriptor = true;
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
if (reportChr != null) {
Log.v(TAG, "Writing report characteristic to enter valve mode");
reportChr.setValue(enterValveMode);
gatt.writeCharacteristic(reportChr);
}
}
finishCurrentGattOperation();
}
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
}
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
//Log.v(TAG, "onReadRemoteRssi status=" + status);
}
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
//Log.v(TAG, "onMtuChanged status=" + status);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////// Public API
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
// Valve Corporation
final int VALVE_USB_VID = 0x28DE;
return VALVE_USB_VID;
}
@Override
public int getProductId() {
// We don't have an easy way to query from the Bluetooth device, but we know what it is
final int D0G_BLE2_PID = 0x1106;
return D0G_BLE2_PID;
}
@Override
public String getSerialNumber() {
// This will be read later via feature report by Steam
return "12345";
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
return "Valve Corporation";
}
@Override
public String getProductName() {
return "Steam Controller";
}
@Override
public UsbDevice getDevice() {
return null;
}
@Override
public boolean open() {
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
// We need to skip the first byte, as that doesn't go over the air
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(actual_report));
writeCharacteristic(reportCharacteristic, actual_report);
return report.length;
}
@Override
public int sendOutputReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
//Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report));
writeCharacteristic(reportCharacteristic, report);
return report.length;
}
@Override
public boolean getFeatureReport(byte[] report) {
if (!isRegistered()) {
Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return false;
}
//Log.v(TAG, "getFeatureReport");
readCharacteristic(reportCharacteristic);
return true;
}
@Override
public void close() {
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
@Override
public void shutdown() {
close();
BluetoothGatt g = mGatt;
if (g != null) {
g.disconnect();
g.close();
mGatt = null;
}
mManager = null;
mIsRegistered = false;
mIsConnected = false;
mOperations.clear();
}
}

View file

@ -1,698 +0,0 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.*;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class HIDDeviceManager {
private static final String TAG = "hidapi";
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
private static HIDDeviceManager sManager;
private static int sManagerRefCount = 0;
public static HIDDeviceManager acquire(Context context) {
if (sManagerRefCount == 0) {
sManager = new HIDDeviceManager(context);
}
++sManagerRefCount;
return sManager;
}
public static void release(HIDDeviceManager manager) {
if (manager == sManager) {
--sManagerRefCount;
if (sManagerRefCount == 0) {
sManager.close();
sManager = null;
}
}
}
private Context mContext;
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
private int mNextDeviceId = 0;
private SharedPreferences mSharedPreferences = null;
private boolean mIsChromebook = false;
private UsbManager mUsbManager;
private Handler mHandler;
private BluetoothManager mBluetoothManager;
private List<BluetoothDevice> mLastBluetoothDevices;
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceAttached(usbDevice);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceDetached(usbDevice);
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
}
}
};
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Bluetooth device was connected. If it was a Steam Controller, handle it
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device connected: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// Bluetooth device was disconnected, remove from controller manager (if any)
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device disconnected: " + device);
disconnectBluetoothDevice(device);
}
}
};
private HIDDeviceManager(final Context context) {
mContext = context;
HIDDeviceRegisterCallback();
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
mIsChromebook = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management");
// if (shouldClear) {
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
// spedit.clear();
// spedit.commit();
// }
// else
{
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
}
}
public Context getContext() {
return mContext;
}
public int getDeviceIDForIdentifier(String identifier) {
SharedPreferences.Editor spedit = mSharedPreferences.edit();
int result = mSharedPreferences.getInt(identifier, 0);
if (result == 0) {
result = mNextDeviceId++;
spedit.putInt("next_device_id", mNextDeviceId);
}
spedit.putInt(identifier, result);
spedit.commit();
return result;
}
private void initializeUSB() {
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
if (mUsbManager == null) {
return;
}
/*
// Logging
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
Log.i(TAG,"Path: " + device.getDeviceName());
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
Log.i(TAG,"Product: " + device.getProductName());
Log.i(TAG,"ID: " + device.getDeviceId());
Log.i(TAG,"Class: " + device.getDeviceClass());
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
Log.i(TAG,"Vendor ID " + device.getVendorId());
Log.i(TAG,"Product ID: " + device.getProductId());
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
Log.i(TAG,"---------------------------------------");
// Get interface details
for (int index = 0; index < device.getInterfaceCount(); index++) {
UsbInterface mUsbInterface = device.getInterface(index);
Log.i(TAG," ***** *****");
Log.i(TAG," Interface index: " + index);
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
// Get endpoint details
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
{
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
Log.i(TAG," ++++ ++++ ++++");
Log.i(TAG," Endpoint index: " + epi);
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
Log.i(TAG," Direction: " + mEndpoint.getDirection());
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
Log.i(TAG," Interval: " + mEndpoint.getInterval());
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
Log.i(TAG," Type: " + mEndpoint.getType());
}
}
}
Log.i(TAG," No more devices connected.");
*/
// Register for USB broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
mContext.registerReceiver(mUsbBroadcast, filter);
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
handleUsbDeviceAttached(usbDevice);
}
}
UsbManager getUSBManager() {
return mUsbManager;
}
private void shutdownUSB() {
try {
mContext.unregisterReceiver(mUsbBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
return true;
}
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
return true;
}
return false;
}
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB360_IFACE_SUBCLASS = 93;
final int XB360_IFACE_PROTOCOL = 1; // Wired
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // PDP
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126, Inc.
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2c22, // Qanba
0x2dc8, // 8BitDo
0x9886, // ASTRO Gaming
};
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB1_IFACE_SUBCLASS = 71;
final int XB1_IFACE_PROTOCOL = 208;
final int[] SUPPORTED_VENDORS = {
0x03f0, // HP
0x044f, // Thrustmaster
0x045e, // Microsoft
0x0738, // Mad Catz
0x0b05, // ASUS
0x0e6f, // PDP
0x0f0d, // Hori
0x10f5, // Turtle Beach
0x1532, // Razer Wildcat
0x20d6, // PowerA
0x24c6, // PowerA
0x2dc8, // 8BitDo
0x2e24, // Hyperkin
0x3537, // GameSir
};
if (usbInterface.getId() == 0 &&
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
connectHIDDeviceUSB(usbDevice);
}
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
List<Integer> devices = new ArrayList<Integer>();
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
devices.add(device.getId());
}
}
for (int id : devices) {
HIDDevice device = mDevicesById.get(id);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
boolean opened = false;
if (permission_granted) {
opened = device.open();
}
HIDDeviceOpenResult(device.getId(), opened);
}
}
}
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
synchronized (this) {
int interface_mask = 0;
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
// Check to see if we've already added this interface
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
int interface_id = usbInterface.getId();
if ((interface_mask & (1 << interface_id)) != 0) {
continue;
}
interface_mask |= (1 << interface_id);
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
int id = device.getId();
mDevicesById.put(id, device);
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
}
}
}
}
private void initializeBluetooth() {
Log.d(TAG, "Initializing Bluetooth");
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
return;
}
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
return;
}
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) {
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
return;
}
// Find bonded bluetooth controllers and create SteamControllers for them
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
// This device doesn't support Bluetooth.
return;
}
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
if (btAdapter == null) {
// This device has Bluetooth support in the codebase, but has no available adapters.
return;
}
// Get our bonded devices.
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
Log.d(TAG, "Bluetooth device available: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// NOTE: These don't work on Chromebooks, to my undying dismay.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
mContext.registerReceiver(mBluetoothBroadcast, filter);
if (mIsChromebook) {
mHandler = new Handler(Looper.getMainLooper());
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
// final HIDDeviceManager finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.chromebookConnectionHandler();
// }
// }, 5000);
}
}
private void shutdownBluetooth() {
try {
mContext.unregisterReceiver(mBluetoothBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
// This function provides a sort of dummy version of that, watching for changes in the
// connected devices and attempting to add controllers as things change.
public void chromebookConnectionHandler() {
if (!mIsChromebook) {
return;
}
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
connected.add(bluetoothDevice);
}
}
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
if (!currentConnected.contains(bluetoothDevice)) {
disconnected.add(bluetoothDevice);
}
}
mLastBluetoothDevices = currentConnected;
for (BluetoothDevice bluetoothDevice : disconnected) {
disconnectBluetoothDevice(bluetoothDevice);
}
for (BluetoothDevice bluetoothDevice : connected) {
connectBluetoothDevice(bluetoothDevice);
}
final HIDDeviceManager finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.chromebookConnectionHandler();
}
}, 10000);
}
public boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
synchronized (this) {
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
device.reconnect();
return false;
}
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
int id = device.getId();
mBluetoothDevices.put(bluetoothDevice, device);
mDevicesById.put(id, device);
// The Steam Controller will mark itself connected once initialization is complete
}
return true;
}
public void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
synchronized (this) {
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
if (device == null)
return;
int id = device.getId();
mBluetoothDevices.remove(bluetoothDevice);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
public boolean isSteamController(BluetoothDevice bluetoothDevice) {
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
if (bluetoothDevice == null) {
return false;
}
// If the device has no local name, we really don't want to try an equality check against it.
if (bluetoothDevice.getName() == null) {
return false;
}
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
}
private void close() {
shutdownUSB();
shutdownBluetooth();
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.shutdown();
}
mDevicesById.clear();
mBluetoothDevices.clear();
HIDDeviceReleaseCallback();
}
}
public void setFrozen(boolean frozen) {
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.setFrozen(frozen);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private HIDDevice getDevice(int id) {
synchronized (this) {
HIDDevice result = mDevicesById.get(id);
if (result == null) {
Log.v(TAG, "No device for id: " + id);
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
}
return result;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////// JNI interface functions
//////////////////////////////////////////////////////////////////////////////////////////////////////
public boolean initialize(boolean usb, boolean bluetooth) {
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
if (usb) {
initializeUSB();
}
if (bluetooth) {
initializeBluetooth();
}
return true;
}
public boolean openDevice(int deviceID) {
Log.v(TAG, "openDevice deviceID=" + deviceID);
HIDDevice device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
// Look to see if this is a USB device and we have permission to access it
UsbDevice usbDevice = device.getDevice();
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
HIDDeviceOpenPending(deviceID);
try {
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
int flags;
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
flags = FLAG_MUTABLE;
} else {
flags = 0;
}
if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) {
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
intent.setPackage(mContext.getPackageName());
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
} else {
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), flags));
}
} catch (Exception e) {
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
HIDDeviceOpenResult(deviceID, false);
}
return false;
}
try {
return device.open();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public int sendOutputReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendOutputReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public int sendFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.sendFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
public boolean getFeatureReport(int deviceID, byte[] report) {
try {
//Log.v(TAG, "getFeatureReport deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
return device.getFeatureReport(report);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
public void closeDevice(int deviceID) {
try {
Log.v(TAG, "closeDevice deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return;
}
device.close();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////// Native methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
private native void HIDDeviceRegisterCallback();
private native void HIDDeviceReleaseCallback();
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol);
native void HIDDeviceOpenPending(int deviceID);
native void HIDDeviceOpenResult(int deviceID, boolean opened);
native void HIDDeviceDisconnected(int deviceID);
native void HIDDeviceInputReport(int deviceID, byte[] report);
native void HIDDeviceFeatureReport(int deviceID, byte[] report);
}

View file

@ -1,309 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.*;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
class HIDDeviceUSB implements HIDDevice {
private static final String TAG = "hidapi";
protected HIDDeviceManager mManager;
protected UsbDevice mDevice;
protected int mInterfaceIndex;
protected int mInterface;
protected int mDeviceId;
protected UsbDeviceConnection mConnection;
protected UsbEndpoint mInputEndpoint;
protected UsbEndpoint mOutputEndpoint;
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
mDevice = usbDevice;
mInterfaceIndex = interface_index;
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
}
public String getIdentifier() {
return String.format("%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
return mDevice.getVendorId();
}
@Override
public int getProductId() {
return mDevice.getProductId();
}
@Override
public String getSerialNumber() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
try {
result = mDevice.getSerialNumber();
}
catch (SecurityException exception) {
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
}
}
if (result == null) {
result = "";
}
return result;
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
result = mDevice.getManufacturerName();
}
if (result == null) {
result = String.format("%x", getVendorId());
}
return result;
}
@Override
public String getProductName() {
String result = null;
if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) {
result = mDevice.getProductName();
}
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
public String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present
if (mInputEndpoint == null || mOutputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int sendFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "sendFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
}
@Override
public int sendOutputReport(byte[] report) {
int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (r != report.length) {
Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName());
}
return r;
}
@Override
public boolean getFeatureReport(byte[] report) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceFeatureReport(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
mConnection.close();
mConnection = null;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}

View file

@ -1,90 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import java.lang.Class;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
public static void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
public static void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
public static void setContext(Context context) {
SDLAudioManager.setContext(context);
mContext = context;
}
public static Context getContext() {
return mContext;
}
public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
loadLibrary(libraryName, mContext);
}
public static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
if (libraryName == null) {
throw new NullPointerException("No library name provided.");
}
try {
// Let's see if we have ReLinker available in the project. This is necessary for
// some projects that have huge numbers of local libraries bundled, and thus may
// trip a bug in Android's native library loader which ReLinker works around. (If
// loadLibrary works properly, ReLinker will simply use the normal Android method
// internally.)
//
// To use ReLinker, just add it as a dependency. For more information, see
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
//
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
// they've changed during updates.
Method forceMethod = relinkClass.getDeclaredMethod("force");
Object relinkInstance = forceMethod.invoke(null);
Class<?> relinkInstanceClass = relinkInstance.getClass();
// Actually load the library!
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Context mContext;
}

File diff suppressed because it is too large Load diff

View file

@ -1,514 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
public class SDLAudioManager {
protected static final String TAG = "SDLAudio";
protected static AudioTrack mAudioTrack;
protected static AudioRecord mAudioRecord;
protected static Context mContext;
private static final int[] NO_DEVICES = {};
private static AudioDeviceCallback mAudioDeviceCallback;
public static void initialize() {
mAudioTrack = null;
mAudioRecord = null;
mAudioDeviceCallback = null;
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
{
mAudioDeviceCallback = new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
}
};
}
}
public static void setContext(Context context) {
mContext = context;
if (context != null) {
registerAudioDeviceCallback();
}
}
public static void release(Context context) {
unregisterAudioDeviceCallback(context);
}
// Audio
protected static String getAudioFormatString(int audioFormat) {
switch (audioFormat) {
case AudioFormat.ENCODING_PCM_8BIT:
return "8-bit";
case AudioFormat.ENCODING_PCM_16BIT:
return "16-bit";
case AudioFormat.ENCODING_PCM_FLOAT:
return "float";
default:
return Integer.toString(audioFormat);
}
}
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
int channelConfig;
int sampleSize;
int frameSize;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
/* On older devices let's use known good settings */
if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
if (desiredChannels > 2) {
desiredChannels = 2;
}
}
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) {
if (sampleRate < 8000) {
sampleRate = 8000;
} else if (sampleRate > 48000) {
sampleRate = 48000;
}
}
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */);
if (Build.VERSION.SDK_INT < minSDKVersion) {
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
}
}
switch (audioFormat)
{
case AudioFormat.ENCODING_PCM_8BIT:
sampleSize = 1;
break;
case AudioFormat.ENCODING_PCM_16BIT:
sampleSize = 2;
break;
case AudioFormat.ENCODING_PCM_FLOAT:
sampleSize = 4;
break;
default:
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
sampleSize = 2;
break;
}
if (isCapture) {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_IN_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
}
} else {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 3:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 5:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
case 7:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
break;
case 8:
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else {
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
desiredChannels = 6;
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
}
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
}
/*
Log.v(TAG, "Speaker configuration (and order of channels):");
if ((channelConfig & 0x00000004) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
}
if ((channelConfig & 0x00000008) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
}
if ((channelConfig & 0x00000010) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
}
if ((channelConfig & 0x00000020) != 0) {
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
}
if ((channelConfig & 0x00000040) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
}
if ((channelConfig & 0x00000080) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
}
if ((channelConfig & 0x00000100) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
}
if ((channelConfig & 0x00000200) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
}
if ((channelConfig & 0x00000400) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
}
if ((channelConfig & 0x00000800) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
}
if ((channelConfig & 0x00001000) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
}
*/
}
frameSize = (sampleSize * desiredChannels);
// Let the user pick a larger buffer if they really want -- but ye
// gods they probably shouldn't, the minimums are horrifyingly high
// latency already
int minBufferSize;
if (isCapture) {
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
} else {
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
}
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
int[] results = new int[4];
if (isCapture) {
if (mAudioRecord == null) {
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
channelConfig, audioFormat, desiredFrames * frameSize);
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "Failed during initialization of AudioRecord");
mAudioRecord.release();
mAudioRecord = null;
return null;
}
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId));
}
mAudioRecord.startRecording();
}
results[0] = mAudioRecord.getSampleRate();
results[1] = mAudioRecord.getAudioFormat();
results[2] = mAudioRecord.getChannelCount();
} else {
if (mAudioTrack == null) {
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
/* Try again, with safer values */
Log.e(TAG, "Failed during initialization of Audio Track");
mAudioTrack.release();
mAudioTrack = null;
return null;
}
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId));
}
mAudioTrack.play();
}
results[0] = mAudioTrack.getSampleRate();
results[1] = mAudioTrack.getAudioFormat();
results[2] = mAudioTrack.getChannelCount();
}
results[3] = desiredFrames;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
return results;
}
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
.findFirst()
.orElse(null);
} else {
return null;
}
}
private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
.findFirst()
.orElse(null);
} else {
return null;
}
}
private static void registerAudioDeviceCallback() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
}
}
private static void unregisterAudioDeviceCallback(Context context) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] getAudioOutputDevices() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
} else {
return NO_DEVICES;
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] getAudioInputDevices() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
} else {
return NO_DEVICES;
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteFloatBuffer(float[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(float)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteShortBuffer(short[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(short)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteByteBuffer(byte[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length; ) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(byte)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
}
/** This method is called by SDL using JNI. */
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return 0;
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static void audioClose() {
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
}
/** This method is called by SDL using JNI. */
public static void captureClose() {
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
}
/** This method is called by SDL using JNI. */
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
try {
/* Set thread name */
if (iscapture) {
Thread.currentThread().setName("SDLAudioC" + device_id);
} else {
Thread.currentThread().setName("SDLAudioP" + device_id);
}
/* Set thread priority */
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
}
public static native int nativeSetupJNI();
public static native void removeAudioDevice(boolean isCapture, int deviceId);
public static native void addAudioDevice(boolean isCapture, int deviceId);
}

View file

@ -1,856 +0,0 @@
package org.libsdl.app;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
public class SDLControllerManager
{
public static native int nativeSetupJNI();
public static native int nativeAddJoystick(int device_id, String name, String desc,
int vendor_id, int product_id,
boolean is_accelerometer, int button_mask,
int naxes, int axis_mask, int nhats, int nballs);
public static native int nativeRemoveJoystick(int device_id);
public static native int nativeAddHaptic(int device_id, String name);
public static native int nativeRemoveHaptic(int device_id);
public static native int onNativePadDown(int device_id, int keycode);
public static native int onNativePadUp(int device_id, int keycode);
public static native void onNativeJoy(int device_id, int axis,
float value);
public static native void onNativeHat(int device_id, int hat_id,
int x, int y);
protected static SDLJoystickHandler mJoystickHandler;
protected static SDLHapticHandler mHapticHandler;
private static final String TAG = "SDLControllerManager";
public static void initialize() {
if (mJoystickHandler == null) {
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
mJoystickHandler = new SDLJoystickHandler_API19();
} else {
mJoystickHandler = new SDLJoystickHandler_API16();
}
}
if (mHapticHandler == null) {
if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
mHapticHandler = new SDLHapticHandler_API26();
} else {
mHapticHandler = new SDLHapticHandler();
}
}
}
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
public static boolean handleJoystickMotionEvent(MotionEvent event) {
return mJoystickHandler.handleMotionEvent(event);
}
/**
* This method is called by SDL using JNI.
*/
public static void pollInputDevices() {
mJoystickHandler.pollInputDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void pollHapticDevices() {
mHapticHandler.pollHapticDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticRun(int device_id, float intensity, int length) {
mHapticHandler.run(device_id, intensity, length);
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticStop(int device_id)
{
mHapticHandler.stop(device_id);
}
// Check if a given device is considered a possible SDL joystick
public static boolean isDeviceSDLJoystick(int deviceId) {
InputDevice device = InputDevice.getDevice(deviceId);
// We cannot use InputDevice.isVirtual before API 16, so let's accept
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
if ((device == null) || (deviceId < 0)) {
return false;
}
int sources = device.getSources();
/* This is called for every button press, so let's not spam the logs */
/*
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
}
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
}
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
}
*/
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
);
}
}
class SDLJoystickHandler {
/**
* Handles given MotionEvent.
* @param event the event to be handled.
* @return if given event was processed.
*/
public boolean handleMotionEvent(MotionEvent event) {
return false;
}
/**
* Handles adding and removing of input devices.
*/
public void pollInputDevices() {
}
}
/* Actual joystick functionality available for API >= 12 devices */
class SDLJoystickHandler_API16 extends SDLJoystickHandler {
static class SDLJoystick {
public int device_id;
public String name;
public String desc;
public ArrayList<InputDevice.MotionRange> axes;
public ArrayList<InputDevice.MotionRange> hats;
}
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
@Override
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
int arg0Axis = arg0.getAxis();
int arg1Axis = arg1.getAxis();
if (arg0Axis == MotionEvent.AXIS_GAS) {
arg0Axis = MotionEvent.AXIS_BRAKE;
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
arg0Axis = MotionEvent.AXIS_GAS;
}
if (arg1Axis == MotionEvent.AXIS_GAS) {
arg1Axis = MotionEvent.AXIS_BRAKE;
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
arg1Axis = MotionEvent.AXIS_GAS;
}
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
// This is because the usual pairing are:
// - AXIS_X + AXIS_Y (left stick).
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
// This sorts the axes in the above order, which tends to be correct
// for Xbox-ish game pads that have the right stick on RX/RY and the
// triggers on Z/RZ.
//
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
//
// References:
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
if (arg0Axis == MotionEvent.AXIS_Z) {
arg0Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
--arg0Axis;
}
if (arg1Axis == MotionEvent.AXIS_Z) {
arg1Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
--arg1Axis;
}
return arg0Axis - arg1Axis;
}
}
private final ArrayList<SDLJoystick> mJoysticks;
public SDLJoystickHandler_API16() {
mJoysticks = new ArrayList<SDLJoystick>();
}
@Override
public void pollInputDevices() {
int[] deviceIds = InputDevice.getDeviceIds();
for (int device_id : deviceIds) {
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
SDLJoystick joystick = getJoystick(device_id);
if (joystick == null) {
InputDevice joystickDevice = InputDevice.getDevice(device_id);
joystick = new SDLJoystick();
joystick.device_id = device_id;
joystick.name = joystickDevice.getName();
joystick.desc = getJoystickDescriptor(joystickDevice);
joystick.axes = new ArrayList<InputDevice.MotionRange>();
joystick.hats = new ArrayList<InputDevice.MotionRange>();
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
for (InputDevice.MotionRange range : ranges) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
joystick.hats.add(range);
} else {
joystick.axes.add(range);
}
}
}
mJoysticks.add(joystick);
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
getVendorId(joystickDevice), getProductId(joystickDevice), false,
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLJoystick joystick : mJoysticks) {
int device_id = joystick.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveJoystick(device_id);
for (int i = 0; i < mJoysticks.size(); i++) {
if (mJoysticks.get(i).device_id == device_id) {
mJoysticks.remove(i);
break;
}
}
}
}
}
protected SDLJoystick getJoystick(int device_id) {
for (SDLJoystick joystick : mJoysticks) {
if (joystick.device_id == device_id) {
return joystick;
}
}
return null;
}
@Override
public boolean handleMotionEvent(MotionEvent event) {
int actionPointerIndex = event.getActionIndex();
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_MOVE) {
SDLJoystick joystick = getJoystick(event.getDeviceId());
if (joystick != null) {
for (int i = 0; i < joystick.axes.size(); i++) {
InputDevice.MotionRange range = joystick.axes.get(i);
/* Normalize the value to -1...1 */
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
}
for (int i = 0; i < joystick.hats.size() / 2; i++) {
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
}
}
}
return true;
}
public String getJoystickDescriptor(InputDevice joystickDevice) {
String desc = joystickDevice.getDescriptor();
if (desc != null && !desc.isEmpty()) {
return desc;
}
return joystickDevice.getName();
}
public int getProductId(InputDevice joystickDevice) {
return 0;
}
public int getVendorId(InputDevice joystickDevice) {
return 0;
}
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
return -1;
}
public int getButtonMask(InputDevice joystickDevice) {
return -1;
}
}
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
@Override
public int getProductId(InputDevice joystickDevice) {
return joystickDevice.getProductId();
}
@Override
public int getVendorId(InputDevice joystickDevice) {
return joystickDevice.getVendorId();
}
@Override
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
// For compatibility, keep computing the axis mask like before,
// only really distinguishing 2, 4 and 6 axes.
int axis_mask = 0;
if (ranges.size() >= 2) {
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
axis_mask |= 0x0003;
}
if (ranges.size() >= 4) {
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
axis_mask |= 0x000c;
}
if (ranges.size() >= 6) {
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
axis_mask |= 0x0030;
}
// Also add an indicator bit for whether the sorting order has changed.
// This serves to disable outdated gamecontrollerdb.txt mappings.
boolean have_z = false;
boolean have_past_z_before_rz = false;
for (InputDevice.MotionRange range : ranges) {
int axis = range.getAxis();
if (axis == MotionEvent.AXIS_Z) {
have_z = true;
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
have_past_z_before_rz = true;
}
}
if (have_z && have_past_z_before_rz) {
// If both these exist, the compare() function changed sorting order.
// Set a bit to indicate this fact.
axis_mask |= 0x8000;
}
return axis_mask;
}
@Override
public int getButtonMask(InputDevice joystickDevice) {
int button_mask = 0;
int[] keys = new int[] {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_MENU,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER,
// These don't map into any SDL controller buttons directly
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_Z,
KeyEvent.KEYCODE_BUTTON_1,
KeyEvent.KEYCODE_BUTTON_2,
KeyEvent.KEYCODE_BUTTON_3,
KeyEvent.KEYCODE_BUTTON_4,
KeyEvent.KEYCODE_BUTTON_5,
KeyEvent.KEYCODE_BUTTON_6,
KeyEvent.KEYCODE_BUTTON_7,
KeyEvent.KEYCODE_BUTTON_8,
KeyEvent.KEYCODE_BUTTON_9,
KeyEvent.KEYCODE_BUTTON_10,
KeyEvent.KEYCODE_BUTTON_11,
KeyEvent.KEYCODE_BUTTON_12,
KeyEvent.KEYCODE_BUTTON_13,
KeyEvent.KEYCODE_BUTTON_14,
KeyEvent.KEYCODE_BUTTON_15,
KeyEvent.KEYCODE_BUTTON_16,
};
int[] masks = new int[] {
(1 << 0), // A -> A
(1 << 1), // B -> B
(1 << 2), // X -> X
(1 << 3), // Y -> Y
(1 << 4), // BACK -> BACK
(1 << 6), // MENU -> START
(1 << 5), // MODE -> GUIDE
(1 << 6), // START -> START
(1 << 7), // THUMBL -> LEFTSTICK
(1 << 8), // THUMBR -> RIGHTSTICK
(1 << 9), // L1 -> LEFTSHOULDER
(1 << 10), // R1 -> RIGHTSHOULDER
(1 << 11), // DPAD_UP -> DPAD_UP
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
(1 << 4), // SELECT -> BACK
(1 << 0), // DPAD_CENTER -> A
(1 << 15), // L2 -> ??
(1 << 16), // R2 -> ??
(1 << 17), // C -> ??
(1 << 18), // Z -> ??
(1 << 20), // 1 -> ??
(1 << 21), // 2 -> ??
(1 << 22), // 3 -> ??
(1 << 23), // 4 -> ??
(1 << 24), // 5 -> ??
(1 << 25), // 6 -> ??
(1 << 26), // 7 -> ??
(1 << 27), // 8 -> ??
(1 << 28), // 9 -> ??
(1 << 29), // 10 -> ??
(1 << 30), // 11 -> ??
(1 << 31), // 12 -> ??
// We're out of room...
0xFFFFFFFF, // 13 -> ??
0xFFFFFFFF, // 14 -> ??
0xFFFFFFFF, // 15 -> ??
0xFFFFFFFF, // 16 -> ??
};
boolean[] has_keys = joystickDevice.hasKeys(keys);
for (int i = 0; i < keys.length; ++i) {
if (has_keys[i]) {
button_mask |= masks[i];
}
}
return button_mask;
}
}
class SDLHapticHandler_API26 extends SDLHapticHandler {
@Override
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
if (intensity == 0.0f) {
stop(device_id);
return;
}
int vibeValue = Math.round(intensity * 255);
if (vibeValue > 255) {
vibeValue = 255;
}
if (vibeValue < 1) {
stop(device_id);
return;
}
try {
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
}
catch (Exception e) {
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
// something went horribly wrong with the Android 8.0 APIs.
haptic.vib.vibrate(length);
}
}
}
}
class SDLHapticHandler {
static class SDLHaptic {
public int device_id;
public String name;
public Vibrator vib;
}
private final ArrayList<SDLHaptic> mHaptics;
public SDLHapticHandler() {
mHaptics = new ArrayList<SDLHaptic>();
}
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.vibrate(length);
}
}
public void stop(int device_id) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.cancel();
}
}
public void pollHapticDevices() {
final int deviceId_VIBRATOR_SERVICE = 999999;
boolean hasVibratorService = false;
int[] deviceIds = InputDevice.getDeviceIds();
// It helps processing the device ids in reverse order
// For example, in the case of the XBox 360 wireless dongle,
// so the first controller seen by SDL matches what the receiver
// considers to be the first controller
for (int i = deviceIds.length - 1; i > -1; i--) {
SDLHaptic haptic = getHaptic(deviceIds[i]);
if (haptic == null) {
InputDevice device = InputDevice.getDevice(deviceIds[i]);
Vibrator vib = device.getVibrator();
if (vib != null) {
if (vib.hasVibrator()) {
haptic = new SDLHaptic();
haptic.device_id = deviceIds[i];
haptic.name = device.getName();
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
}
/* Check VIBRATOR_SERVICE */
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null) {
hasVibratorService = vib.hasVibrator();
if (hasVibratorService) {
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
if (haptic == null) {
haptic = new SDLHaptic();
haptic.device_id = deviceId_VIBRATOR_SERVICE;
haptic.name = "VIBRATOR_SERVICE";
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLHaptic haptic : mHaptics) {
int device_id = haptic.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
} // else: don't remove the vibrator if it is still present
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveHaptic(device_id);
for (int i = 0; i < mHaptics.size(); i++) {
if (mHaptics.get(i).device_id == device_id) {
mHaptics.remove(i);
break;
}
}
}
}
}
protected SDLHaptic getHaptic(int device_id) {
for (SDLHaptic haptic : mHaptics) {
if (haptic.device_id == device_id) {
return haptic;
}
}
return null;
}
}
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
// Generic Motion (mouse hover, joystick...) events go here
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
public boolean supportsRelativeMouse() {
return false;
}
public boolean inRelativeMode() {
return false;
}
public boolean setRelativeMouseEnabled(boolean enabled) {
return false;
}
public void reclaimRelativeMouseModeIfNeeded()
{
}
public float getEventX(MotionEvent event) {
return event.getX(0);
}
public float getEventY(MotionEvent event) {
return event.getY(0);
}
}
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
// Handle relative mouse mode
if (mRelativeModeEnabled) {
if (event.getSource() == InputDevice.SOURCE_MOUSE) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_HOVER_MOVE) {
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
}
}
}
// Event was not managed, call SDLGenericMotionListener_API12 method
return super.onGenericMotion(v, event);
}
@Override
public boolean supportsRelativeMouse() {
return true;
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
mRelativeModeEnabled = enabled;
return true;
}
@Override
public float getEventX(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
} else {
return event.getX(0);
}
}
@Override
public float getEventY(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
} else {
return event.getY(0);
}
}
}
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
// DeX desktop mouse cursor is a separate non-standard input type.
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
case InputDevice.SOURCE_MOUSE_RELATIVE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
@Override
public boolean supportsRelativeMouse() {
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
if (enabled) {
SDLActivity.getContentView().requestPointerCapture();
} else {
SDLActivity.getContentView().releasePointerCapture();
}
mRelativeModeEnabled = enabled;
return true;
} else {
return false;
}
}
@Override
public void reclaimRelativeMouseModeIfNeeded()
{
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
SDLActivity.getContentView().requestPointerCapture();
}
}
@Override
public float getEventX(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getX(0);
}
@Override
public float getEventY(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getY(0);
}
}

View file

@ -1,405 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
/**
SDLSurface. This is what we draw on, so we need to know when it's created
in order to do anything useful.
Because of this, that's where we set up the SDL thread
*/
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
// Keep track of the surface size to normalize touch events
protected float mWidth, mHeight;
// Is SurfaceView ready for rendering
public boolean mIsSurfaceReady;
// Startup
public SDLSurface(Context context) {
super(context);
getHolder().addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
setOnGenericMotionListener(SDLActivity.getMotionListener());
// Some arbitrary defaults to avoid a potential division by zero
mWidth = 1.0f;
mHeight = 1.0f;
mIsSurfaceReady = false;
}
public void handlePause() {
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
}
public void handleResume() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
}
public Surface getNativeSurface() {
return getHolder().getSurface();
}
// Called when we have a valid drawing surface
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
SDLActivity.onNativeSurfaceCreated();
}
// Called when we lose the surface
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
SDLActivity.handleNativeState();
mIsSurfaceReady = false;
SDLActivity.onNativeSurfaceDestroyed();
}
// Called when the surface is resized
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
Log.v("SDL", "surfaceChanged()");
if (SDLActivity.mSingleton == null) {
return;
}
mWidth = width;
mHeight = height;
int nDeviceWidth = width;
int nDeviceHeight = height;
try
{
if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) {
DisplayMetrics realMetrics = new DisplayMetrics();
mDisplay.getRealMetrics( realMetrics );
nDeviceWidth = realMetrics.widthPixels;
nDeviceHeight = realMetrics.heightPixels;
}
} catch(Exception ignored) {
}
synchronized(SDLActivity.getContext()) {
// In case we're waiting on a size change after going fullscreen, send a notification.
SDLActivity.getContext().notifyAll();
}
Log.v("SDL", "Window size: " + width + "x" + height);
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
SDLActivity.onNativeResize();
// Prevent a screen distortion glitch,
// for instance when the device is in Landscape and a Portrait App is resumed.
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
}
// Special Patch for Square Resolution: Black Berry Passport
if (skip) {
double min = Math.min(mWidth, mHeight);
double max = Math.max(mWidth, mHeight);
if (max / min < 1.20) {
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
skip = false;
}
}
// Don't skip in MultiWindow.
if (skip) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
Log.v("SDL", "Don't skip in Multi-Window");
skip = false;
}
}
}
if (skip) {
Log.v("SDL", "Skip .. Surface is not ready.");
mIsSurfaceReady = false;
return;
}
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
SDLActivity.onNativeSurfaceChanged();
/* Surface is ready */
mIsSurfaceReady = true;
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
/* Ref: http://developer.android.com/training/gestures/multi.html */
int touchDevId = event.getDeviceId();
final int pointerCount = event.getPointerCount();
int action = event.getActionMasked();
int pointerFingerId;
int i = -1;
float x,y,p;
/*
* Prevent id to be -1, since it's used in SDL internal for synthetic events
* Appears when using Android emulator, eg:
* adb shell input mouse tap 100 100
* adb shell input touchscreen tap 100 100
*/
if (touchDevId < 0) {
touchDevId -= 1;
}
// 12290 = Samsung DeX mode desktop mouse
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
// 0x2 = SOURCE_CLASS_POINTER
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
int mouseButton = 1;
try {
Object object = event.getClass().getMethod("getButtonState").invoke(event);
if (object != null) {
mouseButton = (Integer) object;
}
} catch(Exception ignored) {
}
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
// if we are. We'll leverage our existing mouse motion listener
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
x = motionListener.getEventX(event);
y = motionListener.getEventY(event);
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
} else {
switch(action) {
case MotionEvent.ACTION_MOVE:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
// Primary pointer up/down, the index is always zero
i = 0;
/* fallthrough */
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
// Non primary pointer up/down
if (i == -1) {
i = event.getActionIndex();
}
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
break;
case MotionEvent.ACTION_CANCEL:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
}
break;
default:
break;
}
}
return true;
}
// Sensor events
public void enableSensor(int sensortype, boolean enabled) {
// TODO: This uses getDefaultSensor - what if we have >1 accels?
if (enabled) {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(sensortype),
SensorManager.SENSOR_DELAY_GAME, null);
} else {
mSensorManager.unregisterListener(this,
mSensorManager.getDefaultSensor(sensortype));
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
// We thus should check here.
int newOrientation;
float x, y;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_90:
x = -event.values[1];
y = event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
break;
case Surface.ROTATION_270:
x = event.values[1];
y = -event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
break;
case Surface.ROTATION_180:
x = -event.values[0];
y = -event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
break;
case Surface.ROTATION_0:
default:
x = event.values[0];
y = event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
break;
}
if (newOrientation != SDLActivity.mCurrentOrientation) {
SDLActivity.mCurrentOrientation = newOrientation;
SDLActivity.onNativeOrientationChanged(newOrientation);
}
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
y / SensorManager.GRAVITY_EARTH,
event.values[2] / SensorManager.GRAVITY_EARTH);
}
}
// Captured pointer events for API 26.
public boolean onCapturedPointerEvent(MotionEvent event)
{
int action = event.getActionMasked();
float x, y;
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_BUTTON_RELEASE:
// Change our action value to what SDL's code expects.
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
action = MotionEvent.ACTION_DOWN;
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
action = MotionEvent.ACTION_UP;
}
x = event.getX(0);
y = event.getY(0);
int button = event.getButtonState();
SDLActivity.onNativeMouse(button, action, x, y, true);
return true;
}
return false;
}
}

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="loading">Lädt…</string>
<string name="unzip_notification_title">Luanti lädt</string>
<string name="unzip_notification_description">Weniger als 1 Minute…</string>
<string name="ime_dialog_done">Fertig</string>
<string name="no_web_browser">Kein Web-Browser gefunden</string>
<string name="notification_channel_name">Allgemeine Benachrichtigung</string>
<string name="notification_channel_description">Benachrichtigungen von Luanti</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="unzip_notification_description">Menos de 1 minuto…</string>
<string name="ime_dialog_done">Hecho</string>
<string name="no_web_browser">No se encontró ningún navegador web</string>
<string name="loading">Cargando…</string>
<string name="notification_channel_name">Notificación General</string>
<string name="label">Luanti</string>
<string name="notification_channel_description">Notificaciones de Luanti</string>
<string name="unzip_notification_title">Cargando Luanti</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_web_browser">No se encontró ningún navegador web</string>
<string name="loading">Cargando…</string>
<string name="notification_channel_description">Notificaciones de Luanti</string>
<string name="unzip_notification_description">Menos de 1 minuto…</string>
<string name="ime_dialog_done">Hecho</string>
<string name="label">Luanti</string>
<string name="unzip_notification_title">Cargando Luanti</string>
<string name="notification_channel_name">Notificación General</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="unzip_notification_description">کمتر از 1 دقیقه…</string>
<string name="loading">در حال بارگذاری…</string>
<string name="ime_dialog_done">انجام شد</string>
<string name="label">لوآنتی</string>
<string name="unzip_notification_title">در حال بارگذاری لوآنتی</string>
<string name="notification_channel_name">نوتیفیکیشن عمومی</string>
<string name="notification_channel_description">نوتیفیکیشن از لوآنتی</string>
<string name="no_web_browser">هیچ مرورگری یافت نشد</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="loading">Chargement…</string>
<string name="notification_channel_name">Notification générale</string>
<string name="notification_channel_description">Notifications de Luanti</string>
<string name="unzip_notification_title">Chargement de Luanti</string>
<string name="unzip_notification_description">Moins d\'une minute…</string>
<string name="ime_dialog_done">Terminé</string>
<string name="no_web_browser">Aucun navigateur web trouvé</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="loading">Cargando…</string>
<string name="notification_channel_name">Notificación xeral</string>
<string name="notification_channel_description">Notificacións de Luanti</string>
<string name="unzip_notification_title">Cargando Luanti</string>
<string name="unzip_notification_description">Menos de 1 minuto…</string>
<string name="ime_dialog_done">Feito</string>
<string name="no_web_browser">Non se atopou ningún navegador web</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="loading">Betöltés…</string>
<string name="notification_channel_name">Általános értesítés</string>
<string name="notification_channel_description">Értesítések a Luanti-től</string>
<string name="unzip_notification_title">Luanti betöltése…</string>
<string name="ime_dialog_done">Kész</string>
<string name="no_web_browser">Nem található webböngésző</string>
<string name="label">Luanti</string>
<string name="unzip_notification_description">Kevesebb, mint 1 perc…</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="no_web_browser">Tidak ditemukan peramban web</string>
<string name="ime_dialog_done">Selesai</string>
<string name="label">Luanti</string>
<string name="loading">Memuat…</string>
<string name="notification_channel_name">Pemberitahuan umum</string>
<string name="unzip_notification_description">Kurang dari 1 menit…</string>
<string name="notification_channel_description">Pemberitahuan dari Luanti</string>
<string name="unzip_notification_title">Memuat Luanti…</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="notification_channel_description">Pemberitahuan dari Luanti</string>
<string name="unzip_notification_title">Memuatkan Luanti…</string>
<string name="unzip_notification_description">Kurang dari 1 minit…</string>
<string name="no_web_browser">Tiada pelayar sesawang dijumpai</string>
<string name="loading">Memuatkan…</string>
<string name="notification_channel_name">Pemberitahuan umum</string>
<string name="ime_dialog_done">Selesai</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="loading">Laster inn …</string>
<string name="notification_channel_name">Generell merknad</string>
<string name="notification_channel_description">Merknader fra Luanti</string>
<string name="unzip_notification_description">Mindre enn ett minutt …</string>
<string name="ime_dialog_done">Ferdig</string>
<string name="no_web_browser">Fant ingen nettleser</string>
<string name="unzip_notification_title">Laster inn Luanti …</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="unzip_notification_title">Загрузка Luanti</string>
<string name="unzip_notification_description">Меньше чам за 1 минуту…</string>
<string name="ime_dialog_done">Готово</string>
<string name="label">Luаnti</string>
<string name="notification_channel_description">Уведомления от Luanti</string>
<string name="notification_channel_name">Основные уведомления</string>
<string name="loading">Загрузка…</string>
<string name="no_web_browser">Не найдено веб-браузера</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="loading">Laddar…</string>
<string name="unzip_notification_description">Mindre än 1 minut…</string>
<string name="ime_dialog_done">Färdig</string>
<string name="no_web_browser">Ingen webbläsare kunde hittas</string>
<string name="notification_channel_name">Generell notis</string>
<string name="notification_channel_description">Notiser från Luanti</string>
<string name="unzip_notification_title">Laddar Luanti</string>
</resources>

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="no_web_browser">Браузерів не знайдено</string>
<string name="loading">Завантаження…</string>
<string name="notification_channel_name">Загальні сповіщення</string>
<string name="unzip_notification_title">Luanti завантажується</string>
<string name="unzip_notification_description">Менше за 1 хвилину…</string>
<string name="ime_dialog_done">Готово</string>
<string name="notification_channel_description">Сповіщення від Luanti</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="loading">加载中…</string>
<string name="notification_channel_name">一般通知</string>
<string name="notification_channel_description">Luanti 的通知</string>
<string name="unzip_notification_title">加载 Luanti 中</string>
<string name="unzip_notification_description">不到1分钟…</string>
<string name="ime_dialog_done">完成</string>
<string name="no_web_browser">未找到网页浏览器</string>
</resources>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="loading">載入中…</string>
<string name="notification_channel_name">一般通知</string>
<string name="notification_channel_description">Luanti 的通知</string>
<string name="unzip_notification_description">不到1分鐘…</string>
<string name="ime_dialog_done">完畢</string>
<string name="no_web_browser">未找到任何網頁瀏覽器</string>
<string name="label">Luanti</string>
<string name="unzip_notification_title">載入Luanti中</string>
</resources>

View file

@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="label">Luanti</string>
<string name="label">Minetest</string>
<string name="loading">Loading&#8230;</string>
<string name="notification_channel_name">General notification</string>
<string name="notification_channel_description">Notifications from Luanti</string>
<string name="unzip_notification_title">Loading Luanti</string>
<string name="unzip_notification_description">Less than 1 minute&#8230;</string>
<string name="migrating">Migrating save data from old install&#8230; (this may take a while)</string>
<string name="not_granted">Required permission wasn\'t granted, Minetest can\'t run without it</string>
<string name="notification_title">Loading Minetest</string>
<string name="notification_description">Less than 1 minute&#8230;</string>
<string name="ime_dialog_done">Done</string>
<string name="no_web_browser">No web browser found</string>
<string name="no_external_storage">External storage isn\'t available. If you use an SDCard, please reinsert it. Otherwise, try restarting your phone or contacting the Minetest developers</string>
</resources>

View file

@ -1,21 +1,23 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
project.ext.set("versionMajor", 5) // Version Major
project.ext.set("versionMinor", 12) // Version Minor
project.ext.set("versionMinor", 7) // Version Minor
project.ext.set("versionPatch", 0) // Version Patch
// ^ keep in sync with cmake
project.ext.set("versionBuild", 0) // Version Build
// ^ fourth version number to allow releasing Android-only fixes and beta versions
project.ext.set("versionExtra", "") // Version Extra
project.ext.set("versionCode", 44) // Android Version Code
project.ext.set("developmentBuild", 0) // Whether it is a development build, or a release
// NOTE: +2 after each release!
// +1 for ARM and +1 for ARM64 APK's, because
// each APK must have a larger `versionCode` than the previous
buildscript {
ext.ndk_version = '27.2.12479018'
ext.ndk_version = '25.2.9519653'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.5.1'
classpath 'com.android.tools.build:gradle:7.4.1'
classpath 'de.undercouch:gradle-download-task:4.1.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View file

@ -1,13 +1,11 @@
<#if isLowMemory>
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx4G -XX:MaxPermSize=2G -XX:+HeapDumpOnOutOfMemoryError
<#else>
org.gradle.jvmargs=-Xmx16G -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx16G -XX:MaxPermSize=8G -XX:+HeapDumpOnOutOfMemoryError
</#if>
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.parallel.threads=8
org.gradle.configureondemand=true
android.enableJetifier=false
android.enableJetifier=true
android.useAndroidX=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

Binary file not shown.

View file

@ -1,7 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip

285
android/gradlew vendored
View file

@ -1,7 +1,7 @@
#!/bin/sh
#!/usr/bin/env sh
#
# Copyright © 2015-2021 the original authors.
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -17,111 +17,78 @@
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
MAX_FD="maximum"
warn () {
echo "$*"
} >&2
}
die () {
echo
echo "$*"
echo
exit 1
} >&2
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD=$JAVA_HOME/bin/java
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -130,120 +97,92 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD="java"
command -v java >/dev/null || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

100
android/gradlew.bat vendored Normal file
View file

@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
sodipodi:docname="exit_btn.svg"
inkscape:export-filename="../../textures/base/pack/exit_btn.png"
inkscape:export-xdpi="24.000002"
inkscape:export-ydpi="24.000002"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs10" />
<sodipodi:namedview
id="base"
pagecolor="#404040"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.84958349"
inkscape:cx="-94.752312"
inkscape:cy="291.31922"
inkscape:document-units="px"
inkscape:current-layer="layer2"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="false"
inkscape:snap-grids="true"
inkscape:snap-page="true"
showguides="false"
inkscape:showpageshadow="2"
inkscape:deskcolor="#404040">
<inkscape:grid
type="xygrid"
id="grid16"
spacingx="0.26458333"
spacingy="0.26458333"
empspacing="4"
color="#40ff40"
opacity="0.1254902"
empcolor="#40ff40"
empopacity="0.25098039" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="display:inline">
<path
id="rect5028"
style="display:inline;fill:none;stroke:#ffffff;stroke-width:5.99996;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 78.052082,90.48746 v 17.4625 l -50.535415,4e-5 V 27.516667 l 50.535415,3.7e-5 v 17.462423"
sodipodi:nodetypes="cccccc" />
<path
style="display:inline;fill:none;stroke:#ffffff;stroke-width:5.99996;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 101.49853,55.033202 12.69966,12.700052 -12.69966,12.699942"
id="path4737"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
<path
style="display:inline;fill:none;stroke:#ffffff;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 113.36416,67.733332 H 59.484405"
id="path4729"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

194
android/icons/gear_icon.svg Normal file
View file

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg8"
inkscape:version="0.92.1 r15371"
sodipodi:docname="gear_icon.svg"
inkscape:export-filename="/home/stu/Desktop/icons/png/gear_icon.png"
inkscape:export-xdpi="24"
inkscape:export-ydpi="24">
<defs
id="defs2">
<marker
style="overflow:visible"
refY="0.0"
refX="0.0"
orient="auto"
id="DistanceX">
<path
id="path4687"
style="stroke:#000000; stroke-width:0.5"
d="M 3,-3 L -3,3 M 0,-5 L 0,5" />
</marker>
<pattern
y="0"
x="0"
width="8"
patternUnits="userSpaceOnUse"
id="Hatch"
height="8">
<path
id="path4690"
stroke-width="0.25"
stroke="#000000"
linecap="square"
d="M8 4 l-4,4" />
<path
id="path4692"
stroke-width="0.25"
stroke="#000000"
linecap="square"
d="M6 2 l-4,4" />
<path
id="path4694"
stroke-width="0.25"
stroke="#000000"
linecap="square"
d="M4 0 l-4,4" />
</pattern>
<marker
style="overflow:visible"
refY="0.0"
refX="0.0"
orient="auto"
id="DistanceX-3">
<path
id="path4756"
style="stroke:#000000; stroke-width:0.5"
d="M 3,-3 L -3,3 M 0,-5 L 0,5" />
</marker>
<pattern
y="0"
x="0"
width="8"
patternUnits="userSpaceOnUse"
id="Hatch-6"
height="8">
<path
id="path4759"
stroke-width="0.25"
stroke="#000000"
linecap="square"
d="M8 4 l-4,4" />
<path
id="path4761"
stroke-width="0.25"
stroke="#000000"
linecap="square"
d="M6 2 l-4,4" />
<path
id="path4763"
stroke-width="0.25"
stroke="#000000"
linecap="square"
d="M4 0 l-4,4" />
</pattern>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#404040"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.5"
inkscape:cx="-308.644"
inkscape:cy="171.10144"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1023"
inkscape:window-x="0"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="false"
inkscape:snap-page="true"
inkscape:snap-grids="true"
inkscape:snap-bbox="true"
inkscape:snap-bbox-midpoints="false"
inkscape:snap-object-midpoints="true"
inkscape:snap-to-guides="false"
inkscape:showpageshadow="false"
inkscape:snap-smooth-nodes="true"
inkscape:object-nodes="false">
<inkscape:grid
type="xygrid"
id="grid16"
spacingx="2.1166666"
spacingy="2.1166666"
empspacing="2"
color="#40ff40"
opacity="0.1254902"
empcolor="#40ff40"
empopacity="0.25098039" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-161.53332)">
<g
id="g4792"
transform="matrix(0.68725287,0,0,0.65623884,67.477909,-509.24679)"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.74041259;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
<g
id="g4772"
inkscape:label="OpenJsCad"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.74041259;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 256,80.943359 -8.28125,0.72461 -3.63477,5.410156 -5.61328,12.685547 -2.28906,9.259768 -0.35156,5.1875 0.0937,0.86133 0.70703,2.44726 0.60547,9.80664 -2.66602,5.47461 -21.5957,5.78711 -5.04492,-3.40625 -4.37696,-8.79687 -0.61132,-2.47071 -0.35157,-0.79297 -2.89648,-4.31836 -6.60938,-6.87304 -11.20507,-8.17969 -5.84961,-2.86719 -7.53516,3.51367 -6.80859,4.76954 -0.44336,6.50195 1.48047,13.79297 2.64453,9.16406 2.28906,4.66992 0.51172,0.69922 1.83398,1.76563 5.42774,8.18945 0.42773,6.07422 -15.81054,15.81445 -6.07032,-0.42773 -8.18945,-5.42969 -1.76367,-1.83399 -0.69922,-0.51171 -4.66992,-2.28907 -9.15821,-2.64843 -13.79297,-1.47852 -6.5,0.44141 -4.76757,6.8125 -3.51367,7.53515 2.86914,5.85157 8.17382,11.20703 6.87305,6.61132 4.31641,2.90039 0.79297,0.34961 2.4707,0.61133 8.79492,4.37696 3.4043,5.04687 -5.78516,21.60156 -5.47265,2.66602 -9.80469,-0.60547 -2.44727,-0.70703 -0.85937,-0.0918 -5.1875,0.35156 -9.257816,2.28907 -12.68164,5.61523 -5.408203,3.63281 -0.72461,8.28516 0.72461,8.28516 5.408203,3.63281 12.68164,5.61523 9.257816,2.28907 5.1875,0.35156 0.85937,-0.0918 2.44727,-0.70703 9.80469,-0.60547 5.47265,2.66602 5.78516,21.60156 -3.4043,5.04687 -8.79492,4.37696 -2.4707,0.61133 -0.79297,0.34961 -4.31641,2.90039 -6.87305,6.61132 -8.17382,11.20703 -2.86914,5.85157 3.51367,7.53515 4.76757,6.8125 6.5,0.44141 13.79297,-1.47852 9.15821,-2.64843 4.66992,-2.28907 0.69922,-0.50976 1.76367,-1.83594 8.18945,-5.42969 6.07032,-0.42773 15.81054,15.81445 -0.42773,6.07422 -5.42774,8.18945 -1.83398,1.76563 -0.51172,0.69922 -2.28906,4.66992 -2.64453,9.16406 -1.48047,13.79297 0.44336,6.50195 6.80859,4.76758 7.53516,3.51758 5.84961,-2.86914 11.20507,-8.17969 6.60938,-6.87304 2.89648,-4.31836 0.35157,-0.79297 0.61132,-2.47071 4.37696,-8.79687 5.04492,-3.40625 21.5957,5.78711 2.66602,5.47461 -0.60547,9.80664 -0.70703,2.44726 -0.0937,0.85938 0.35156,5.18945 2.28906,9.25977 5.61328,12.68555 3.63477,5.41015 8.28125,0.72461 8.28125,-0.72461 3.63477,-5.41015 5.61328,-12.68555 2.28906,-9.25977 0.35156,-5.18945 -0.0937,-0.85938 -0.70703,-2.44726 -0.60547,-9.80664 2.66602,-5.47461 21.5957,-5.78711 5.04492,3.40625 4.37696,8.79687 0.61132,2.47071 0.35157,0.79297 2.89648,4.31836 6.60938,6.87304 11.20507,8.17969 5.84961,2.86914 7.53516,-3.51758 6.80859,-4.76758 0.44336,-6.50195 -1.48047,-13.79297 -2.64453,-9.16406 -2.28906,-4.66992 -0.51172,-0.69922 -1.83398,-1.76563 -5.42774,-8.18945 -0.42773,-6.07422 15.81054,-15.81445 6.07032,0.42773 8.18945,5.42969 1.76367,1.83594 0.69922,0.50976 4.66992,2.28907 9.15821,2.64843 13.79297,1.47852 6.5,-0.44141 v -0.002 l 4.76757,-6.81055 3.51367,-7.53711 -2.86914,-5.85156 -8.17382,-11.20508 -6.87305,-6.61328 -4.31641,-2.89843 -0.79297,-0.34961 -2.4707,-0.61133 -8.79492,-4.37891 -3.4043,-5.04492 5.78516,-21.60352 5.47265,-2.66797 9.80469,0.60938 2.44727,0.70703 0.85937,0.0918 5.1875,-0.35156 9.25782,-2.28907 12.68164,-5.61718 5.4082,-3.63282 0.72461,-8.28515 -0.72461,-8.28321 -5.4082,-3.63476 -12.68164,-5.61524 -9.25782,-2.28711 -5.1875,-0.35351 -0.85937,0.0937 -2.44727,0.70508 -9.80469,0.60937 -5.47265,-2.66797 -5.78516,-21.59961 3.4043,-5.04882 8.79492,-4.37696 2.4707,-0.61133 0.79297,-0.35156 4.31641,-2.89844 6.87305,-6.61132 8.17382,-11.20703 2.86914,-5.85157 -3.51367,-7.53711 -4.76757,-6.81054 -6.5,-0.44336 -13.79297,1.48047 -9.15821,2.64648 -4.66992,2.28906 -0.69922,0.51172 -1.76367,1.83594 -8.18945,5.42773 -6.07032,0.42774 -15.81054,-15.81446 0.42773,-6.07226 5.42774,-8.18945 1.83398,-1.76563 0.51172,-0.69922 2.28906,-4.67187 2.64453,-9.16016 1.48047,-13.79492 -0.44336,-6.50195 -6.80859,-4.76954 -7.53516,-3.51562 -5.84961,2.87109 -11.20507,8.17578 -6.60938,6.875 -2.89648,4.31836 -0.35157,0.79102 -0.61132,2.47266 -4.37696,8.79687 -5.04492,3.4082 -21.5957,-5.79101 -2.66602,-5.47266 0.60547,-9.80664 0.70703,-2.44726 0.0937,-0.85938 -0.35156,-5.19141 -2.28906,-9.259761 -5.61328,-12.683594 -3.63477,-5.412109 z m 0,97.111331 A 77.946197,77.946197 0 0 1 333.94531,256 77.946197,77.946197 0 0 1 256,333.94531 77.946197,77.946197 0 0 1 178.05469,256 77.946197,77.946197 0 0 1 256,178.05469 Z"
transform="matrix(0.38495268,0,0,0.40318156,-98.176247,1022.1341)"
id="path4768"
inkscape:connector-curvature="0" />
</g>
<g
id="g4774"
inkscape:label="0"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.74041259;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -2,23 +2,23 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="256"
height="512"
viewBox="0 0 135.46666 135.46667"
viewBox="0 0 67.73333 135.46667"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
sodipodi:docname="overflow_btn.svg"
inkscape:export-filename="../../textures/base/pack/overflow_btn.png"
inkscape:version="0.92.1 r15371"
sodipodi:docname="rare_controls.svg"
inkscape:export-filename="/home/stu/Desktop/icons/png/rare_controls.png"
inkscape:export-xdpi="24.000002"
inkscape:export-ydpi="24.000002"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
inkscape:export-ydpi="24.000002">
<defs
id="defs2">
<filter
@ -397,24 +397,22 @@
borderopacity="1.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="41.921331"
inkscape:cy="163.13964"
inkscape:zoom="0.7"
inkscape:cx="-59.862018"
inkscape:cy="260.34663"
inkscape:document-units="mm"
inkscape:current-layer="layer2"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-height="1023"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-y="34"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="false"
inkscape:snap-grids="true"
inkscape:snap-page="true"
showguides="false"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1">
showguides="false">
<inkscape:grid
type="xygrid"
id="grid16"
@ -424,11 +422,7 @@
color="#40ff40"
opacity="0.1254902"
empcolor="#40ff40"
empopacity="0.25098039"
originx="0"
originy="0"
units="px"
visible="true" />
empopacity="0.25098039" />
</sodipodi:namedview>
<metadata
id="metadata5">
@ -438,6 +432,7 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
</cc:Work>
@ -501,27 +496,26 @@
x="264.65997"
y="124.10143"
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-opacity:1" /></flowRegion><flowPara
id="flowPara4724" /></flowRoot>
id="flowPara4724" /></flowRoot> <rect
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5231-9"
width="25.4"
height="25.400003"
x="21.166666"
y="101.6" />
<rect
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:5.38756;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5231-9-7"
width="25.4"
height="25.400003"
x="21.166666"
y="55.033333" />
<rect
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5231-9-5"
width="96.212502"
height="0.61243516"
x="19.62678"
y="33.575779" />
<rect
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:5.38755;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5231-9-5-7"
width="96.212448"
height="0.6124543"
x="19.627108"
y="67.442772" />
<rect
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:5.38755;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect5231-9-5-7-5"
width="96.212448"
height="0.6124543"
x="19.627108"
y="101.30978" />
width="25.4"
height="25.400003"
x="21.166664"
y="8.4666681" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -2,24 +2,27 @@ apply plugin: 'com.android.library'
apply plugin: 'de.undercouch.download'
android {
compileSdkVersion 33
buildToolsVersion '33.0.2'
ndkVersion "$ndk_version"
defaultConfig {
minSdkVersion 21
compileSdk 34
targetSdkVersion 34
targetSdkVersion 33
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared",
"-DENABLE_CURL=1", "-DENABLE_SOUND=1",
"-DENABLE_GETTEXT=1",
"-DBUILD_UNITTESTS=0", "-DENABLE_UPDATE_CHECKER=0"
ndkBuild {
arguments '-j' + Runtime.getRuntime().availableProcessors(),
"versionMajor=${versionMajor}",
"versionMinor=${versionMinor}",
"versionPatch=${versionPatch}",
"versionExtra=${versionExtra}",
"developmentBuild=${developmentBuild}"
}
}
}
externalNativeBuild {
cmake {
path file("../../CMakeLists.txt")
ndkBuild {
path file('jni/Android.mk')
}
}
@ -34,34 +37,33 @@ android {
buildTypes {
release {
externalNativeBuild {
ndkBuild {
arguments 'NDEBUG=1'
}
}
ndk {
debugSymbolLevel 'FULL'
debugSymbolLevel 'SYMBOL_TABLE'
}
}
}
namespace 'net.minetest'
}
// get precompiled deps
def depsDir = new File(buildDir.parent, 'deps')
if (new File(depsDir, 'armeabi-v7a').exists()) {
task getDeps {
doLast { logger.lifecycle('Using existing deps from {}', depsDir) }
}
} else {
task downloadDeps(type: Download) {
def depsZip = new File(buildDir, 'deps.zip')
task downloadDeps(type: Download) {
def depsDir = new File(buildDir.parent, 'deps')
def depsZip = new File(buildDir, 'deps.zip')
src 'https://github.com/luanti-org/luanti_android_deps/releases/download/latest/deps-lite.zip'
dest depsZip
overwrite false
src 'https://github.com/minetest/minetest_android_deps/releases/download/latest/deps.zip'
dest depsZip
overwrite false
task getDeps(dependsOn: downloadDeps, type: Copy) {
depsDir.mkdir()
from zipTree(depsZip)
into depsDir
doFirst { logger.lifecycle('Extracting to {}', depsDir) }
}
task getDeps(dependsOn: downloadDeps, type: Copy) {
depsDir.mkdir()
from zipTree(depsZip)
into depsDir
doFirst { logger.lifecycle('Extracting to {}', depsDir) }
}
}

View file

@ -0,0 +1,302 @@
LOCAL_PATH := $(call my-dir)/..
#LOCAL_ADDRESS_SANITIZER:=true
#USE_BUILTIN_LUA:=true
include $(CLEAR_VARS)
LOCAL_MODULE := Curl
LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libcurl.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libmbedcrypto
LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libmbedcrypto.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libmbedtls
LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libmbedtls.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libmbedx509
LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libmbedx509.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Freetype
LOCAL_SRC_FILES := deps/$(APP_ABI)/Freetype/libfreetype.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Iconv
LOCAL_SRC_FILES := deps/$(APP_ABI)/Iconv/libiconv.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libcharset
LOCAL_SRC_FILES := deps/$(APP_ABI)/Iconv/libcharset.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Irrlicht
LOCAL_SRC_FILES := deps/$(APP_ABI)/Irrlicht/libIrrlichtMt.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Irrlicht-libpng
LOCAL_SRC_FILES := deps/$(APP_ABI)/Irrlicht/libpng.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Irrlicht-libjpeg
LOCAL_SRC_FILES := deps/$(APP_ABI)/Irrlicht/libjpeg.a
include $(PREBUILT_STATIC_LIBRARY)
ifndef USE_BUILTIN_LUA
include $(CLEAR_VARS)
LOCAL_MODULE := LuaJIT
LOCAL_SRC_FILES := deps/$(APP_ABI)/LuaJIT/libluajit.a
include $(PREBUILT_STATIC_LIBRARY)
endif
include $(CLEAR_VARS)
LOCAL_MODULE := OpenAL
LOCAL_SRC_FILES := deps/$(APP_ABI)/OpenAL-Soft/libopenal.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Gettext
LOCAL_SRC_FILES := deps/$(APP_ABI)/Gettext/libintl.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := SQLite3
LOCAL_SRC_FILES := deps/$(APP_ABI)/SQLite/libsqlite3.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Vorbis
LOCAL_SRC_FILES := deps/$(APP_ABI)/Vorbis/libvorbis.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libvorbisfile
LOCAL_SRC_FILES := deps/$(APP_ABI)/Vorbis/libvorbisfile.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libogg
LOCAL_SRC_FILES := deps/$(APP_ABI)/Vorbis/libogg.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Zstd
LOCAL_SRC_FILES := deps/$(APP_ABI)/Zstd/libzstd.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := Minetest
LOCAL_CFLAGS += \
-DJSONCPP_NO_LOCALE_SUPPORT \
-DHAVE_TOUCHSCREENGUI \
-DENABLE_GLES=1 \
-DUSE_CURL=1 \
-DUSE_SOUND=1 \
-DUSE_LEVELDB=0 \
-DUSE_GETTEXT=1 \
-DVERSION_MAJOR=${versionMajor} \
-DVERSION_MINOR=${versionMinor} \
-DVERSION_PATCH=${versionPatch} \
-DVERSION_EXTRA=${versionExtra} \
-DDEVELOPMENT_BUILD=${developmentBuild} \
$(GPROF_DEF)
ifdef USE_BUILTIN_LUA
LOCAL_CFLAGS += -DUSE_LUAJIT=0
else
LOCAL_CFLAGS += -DUSE_LUAJIT=1
endif
ifdef NDEBUG
LOCAL_CFLAGS += -DNDEBUG=1
endif
ifdef GPROF
GPROF_DEF := -DGPROF
PROFILER_LIBS := android-ndk-profiler
LOCAL_CFLAGS += -pg
endif
LOCAL_C_INCLUDES := \
../../src \
../../src/script \
../../lib/gmp \
../../lib/jsoncpp \
deps/$(APP_ABI)/Curl/include \
deps/$(APP_ABI)/Freetype/include/freetype2 \
deps/$(APP_ABI)/Irrlicht/include \
deps/$(APP_ABI)/Gettext/include \
deps/$(APP_ABI)/Iconv/include \
deps/$(APP_ABI)/OpenAL-Soft/include \
deps/$(APP_ABI)/SQLite/include \
deps/$(APP_ABI)/Vorbis/include \
deps/$(APP_ABI)/Zstd/include
ifdef USE_BUILTIN_LUA
LOCAL_C_INCLUDES += \
../../lib/lua/src \
../../lib/bitop
else
LOCAL_C_INCLUDES += deps/$(APP_ABI)/LuaJIT/include
endif
LOCAL_SRC_FILES := \
$(wildcard ../../src/client/*.cpp) \
$(wildcard ../../src/client/*/*.cpp) \
$(wildcard ../../src/content/*.cpp) \
../../src/database/database.cpp \
../../src/database/database-dummy.cpp \
../../src/database/database-files.cpp \
../../src/database/database-sqlite3.cpp \
$(wildcard ../../src/gui/*.cpp) \
$(wildcard ../../src/irrlicht_changes/*.cpp) \
$(wildcard ../../src/mapgen/*.cpp) \
$(wildcard ../../src/network/*.cpp) \
$(wildcard ../../src/script/*.cpp) \
$(wildcard ../../src/script/*/*.cpp) \
$(wildcard ../../src/server/*.cpp) \
$(wildcard ../../src/threading/*.cpp) \
$(wildcard ../../src/util/*.c) \
$(wildcard ../../src/util/*.cpp) \
../../src/ban.cpp \
../../src/chat.cpp \
../../src/clientiface.cpp \
../../src/collision.cpp \
../../src/content_mapnode.cpp \
../../src/content_nodemeta.cpp \
../../src/convert_json.cpp \
../../src/craftdef.cpp \
../../src/debug.cpp \
../../src/defaultsettings.cpp \
../../src/emerge.cpp \
../../src/environment.cpp \
../../src/face_position_cache.cpp \
../../src/filesys.cpp \
../../src/gettext.cpp \
../../src/httpfetch.cpp \
../../src/hud.cpp \
../../src/inventory.cpp \
../../src/inventorymanager.cpp \
../../src/itemdef.cpp \
../../src/itemstackmetadata.cpp \
../../src/light.cpp \
../../src/lighting.cpp \
../../src/log.cpp \
../../src/main.cpp \
../../src/map.cpp \
../../src/map_settings_manager.cpp \
../../src/mapblock.cpp \
../../src/mapnode.cpp \
../../src/mapsector.cpp \
../../src/metadata.cpp \
../../src/modchannels.cpp \
../../src/nameidmapping.cpp \
../../src/nodedef.cpp \
../../src/nodemetadata.cpp \
../../src/nodetimer.cpp \
../../src/noise.cpp \
../../src/objdef.cpp \
../../src/object_properties.cpp \
../../src/particles.cpp \
../../src/pathfinder.cpp \
../../src/player.cpp \
../../src/porting.cpp \
../../src/porting_android.cpp \
../../src/profiler.cpp \
../../src/raycast.cpp \
../../src/reflowscan.cpp \
../../src/remoteplayer.cpp \
../../src/rollback.cpp \
../../src/rollback_interface.cpp \
../../src/serialization.cpp \
../../src/server.cpp \
../../src/serverenvironment.cpp \
../../src/serverlist.cpp \
../../src/settings.cpp \
../../src/staticobject.cpp \
../../src/texture_override.cpp \
../../src/tileanimation.cpp \
../../src/tool.cpp \
../../src/translation.cpp \
../../src/version.cpp \
../../src/voxel.cpp \
../../src/voxelalgorithms.cpp
# Built-in Lua
ifdef USE_BUILTIN_LUA
LOCAL_SRC_FILES += \
../../lib/lua/src/lapi.c \
../../lib/lua/src/lauxlib.c \
../../lib/lua/src/lbaselib.c \
../../lib/lua/src/lcode.c \
../../lib/lua/src/ldblib.c \
../../lib/lua/src/ldebug.c \
../../lib/lua/src/ldo.c \
../../lib/lua/src/ldump.c \
../../lib/lua/src/lfunc.c \
../../lib/lua/src/lgc.c \
../../lib/lua/src/linit.c \
../../lib/lua/src/liolib.c \
../../lib/lua/src/llex.c \
../../lib/lua/src/lmathlib.c \
../../lib/lua/src/lmem.c \
../../lib/lua/src/loadlib.c \
../../lib/lua/src/lobject.c \
../../lib/lua/src/lopcodes.c \
../../lib/lua/src/loslib.c \
../../lib/lua/src/lparser.c \
../../lib/lua/src/lstate.c \
../../lib/lua/src/lstring.c \
../../lib/lua/src/lstrlib.c \
../../lib/lua/src/ltable.c \
../../lib/lua/src/ltablib.c \
../../lib/lua/src/ltm.c \
../../lib/lua/src/lundump.c \
../../lib/lua/src/lvm.c \
../../lib/lua/src/lzio.c \
../../lib/bitop/bit.c
endif
# GMP
LOCAL_SRC_FILES += ../../lib/gmp/mini-gmp.c
# JSONCPP
LOCAL_SRC_FILES += ../../lib/jsoncpp/jsoncpp.cpp
LOCAL_STATIC_LIBRARIES += \
Curl libmbedcrypto libmbedtls libmbedx509 \
Freetype \
Iconv libcharset \
Irrlicht Irrlicht-libpng Irrlicht-libjpeg \
OpenAL \
Gettext \
SQLite3 \
Vorbis libvorbisfile libogg \
Zstd
ifndef USE_BUILTIN_LUA
LOCAL_STATIC_LIBRARIES += LuaJIT
endif
LOCAL_STATIC_LIBRARIES += android_native_app_glue $(PROFILER_LIBS)
LOCAL_LDLIBS := -lEGL -lGLESv1_CM -lGLESv2 -landroid -lOpenSLES -lz
include $(BUILD_SHARED_LIBRARY)
ifdef GPROF
$(call import-module,android-ndk-profiler)
endif
$(call import-module,android/native_app_glue)

View file

@ -0,0 +1,32 @@
APP_PLATFORM := ${APP_PLATFORM}
APP_ABI := ${TARGET_ABI}
APP_STL := c++_shared
NDK_TOOLCHAIN_VERSION := clang
APP_SHORT_COMMANDS := true
APP_MODULES := Minetest
APP_CPPFLAGS := -O2 -fvisibility=hidden
ifeq ($(APP_ABI),armeabi-v7a)
APP_CPPFLAGS += -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb
endif
ifeq ($(APP_ABI),x86)
APP_CPPFLAGS += -mssse3 -mfpmath=sse -funroll-loops
endif
ifndef NDEBUG
APP_CPPFLAGS := -g -Og -fno-omit-frame-pointer
endif
APP_CFLAGS := $(APP_CPPFLAGS) -Wno-inconsistent-missing-override -Wno-parentheses-equality
APP_CXXFLAGS := $(APP_CPPFLAGS) -fexceptions -frtti -std=gnu++14
APP_LDFLAGS := -Wl,--no-warn-mismatch,--gc-sections,--icf=safe
ifeq ($(APP_ABI),arm64-v8a)
APP_LDFLAGS := -Wl,--no-warn-mismatch,--gc-sections
endif
ifndef NDEBUG
APP_LDFLAGS :=
endif

View file

@ -1 +1 @@
<manifest />
<manifest package="net.minetest" />

View file

@ -1,2 +1,2 @@
rootProject.name = "Luanti"
rootProject.name = "Minetest"
include ':app', ':native'

View file

@ -36,16 +36,11 @@ do
setmetatable(v, {__newindex = {}})
-- Reassemble the other tables
if v.type == "node" then
getmetatable(v).__index = all.nodedef_default
all.registered_nodes[k] = v
elseif v.type == "craft" then
getmetatable(v).__index = all.craftitemdef_default
elseif v.type == "craftitem" then
all.registered_craftitems[k] = v
elseif v.type == "tool" then
getmetatable(v).__index = all.tooldef_default
all.registered_tools[k] = v
else
getmetatable(v).__index = all.noneitemdef_default
end
end

View file

@ -1,3 +1,5 @@
-- Minetest: builtin/client/chatcommands.lua
core.register_on_sending_chat_message(function(message)
if message:sub(1,2) == ".." then
return false

View file

@ -0,0 +1,15 @@
-- CSM death formspec. Only used when clientside modding is enabled, otherwise
-- handled by the engine.
core.register_on_death(function()
local formspec = "size[11,5.5]bgcolor[#320000b4;true]" ..
"label[4.85,1.35;" .. fgettext("You died") ..
"]button_exit[4,3;3,0.5;btn_respawn;".. fgettext("Respawn") .."]"
core.show_formspec("bultin:death", formspec)
end)
core.register_on_formspec_input(function(formname, fields)
if formname == "bultin:death" then
core.send_respawn()
end
end)

View file

@ -1,15 +1,13 @@
-- Minetest: builtin/client/init.lua
local scriptpath = core.get_builtin_path()
local clientpath = scriptpath.."client"..DIR_DELIM
local commonpath = scriptpath.."common"..DIR_DELIM
local builtin_shared = {}
assert(loadfile(commonpath .. "register.lua"))(builtin_shared)
assert(loadfile(clientpath .. "register.lua"))(builtin_shared)
dofile(clientpath .. "register.lua")
dofile(commonpath .. "after.lua")
dofile(commonpath .. "mod_storage.lua")
dofile(commonpath .. "chatcommands.lua")
dofile(commonpath .. "information_formspecs.lua")
dofile(clientpath .. "chatcommands.lua")
dofile(clientpath .. "death_formspec.lua")
dofile(clientpath .. "misc.lua")
assert(loadfile(commonpath .. "item_s.lua"))({}) -- Just for push/read node functions

View file

@ -5,14 +5,3 @@ function core.setting_get_pos(name)
end
return core.string_to_pos(value)
end
-- old non-method sound functions
function core.sound_stop(handle, ...)
return handle:stop(...)
end
function core.sound_fade(handle, ...)
return handle:fade(...)
end

View file

@ -1,6 +1,68 @@
local builtin_shared = ...
core.callback_origins = {}
local make_registration = builtin_shared.make_registration
local getinfo = debug.getinfo
debug.getinfo = nil
--- Runs given callbacks.
--
-- Note: this function is also called from C++
-- @tparam table callbacks a table with registered callbacks, like `core.registered_on_*`
-- @tparam number mode a RunCallbacksMode, as defined in src/script/common/c_internal.h
-- @param ... arguments for the callback
-- @return depends on mode
function core.run_callbacks(callbacks, mode, ...)
assert(type(callbacks) == "table")
local cb_len = #callbacks
if cb_len == 0 then
if mode == 2 or mode == 3 then
return true
elseif mode == 4 or mode == 5 then
return false
end
end
local ret
for i = 1, cb_len do
local cb_ret = callbacks[i](...)
if mode == 0 and i == 1 or mode == 1 and i == cb_len then
ret = cb_ret
elseif mode == 2 then
if not cb_ret or i == 1 then
ret = cb_ret
end
elseif mode == 3 then
if cb_ret then
return cb_ret
end
ret = cb_ret
elseif mode == 4 then
if (cb_ret and not ret) or i == 1 then
ret = cb_ret
end
elseif mode == 5 and cb_ret then
return cb_ret
end
end
return ret
end
--
-- Callback registration
--
local function make_registration()
local t = {}
local registerfunc = function(func)
t[#t + 1] = func
core.callback_origins[func] = {
mod = core.get_current_modname() or "??",
name = getinfo(1, "n").name or "??"
}
--local origin = core.callback_origins[func]
--print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func))
end
return t, registerfunc
end
core.registered_globalsteps, core.register_globalstep = make_registration()
core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration()

View file

@ -1,116 +1,4 @@
-- This is an implementation of a job sheduling mechanism. It guarantees that
-- coexisting jobs will execute primarily in order of least expiry, and
-- secondarily in order of first registration.
-- These functions implement an intrusive singly linked list of one or more
-- elements where the first element has a pointer to the last. The next pointer
-- is stored with key list_next. The pointer to the last is with key list_end.
local function list_init(first)
first.list_end = first
end
local function list_append(first, append)
first.list_end.list_next = append
first.list_end = append
end
local function list_append_list(first, first_append)
first.list_end.list_next = first_append
first.list_end = first_append.list_end
end
-- The jobs are stored in a map from expiration times to linked lists of jobs
-- as above. The expiration times are also stored in an array representing a
-- binary min heap, which is a particular arrangement of binary tree. A parent
-- at index i has children at indices i*2 and i*2+1. Out-of-bounds indices
-- represent nonexistent children. A parent is never greater than its children.
-- This structure means that, if there is at least one job, the next expiration
-- time is the first item in the array.
-- Push element on a binary min-heap,
-- "bubbling up" the element by swapping with larger parents.
local function heap_push(heap, element)
local index = #heap + 1
while index > 1 do
local parent_index = math.floor(index / 2)
local parent = heap[parent_index]
if element < parent then
heap[index] = parent
index = parent_index
else
break
end
end
heap[index] = element
end
-- Pop smallest element from the heap,
-- "sinking down" the last leaf on the last layer of the heap
-- by swapping with the smaller child.
local function heap_pop(heap)
local removed_element = heap[1]
local length = #heap
local element = heap[length]
heap[length] = nil
length = length - 1
if length > 0 then
local index = 1
while true do
local old_index = index
local smaller_element = element
local left_index = index * 2
local right_index = index * 2 + 1
if left_index <= length then
local left_element = heap[left_index]
if left_element < smaller_element then
index = left_index
smaller_element = left_element
end
end
if right_index <= length then
if heap[right_index] < smaller_element then
index = right_index
end
end
if old_index ~= index then
heap[old_index] = heap[index]
else
break
end
end
heap[index] = element
end
return removed_element
end
local job_map = {}
local expiries = {}
-- Adds an individual job with the given expiry.
-- The worst-case complexity is O(log n), where n is the number of distinct
-- expiration times.
local function add_job(expiry, job)
local list = job_map[expiry]
if list then
list_append(list, job)
else
list_init(job)
job_map[expiry] = job
heap_push(expiries, expiry)
end
end
-- Removes the next expiring jobs and returns the linked list of them.
-- The worst-case complexity is O(log n), where n is the number of distinct
-- expiration times.
local function remove_first_jobs()
local removed_expiry = heap_pop(expiries)
local removed = job_map[removed_expiry]
job_map[removed_expiry] = nil
return removed
end
local jobs = {}
local time = 0.0
local time_next = math.huge
@ -121,54 +9,42 @@ core.register_globalstep(function(dtime)
return
end
-- Remove the expired jobs.
local expired = remove_first_jobs()
time_next = math.huge
-- Remove other expired jobs and append them to the list.
while true do
time_next = expiries[1] or math.huge
if time_next > time then
break
-- Iterate backwards so that we miss any new timers added by
-- a timer callback.
for i = #jobs, 1, -1 do
local job = jobs[i]
if time >= job.expire then
core.set_last_run_mod(job.mod_origin)
job.func(unpack(job.arg))
local jobs_l = #jobs
jobs[i] = jobs[jobs_l]
jobs[jobs_l] = nil
elseif job.expire < time_next then
time_next = job.expire
end
list_append_list(expired, remove_first_jobs())
end
-- Run the callbacks afterward to prevent infinite loops with core.after(0, ...).
local last_expired = expired.list_end
while true do
core.set_last_run_mod(expired.mod_origin)
expired.func(unpack(expired.args, 1, expired.args.n))
if expired == last_expired then
break
end
expired = expired.list_next
end
end)
local job_metatable = {__index = {}}
local function dummy_func() end
function job_metatable.__index:cancel()
self.func = dummy_func
self.args = {n = 0}
end
function core.after(after, func, ...)
assert(tonumber(after) and not core.is_nan(after) and type(func) == "function",
"Invalid core.after invocation")
assert(tonumber(after) and type(func) == "function",
"Invalid minetest.after invocation")
local expire = time + after
local new_job = {
mod_origin = core.get_last_run_mod(),
func = func,
args = {
n = select("#", ...),
...
},
expire = expire,
arg = {...},
mod_origin = core.get_last_run_mod(),
}
local expiry = time + after
add_job(expiry, new_job)
time_next = math.min(time_next, expiry)
jobs[#jobs + 1] = new_job
time_next = math.min(time_next, expire)
return setmetatable(new_job, job_metatable)
return {
cancel = function()
new_job.func = function() end
new_job.args = {}
end
}
end

View file

@ -1,3 +1,5 @@
-- Minetest: builtin/common/chatcommands.lua
-- For server-side translations (if INIT == "game")
-- Otherwise, use core.gettext
local S = core.get_translator("__builtin")
@ -87,7 +89,7 @@ local function do_help_cmd(name, param)
if #args > 1 then
return false, S("Too many arguments, try using just /help <command>")
end
local use_gui = INIT == "client" or core.get_player_by_name(name)
local use_gui = INIT ~= "client" and core.get_player_by_name(name)
use_gui = use_gui and not opts:find("t")
if #args == 0 and not use_gui then
@ -161,8 +163,8 @@ end
if INIT == "client" then
core.register_chatcommand("help", {
params = core.gettext("[all | <cmd>] [-t]"),
description = core.gettext("Get help for commands (-t: output in chat)"),
params = core.gettext("[all | <cmd>]"),
description = core.gettext("Get help for commands"),
func = function(param)
return do_help_cmd(nil, param)
end,

View file

@ -1,4 +1,4 @@
--Luanti
--Minetest
--Copyright (C) 2013 sapier
--
--This program is free software; you can redistribute it and/or modify

View file

@ -61,20 +61,15 @@ local function build_chatcommands_formspec(name, sel, copy)
for i, data in ipairs(mod_cmds) do
rows[#rows + 1] = COLOR_BLUE .. ",0," .. F(data[1]) .. ","
for j, cmds in ipairs(data[2]) do
local has_priv = INIT == "client" or check_player_privs(name, cmds[2].privs)
local has_priv = check_player_privs(name, cmds[2].privs)
rows[#rows + 1] = ("%s,1,%s,%s"):format(
has_priv and COLOR_GREEN or COLOR_GRAY,
cmds[1], F(cmds[2].params))
if sel == #rows then
description = cmds[2].description
if copy then
local msg = S("Command: @1 @2",
core.colorize("#0FF", (INIT == "client" and "." or "/") .. cmds[1]), cmds[2].params)
if INIT == "client" then
core.display_chat_message(msg)
else
core.chat_send_player(name, msg)
end
core.chat_send_player(name, S("Command: @1 @2",
core.colorize("#0FF", "/" .. cmds[1]), cmds[2].params))
end
end
end
@ -116,46 +111,26 @@ end
-- DETAILED CHAT COMMAND INFORMATION
if INIT == "client" then
core.register_on_formspec_input(function(formname, fields)
if formname ~= "__builtin:help_cmds" or fields.quit then
return
end
local event = core.explode_table_event(fields.list)
if event.type ~= "INV" then
core.show_formspec("__builtin:help_cmds",
build_chatcommands_formspec(nil, event.row, event.type == "DCL"))
end
end)
else
core.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "__builtin:help_cmds" or fields.quit then
return
end
core.register_on_player_receive_fields(function(player, formname, fields)
if formname ~= "__builtin:help_cmds" or fields.quit then
return
end
local event = core.explode_table_event(fields.list)
if event.type ~= "INV" then
local name = player:get_player_name()
core.show_formspec(name, "__builtin:help_cmds",
build_chatcommands_formspec(name, event.row, event.type == "DCL"))
end
end)
end
local event = core.explode_table_event(fields.list)
if event.type ~= "INV" then
local name = player:get_player_name()
core.show_formspec(name, "__builtin:help_cmds",
build_chatcommands_formspec(name, event.row, event.type == "DCL"))
end
end)
function core.show_general_help_formspec(name)
if INIT == "client" then
core.show_formspec("__builtin:help_cmds",
build_chatcommands_formspec(name))
else
core.show_formspec(name, "__builtin:help_cmds",
build_chatcommands_formspec(name))
end
core.show_formspec(name, "__builtin:help_cmds",
build_chatcommands_formspec(name))
end
if INIT ~= "client" then
function core.show_privs_help_formspec(name)
core.show_formspec(name, "__builtin:help_privs",
build_privs_formspec(name))
end
function core.show_privs_help_formspec(name)
core.show_formspec(name, "__builtin:help_privs",
build_privs_formspec(name))
end

View file

@ -1,3 +1,4 @@
-- Minetest: builtin/item_s.lua
-- The distinction of what goes here is a bit tricky, basically it's everything
-- that does not (directly or indirectly) need access to ServerEnvironment,
-- Server or writable access to IGameDef on the engine side.
@ -90,7 +91,7 @@ local facedir_to_dir_map = {
1, 4, 3, 2,
}
function core.facedir_to_dir(facedir)
return vector.copy(facedir_to_dir[facedir_to_dir_map[facedir % 32]])
return facedir_to_dir[facedir_to_dir_map[facedir % 32]]
end
function core.dir_to_fourdir(dir)
@ -110,7 +111,7 @@ function core.dir_to_fourdir(dir)
end
function core.fourdir_to_dir(fourdir)
return vector.copy(facedir_to_dir[facedir_to_dir_map[fourdir % 4]])
return facedir_to_dir[facedir_to_dir_map[fourdir % 4]]
end
function core.dir_to_wallmounted(dir)
@ -143,11 +144,9 @@ local wallmounted_to_dir = {
vector.new(-1, 0, 0),
vector.new( 0, 0, 1),
vector.new( 0, 0, -1),
vector.new( 0, 1, 0),
vector.new( 0, -1, 0),
}
function core.wallmounted_to_dir(wallmounted)
return vector.copy(wallmounted_to_dir[wallmounted % 8])
return wallmounted_to_dir[wallmounted % 8]
end
function core.dir_to_yaw(dir)
@ -165,19 +164,20 @@ function core.is_colored_paramtype(ptype)
end
function core.strip_param2_color(param2, paramtype2)
if paramtype2 == "color" then
return param2
elseif paramtype2 == "colorfacedir" then
return math.floor(param2 / 32) * 32
elseif paramtype2 == "color4dir" then
return math.floor(param2 / 4) * 4
elseif paramtype2 == "colorwallmounted" then
return math.floor(param2 / 8) * 8
elseif paramtype2 == "colordegrotate" then
return math.floor(param2 / 32) * 32
else
if not core.is_colored_paramtype(paramtype2) then
return nil
end
if paramtype2 == "colorfacedir" then
param2 = math.floor(param2 / 32) * 32
elseif paramtype2 == "color4dir" then
param2 = math.floor(param2 / 4) * 4
elseif paramtype2 == "colorwallmounted" then
param2 = math.floor(param2 / 8) * 8
elseif paramtype2 == "colordegrotate" then
param2 = math.floor(param2 / 32) * 32
end
-- paramtype2 == "color" requires no modification.
return param2
end
-- Content ID caching

View file

@ -1,41 +0,0 @@
--[[
Math utils.
--]]
function math.hypot(x, y)
return math.sqrt(x * x + y * y)
end
function math.sign(x, tolerance)
tolerance = tolerance or 0
if x > tolerance then
return 1
elseif x < -tolerance then
return -1
end
return 0
end
function math.factorial(x)
assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer")
if x >= 171 then
-- 171! is greater than the biggest double, no need to calculate
return math.huge
end
local v = 1
for k = 2, x do
v = v * k
end
return v
end
function math.round(x)
if x < 0 then
local int = math.ceil(x)
local frac = x - int
return int - ((frac <= -0.5) and 1 or 0)
end
local int = math.floor(x)
local frac = x - int
return int + ((frac >= 0.5) and 1 or 0)
end

View file

@ -1,19 +0,0 @@
-- Registered metatables, used by the C++ packer
local known_metatables = {}
function core.register_portable_metatable(name, mt)
assert(type(name) == "string", ("attempt to use %s value as metatable name"):format(type(name)))
assert(type(mt) == "table", ("attempt to register a %s value as metatable"):format(type(mt)))
assert(known_metatables[name] == nil or known_metatables[name] == mt,
("attempt to override metatable %s"):format(name))
known_metatables[name] = mt
known_metatables[mt] = name
end
core.known_metatables = known_metatables
function core.register_async_metatable(...)
core.log("deprecated", "core.register_async_metatable is deprecated. " ..
"Use core.register_portable_metatable instead.")
return core.register_portable_metatable(...)
end
core.register_portable_metatable("__builtin:vector", vector.metatable)

View file

@ -1,7 +1,8 @@
-- Minetest: builtin/misc_helpers.lua
--------------------------------------------------------------------------------
-- Localize functions to avoid table lookups (better performance).
local string_sub, string_find = string.sub, string.find
local math = math
--------------------------------------------------------------------------------
local function basic_dump(o)
@ -205,18 +206,46 @@ function table.indexof(list, val)
end
--------------------------------------------------------------------------------
function table.keyof(tb, val)
for k, v in pairs(tb) do
if v == val then
return k
end
end
return nil
function string:trim()
return self:match("^%s*(.-)%s*$")
end
--------------------------------------------------------------------------------
function string:trim()
return self:match("^%s*(.-)%s*$")
function math.hypot(x, y)
return math.sqrt(x * x + y * y)
end
--------------------------------------------------------------------------------
function math.sign(x, tolerance)
tolerance = tolerance or 0
if x > tolerance then
return 1
elseif x < -tolerance then
return -1
end
return 0
end
--------------------------------------------------------------------------------
function math.factorial(x)
assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer")
if x >= 171 then
-- 171! is greater than the biggest double, no need to calculate
return math.huge
end
local v = 1
for k = 2, x do
v = v * k
end
return v
end
function math.round(x)
if x >= 0 then
return math.floor(x + 0.5)
end
return math.ceil(x - 0.5)
end
local formspec_escapes = {
@ -233,16 +262,6 @@ function core.formspec_escape(text)
end
local hypertext_escapes = {
["\\"] = "\\\\",
["<"] = "\\<",
[">"] = "\\>",
}
function core.hypertext_escape(text)
return text and text:gsub("[\\<>]", hypertext_escapes)
end
function core.wrap_text(text, max_length, as_table)
local result = {}
local line = {}
@ -471,9 +490,6 @@ end
function table.insert_all(t, other)
if table.move then -- LuaJIT
return table.move(other, 1, #other, #t + 1, t)
end
for i=1, #other do
t[#t + 1] = other[i]
end
@ -572,14 +588,12 @@ function core.strip_colors(str)
return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", ""))
end
local function translate(textdomain, str, num, ...)
function core.translate(textdomain, str, ...)
local start_seq
if textdomain == "" and num == "" then
if textdomain == "" then
start_seq = ESCAPE_CHAR .. "T"
elseif num == "" then
start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")"
else
start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. "@" .. num .. ")"
start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")"
end
local arg = {n=select('#', ...), ...}
local end_seq = ESCAPE_CHAR .. "E"
@ -610,31 +624,8 @@ local function translate(textdomain, str, num, ...)
return start_seq .. translated .. end_seq
end
function core.translate(textdomain, str, ...)
return translate(textdomain, str, "", ...)
end
function core.translate_n(textdomain, str, str_plural, n, ...)
assert (type(n) == "number")
assert (n >= 0)
assert (math.floor(n) == n)
-- Truncate n if too large
local max = 1000000
if n >= 2 * max then
n = n % max + max
end
if n == 1 then
return translate(textdomain, str, "1", ...)
else
return translate(textdomain, str_plural, tostring(n), ...)
end
end
function core.get_translator(textdomain)
return
(function(str, ...) return core.translate(textdomain or "", str, ...) end),
(function(str, str_plural, n, ...) return core.translate_n(textdomain or "", str, str_plural, n, ...) end)
return function(str, ...) return core.translate(textdomain or "", str, ...) end
end
--------------------------------------------------------------------------------
@ -695,7 +686,6 @@ function core.privs_to_string(privs, delim)
list[#list + 1] = priv
end
end
table.sort(list)
return table.concat(list, delim)
end

View file

@ -1,76 +0,0 @@
local builtin_shared = ...
do
local default = {mod = "??", name = "??"}
core.callback_origins = setmetatable({}, {
__index = function()
return default
end
})
end
function core.run_callbacks(callbacks, mode, ...)
assert(type(callbacks) == "table")
local cb_len = #callbacks
if cb_len == 0 then
if mode == 2 or mode == 3 then
return true
elseif mode == 4 or mode == 5 then
return false
end
end
local ret = nil
for i = 1, cb_len do
local origin = core.callback_origins[callbacks[i]]
core.set_last_run_mod(origin.mod)
local cb_ret = callbacks[i](...)
if mode == 0 and i == 1 then
ret = cb_ret
elseif mode == 1 and i == cb_len then
ret = cb_ret
elseif mode == 2 then
if not cb_ret or i == 1 then
ret = cb_ret
end
elseif mode == 3 then
if cb_ret then
return cb_ret
end
ret = cb_ret
elseif mode == 4 then
if (cb_ret and not ret) or i == 1 then
ret = cb_ret
end
elseif mode == 5 and cb_ret then
return cb_ret
end
end
return ret
end
function builtin_shared.make_registration()
local t = {}
local registerfunc = function(func)
t[#t + 1] = func
core.callback_origins[func] = {
-- may be nil or return nil
mod = core.get_current_modname and core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??"
}
end
return t, registerfunc
end
function builtin_shared.make_registration_reverse()
local t = {}
local registerfunc = function(func)
table.insert(t, 1, func)
core.callback_origins[func] = {
-- may be nil or return nil
mod = core.get_current_modname and core.get_current_modname() or "??",
name = debug.getinfo(1, "n").name or "??"
}
end
return t, registerfunc
end

View file

@ -204,18 +204,18 @@ local function dummy_func() end
function core.deserialize(str, safe)
-- Backwards compatibility
if str == nil then
core.log("deprecated", "core.deserialize called with nil (expected string).")
core.log("deprecated", "minetest.deserialize called with nil (expected string).")
return nil, "Invalid type: Expected a string, got nil"
end
local t = type(str)
if t ~= "string" then
error(("core.deserialize called with %s (expected string)."):format(t))
error(("minetest.deserialize called with %s (expected string)."):format(t))
end
local func, err = loadstring(str)
if not func then return nil, err end
-- math.huge was serialized to inf and NaNs to nan by Lua in engine version 5.6, so we have to support this here
-- math.huge was serialized to inf and NaNs to nan by Lua in Minetest 5.6, so we have to support this here
local env = {inf = math_huge, nan = 0/0}
if safe then
env.loadstring = dummy_func

View file

@ -1,463 +0,0 @@
--Luanti
--Copyright (C) 2022 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local make = {}
-- This file defines various component constructors, of the form:
--
-- make.component(setting)
--
-- `setting` is a table representing the settingtype.
--
-- A component is a table with the following:
--
-- * `full_width`: (Optional) true if the component shouldn't reserve space for info / reset.
-- * `info_text`: (Optional) string, informational text shown in an info icon.
-- * `setting`: (Optional) the setting.
-- * `max_w`: (Optional) maximum width, `avail_w` will never exceed this.
-- * `resettable`: (Optional) if this is true, a reset button is shown.
-- * `get_formspec = function(self, avail_w)`:
-- * `avail_w` is the available width for the component.
-- * Returns `fs, used_height`.
-- * `fs` is a string for the formspec.
-- Components should be relative to `0,0`, and not exceed `avail_w` or the returned `used_height`.
-- * `used_height` is the space used by components in `fs`.
-- * `on_submit = function(self, fields, parent)`:
-- * `fields`: submitted formspec fields
-- * `parent`: the fstk element for the settings UI, use to show dialogs
-- * Return true if the event was handled, to prevent future components receiving it.
local function get_label(setting)
local show_technical_names = core.settings:get_bool("show_technical_names")
if not show_technical_names and setting.readable_name then
return fgettext(setting.readable_name)
end
return setting.name
end
local function is_valid_number(value)
return type(value) == "number" and not (value ~= value or value >= math.huge or value <= -math.huge)
end
function make.heading(text)
return {
full_width = true,
get_formspec = function(self, avail_w)
return ("label[0,0.6;%s]box[0,0.9;%f,0.05;#ccc6]"):format(core.formspec_escape(text), avail_w), 1.2
end,
}
end
function make.note(text)
return {
full_width = true,
get_formspec = function(self, avail_w)
-- Assuming label height 0.4:
-- Position at y=0 to eat 0.2 of the padding above, leave 0.05.
-- The returned used_height doesn't include padding.
return ("label[0,0;%s]"):format(core.colorize("#bbb", core.formspec_escape(text))), 0.2
end,
}
end
--- Used for string and numeric style fields
---
--- @param converter Function to coerce values from strings.
--- @param validator Validator function, optional. Returns true when valid.
--- @param stringifier Function to convert values to strings, optional.
local function make_field(converter, validator, stringifier)
return function(setting)
return {
info_text = setting.comment,
setting = setting,
get_formspec = function(self, avail_w)
local value = core.settings:get(setting.name) or setting.default
self.resettable = core.settings:has(setting.name)
local fs = ("field[0,0.3;%f,0.8;%s;%s;%s]"):format(
avail_w - 1.5, setting.name, get_label(setting), core.formspec_escape(value))
fs = fs .. ("field_enter_after_edit[%s;true]"):format(setting.name)
fs = fs .. ("field_close_on_enter[%s;false]"):format(setting.name) -- for pause menu env
fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 1.5, "set_" .. setting.name, fgettext("Set"))
return fs, 1.1
end,
on_submit = function(self, fields)
if fields["set_" .. setting.name] or fields.key_enter_field == setting.name then
local value = converter(fields[setting.name])
if value == nil or (validator and not validator(value)) then
return true
end
if setting.min then
value = math.max(value, setting.min)
end
if setting.max then
value = math.min(value, setting.max)
end
core.settings:set(setting.name, (stringifier or tostring)(value))
return true
end
end,
}
end
end
make.float = make_field(tonumber, is_valid_number, function(x)
local str = tostring(x)
if str:match("^[+-]?%d+$") then
str = str .. ".0"
end
return str
end)
make.int = make_field(function(x)
local value = tonumber(x)
return value and math.floor(value)
end, is_valid_number)
make.string = make_field(tostring, nil)
function make.bool(setting)
return {
info_text = setting.comment,
setting = setting,
get_formspec = function(self, avail_w)
local value = core.settings:get_bool(setting.name, core.is_yes(setting.default))
self.resettable = core.settings:has(setting.name)
local fs = ("checkbox[0,0.25;%s;%s;%s]"):format(
setting.name, get_label(setting), tostring(value))
return fs, 0.5
end,
on_submit = function(self, fields)
if fields[setting.name] == nil then
return false
end
core.settings:set_bool(setting.name, core.is_yes(fields[setting.name]))
return true
end,
}
end
function make.enum(setting)
return {
info_text = setting.comment,
setting = setting,
max_w = 4.5,
get_formspec = function(self, avail_w)
local value = core.settings:get(setting.name) or setting.default
self.resettable = core.settings:has(setting.name)
local labels = setting.option_labels or {}
local items = {}
for i, option in ipairs(setting.values) do
items[i] = core.formspec_escape(labels[option] or option)
end
local selected_idx = table.indexof(setting.values, value)
local fs = "label[0,0.1;" .. get_label(setting) .. "]"
fs = fs .. ("dropdown[0,0.3;%f,0.8;%s;%s;%d;true]"):format(
avail_w, setting.name, table.concat(items, ","), selected_idx, value)
return fs, 1.1
end,
on_submit = function(self, fields)
local old_value = core.settings:get(setting.name) or setting.default
local idx = tonumber(fields[setting.name]) or 0
local value = setting.values[idx]
if value == nil or value == old_value then
return false
end
core.settings:set(setting.name, value)
return true
end,
}
end
local function make_path(setting)
return {
info_text = setting.comment,
setting = setting,
get_formspec = function(self, avail_w)
local value = core.settings:get(setting.name) or setting.default
self.resettable = core.settings:has(setting.name)
local fs = ("field[0,0.3;%f,0.8;%s;%s;%s]"):format(
avail_w - 3, setting.name, get_label(setting), core.formspec_escape(value))
fs = fs .. ("field_enter_after_edit[%s;true]"):format(setting.name)
fs = fs .. ("field_close_on_enter[%s;false]"):format(setting.name) -- for pause menu env
fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 3, "pick_" .. setting.name, fgettext("Browse"))
fs = fs .. ("button[%f,0.3;1.5,0.8;%s;%s]"):format(avail_w - 1.5, "set_" .. setting.name, fgettext("Set"))
return fs, 1.1
end,
on_submit = function(self, fields)
local dialog_name = "dlg_path_" .. setting.name
if fields["pick_" .. setting.name] then
local is_file = setting.type ~= "path"
core.show_path_select_dialog(dialog_name,
is_file and fgettext_ne("Select file") or fgettext_ne("Select directory"), is_file)
return true
end
if fields[dialog_name .. "_accepted"] then
local value = fields[dialog_name .. "_accepted"]
if value ~= nil then
core.settings:set(setting.name, value)
end
return true
end
if fields["set_" .. setting.name] or fields.key_enter_field == setting.name then
local value = fields[setting.name]
if value ~= nil then
core.settings:set(setting.name, value)
end
return true
end
end,
}
end
if PLATFORM == "Android" or INIT == "pause_menu" then
-- The Irrlicht file picker doesn't work on Android.
-- Access to the Irrlicht file picker isn't implemented in the pause menu.
-- We want to delete the Irrlicht file picker anyway, so any time spent on
-- that would be wasted.
make.path = make.string
make.filepath = make.string
else
make.path = make_path
make.filepath = make_path
end
function make.v3f(setting)
return {
info_text = setting.comment,
setting = setting,
get_formspec = function(self, avail_w)
local value = vector.from_string(core.settings:get(setting.name) or setting.default)
self.resettable = core.settings:has(setting.name)
-- Allocate space for "Set" button
avail_w = avail_w - 1
local fs = "label[0,0.1;" .. get_label(setting) .. "]"
local field_width = (avail_w - 3*0.25) / 3
fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
0, field_width, setting.name .. "_x", "X", value.x)
fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
field_width + 0.25, field_width, setting.name .. "_y", "Y", value.y)
fs = fs .. ("field[%f,0.6;%f,0.8;%s;%s;%s]"):format(
2 * (field_width + 0.25), field_width, setting.name .. "_z", "Z", value.z)
fs = fs .. ("field_enter_after_edit[%s;true]"):format(setting.name .. "_x")
fs = fs .. ("field_enter_after_edit[%s;true]"):format(setting.name .. "_y")
fs = fs .. ("field_enter_after_edit[%s;true]"):format(setting.name .. "_z")
-- for pause menu env
fs = fs .. ("field_close_on_enter[%s;false]"):format(setting.name .. "_x")
fs = fs .. ("field_close_on_enter[%s;false]"):format(setting.name .. "_y")
fs = fs .. ("field_close_on_enter[%s;false]"):format(setting.name .. "_z")
fs = fs .. ("button[%f,0.6;1,0.8;%s;%s]"):format(avail_w, "set_" .. setting.name, fgettext("Set"))
return fs, 1.4
end,
on_submit = function(self, fields)
if fields["set_" .. setting.name] or
fields.key_enter_field == setting.name .. "_x" or
fields.key_enter_field == setting.name .. "_y" or
fields.key_enter_field == setting.name .. "_z" then
local x = tonumber(fields[setting.name .. "_x"])
local y = tonumber(fields[setting.name .. "_y"])
local z = tonumber(fields[setting.name .. "_z"])
if is_valid_number(x) and is_valid_number(y) and is_valid_number(z) then
core.settings:set(setting.name, vector.new(x, y, z):to_string())
else
core.log("error", "Invalid vector: " .. dump({x, y, z}))
end
return true
end
end,
}
end
function make.flags(setting)
local checkboxes = {}
return {
info_text = setting.comment,
setting = setting,
get_formspec = function(self, avail_w)
local fs = {
"label[0,0.1;" .. get_label(setting) .. "]",
}
self.resettable = core.settings:has(setting.name)
checkboxes = {}
for _, name in ipairs(setting.possible) do
checkboxes[name] = false
end
local function apply_flags(flag_string, what)
local prefixed_flags = {}
for _, name in ipairs(flag_string:split(",")) do
prefixed_flags[name:trim()] = true
end
for _, name in ipairs(setting.possible) do
local enabled = prefixed_flags[name]
local disabled = prefixed_flags["no" .. name]
if enabled and disabled then
core.log("warning", "Flag " .. name .. " in " .. what .. " " ..
setting.name .. " both enabled and disabled, ignoring")
elseif enabled then
checkboxes[name] = true
elseif disabled then
checkboxes[name] = false
end
end
end
-- First apply the default, which is necessary since flags
-- which are not overridden may be missing from the value.
apply_flags(setting.default, "default for setting")
local value = core.settings:get(setting.name)
if value then
apply_flags(value, "setting")
end
local columns = math.max(math.floor(avail_w / 2.5), 1)
local column_width = avail_w / columns
local x = 0
local y = 0.55
for _, possible in ipairs(setting.possible) do
if x >= avail_w then
x = 0
y = y + 0.5
end
local is_checked = checkboxes[possible]
fs[#fs + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format(
x, y, setting.name .. "_" .. possible,
core.formspec_escape(possible), tostring(is_checked))
x = x + column_width
end
return table.concat(fs, ""), y + 0.25
end,
on_submit = function(self, fields)
local changed = false
for name, _ in pairs(checkboxes) do
local value = fields[setting.name .. "_" .. name]
if value ~= nil then
checkboxes[name] = core.is_yes(value)
changed = true
end
end
if changed then
local values = {}
for _, name in ipairs(setting.possible) do
if checkboxes[name] then
table.insert(values, name)
else
table.insert(values, "no" .. name)
end
end
core.settings:set(setting.name, table.concat(values, ","))
end
return changed
end
}
end
local function make_noise_params(setting)
return {
info_text = setting.comment,
setting = setting,
get_formspec = function(self, avail_w)
-- The "defaults" noise parameter flag doesn't reset a noise
-- setting to its default value, so we offer a regular reset button.
self.resettable = core.settings:has(setting.name)
local fs = "label[0,0.4;" .. get_label(setting) .. "]" ..
("button[%f,0;2.5,0.8;%s;%s]"):format(avail_w - 2.5, "edit_" .. setting.name, fgettext("Edit"))
return fs, 0.8
end,
on_submit = function(self, fields, tabview)
if fields["edit_" .. setting.name] then
local dlg = create_change_mapgen_flags_dlg(setting)
dlg:set_parent(tabview)
tabview:hide()
dlg:show()
return true
end
end,
}
end
if INIT == "pause_menu" then
-- Making the noise parameter dialog work in the pause menu settings would
-- require porting "FSTK" (at least the dialog API) from the mainmenu formspec
-- API to the in-game formspec API.
-- There's no reason you'd want to adjust mapgen noise parameter settings
-- in-game (they only apply to new worlds), so there's no reason to implement
-- this.
local empty = function()
return { get_formspec = function() return "", 0 end }
end
make.noise_params_2d = empty
make.noise_params_3d = empty
else
make.noise_params_2d = make_noise_params
make.noise_params_3d = make_noise_params
end
return make

View file

@ -1,252 +0,0 @@
--Luanti
--Copyright (C) 2015 PilzAdam
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local checkboxes = {}
local function flags_to_table(flags)
return flags:gsub("%s+", ""):split(",", true) -- Remove all spaces and split
end
local function get_current_np_group(setting)
local value = core.settings:get_np_group(setting.name)
if value == nil then
return setting.values
end
local p = "%g"
return {
p:format(value.offset),
p:format(value.scale),
p:format(value.spread.x),
p:format(value.spread.y),
p:format(value.spread.z),
p:format(value.seed),
p:format(value.octaves),
p:format(value.persistence),
p:format(value.lacunarity),
value.flags
}
end
local function get_formspec(dialogdata)
local setting = dialogdata.setting
-- Final formspec will be created at the end of this function
-- Default values below, may be changed depending on setting type
local width = 10
local height = 2
local description_height = 1.5
local t = get_current_np_group(setting)
local dimension = 3
if setting.type == "noise_params_2d" then
dimension = 2
end
local fields = {}
local function add_field(x, name, label, value)
fields[#fields + 1] = ("field[%f,%f;3.3,1;%s;%s;%s]"):format(
x, height, name, label, core.formspec_escape(value or "")
)
end
-- First row
height = height + 0.3
add_field(0.3, "te_offset", fgettext("Offset"), t[1])
add_field(3.6, "te_scale", fgettext("Scale"), t[2])
add_field(6.9, "te_seed", fgettext("Seed"), t[6])
height = height + 1.1
-- Second row
add_field(0.3, "te_spreadx", fgettext("X spread"), t[3])
if dimension == 3 then
add_field(3.6, "te_spready", fgettext("Y spread"), t[4])
else
fields[#fields + 1] = "label[4," .. height - 0.2 .. ";" ..
fgettext("2D Noise") .. "]"
end
add_field(6.9, "te_spreadz", fgettext("Z spread"), t[5])
height = height + 1.1
-- Third row
add_field(0.3, "te_octaves", fgettext("Octaves"), t[7])
add_field(3.6, "te_persist", fgettext("Persistence"), t[8])
add_field(6.9, "te_lacun", fgettext("Lacunarity"), t[9])
height = height + 1.1
local enabled_flags = flags_to_table(t[10])
local flags = {}
for _, name in ipairs(enabled_flags) do
-- Index by name, to avoid iterating over all enabled_flags for every possible flag.
flags[name] = true
end
for _, name in ipairs(setting.flags) do
local checkbox_name = "cb_" .. name
local is_enabled = flags[name] == true -- to get false if nil
checkboxes[checkbox_name] = is_enabled
end
local formspec = table.concat(fields)
.. "checkbox[0.5," .. height - 0.6 .. ";cb_defaults;"
--[[~ "defaults" is a noise parameter flag.
It describes the default processing options
for noise settings in the settings menu. ]]
.. fgettext("defaults") .. ";" -- defaults
.. tostring(flags["defaults"] == true) .. "]" -- to get false if nil
.. "checkbox[5," .. height - 0.6 .. ";cb_eased;"
--[[~ "eased" is a noise parameter flag.
It is used to make the map smoother and
can be enabled in noise settings in
the settings menu. ]]
.. fgettext("eased") .. ";" -- eased
.. tostring(flags["eased"] == true) .. "]"
.. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;"
--[[~ "absvalue" is a noise parameter flag.
It is short for "absolute value".
It can be enabled in noise settings in
the settings menu. ]]
.. fgettext("absvalue") .. ";" -- absvalue
.. tostring(flags["absvalue"] == true) .. "]"
height = height + 1
-- Box good, textarea bad. Calculate textarea size from box.
local function create_textfield(size, label, text, bg_color)
local textarea = {
x = size.x + 0.3,
y = size.y,
w = size.w + 0.25,
h = size.h * 1.16 + 0.12
}
return ("box[%f,%f;%f,%f;%s]textarea[%f,%f;%f,%f;;%s;%s]"):format(
size.x, size.y, size.w, size.h, bg_color or "#000",
textarea.x, textarea.y, textarea.w, textarea.h,
core.formspec_escape(label), core.formspec_escape(text)
)
end
-- When there's an error: Shrink description textarea and add error below
if dialogdata.error_message then
local error_box = {
x = 0,
y = description_height - 0.4,
w = width - 0.25,
h = 0.5
}
formspec = formspec ..
create_textfield(error_box, "", dialogdata.error_message, "#600")
description_height = description_height - 0.75
end
-- Get description field
local description_box = {
x = 0,
y = 0.2,
w = width - 0.25,
h = description_height
}
local setting_name = setting.name
if setting.readable_name then
setting_name = fgettext_ne(setting.readable_name) ..
" (" .. setting.name .. ")"
end
local comment_text
if setting.comment == "" then
comment_text = fgettext_ne("(No description of setting given)")
else
comment_text = fgettext_ne(setting.comment)
end
return (
"size[" .. width .. "," .. height + 0.25 .. ",true]" ..
create_textfield(description_box, setting_name, comment_text) ..
formspec ..
"button[" .. width / 2 - 2.5 .. "," .. height - 0.4 .. ";2.5,1;btn_done;" ..
fgettext("Save") .. "]" ..
"button[" .. width / 2 .. "," .. height - 0.4 .. ";2.5,1;btn_cancel;" ..
fgettext("Cancel") .. "]"
)
end
local function buttonhandler(this, fields)
local setting = this.data.setting
if fields["btn_done"] or fields["key_enter"] then
local np_flags = {}
for _, name in ipairs(setting.flags) do
if checkboxes["cb_" .. name] then
table.insert(np_flags, name)
end
end
checkboxes = {}
if setting.type == "noise_params_2d" then
fields["te_spready"] = fields["te_spreadz"]
end
local new_value = {
offset = fields["te_offset"],
scale = fields["te_scale"],
spread = {
x = fields["te_spreadx"],
y = fields["te_spready"],
z = fields["te_spreadz"]
},
seed = fields["te_seed"],
octaves = fields["te_octaves"],
persistence = fields["te_persist"],
lacunarity = fields["te_lacun"],
flags = table.concat(np_flags, ", ")
}
core.settings:set_np_group(setting.name, new_value)
core.settings:write()
this:delete()
return true
end
if fields["btn_cancel"] then
this:delete()
return true
end
for name, value in pairs(fields) do
if name:sub(1, 3) == "cb_" then
checkboxes[name] = core.is_yes(value)
return false -- Don't update the formspec!
end
end
return false
end
function create_change_mapgen_flags_dlg(setting)
assert(type(setting) == "table")
local retval = dialog_create("dlg_change_mapgen_flags",
get_formspec,
buttonhandler,
nil)
retval.data.setting = setting
return retval
end

Some files were not shown because too many files have changed in this diff Show more