Merge branch 'master' into doc_ide

This commit is contained in:
AFCMS 2024-05-16 16:13:57 +02:00 committed by GitHub
commit 1715a9335e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
659 changed files with 128140 additions and 11721 deletions

3
.gitattributes vendored
View File

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

View File

@ -8,6 +8,8 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'android/**'
@ -32,8 +34,17 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends gettext openjdk-11-jdk-headless
- name: Build with Gradle
- 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".
run: cd android; ./gradlew assemblerelease
- name: Save AAB artifact
uses: actions/upload-artifact@v4
with:
name: Minetest-release.aab
path: android/app/build/outputs/bundle/release/app-release.aab
- name: Save armeabi artifact
uses: actions/upload-artifact@v4
with:

View File

@ -8,6 +8,8 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/ci/**'

View File

@ -1,5 +1,4 @@
---
name: Docker Image
name: docker_image
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images
# https://docs.docker.com/build/ci/github-actions/multi-platform
@ -9,11 +8,28 @@ on:
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ "*.*.*" ]
tags: [ "*" ]
pull_request:
# Build docker image on pull requests to master. (but do not publish)
branches: [ "master" ]
# Build docker image on pull requests. (but do not publish)
paths:
- 'lib/**.[ch]'
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.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
@ -32,9 +48,6 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3.0.0
@ -57,16 +70,26 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
org.opencontainers.image.title=Minetest
org.opencontainers.image.vendor=Minetest
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,linux/arm64
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") minetestserver --version
shell: bash

View File

@ -8,10 +8,11 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/ci/**'
- 'misc/irrlichtmt_tag.txt'
- 'Dockerfile'
- '.dockerignore'
- '.github/workflows/linux.yml'
@ -24,7 +25,6 @@ on:
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/ci/**'
- 'misc/irrlichtmt_tag.txt'
- 'Dockerfile'
- '.dockerignore'
- '.github/workflows/linux.yml'
@ -108,7 +108,7 @@ jobs:
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-14 gdb
install_linux_deps clang-14 lldb
- name: Build
run: |
@ -151,13 +151,3 @@ jobs:
- name: Test
run: |
./bin/minetestserver --run-unittests
docker:
name: "Docker image"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Build docker image
run: |
docker build . -t minetest:latest
docker run --rm minetest:latest /usr/local/bin/minetestserver --version

View File

@ -14,24 +14,24 @@ on:
- '.github/workflows/**.yml'
jobs:
# Note that the integration tests are also run build.yml, but only when C++ code is changed.
# Note that the integration tests are also run in build.yml, but only when C++ code is changed.
integration_tests:
name: "Compile and run multiplayer tests"
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source ./util/ci/common.sh
install_linux_deps clang-10 gdb libluajit-5.1-dev
install_linux_deps clang gdb libluajit-5.1-dev
- name: Build
run: |
./util/ci/build.sh
env:
CC: clang-10
CXX: clang++-10
CMAKE_FLAGS: "-DENABLE_GETTEXT=0 -DBUILD_SERVER=0"
CC: clang
CXX: clang++
CMAKE_FLAGS: "-DENABLE_GETTEXT=0 -DBUILD_SERVER=0 -DBUILD_UNITTESTS=0"
- name: Integration test + devtest
run: |

View File

@ -16,8 +16,8 @@ on:
jobs:
build:
runs-on: ubuntu-22.04
if: github.repository == 'minetest/minetest'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View File

@ -8,6 +8,8 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- '.github/workflows/macos.yml'
@ -33,7 +35,6 @@ jobs:
- name: Build
run: |
git clone https://github.com/minetest/irrlicht lib/irrlichtmt --depth 1 -b $(cat misc/irrlichtmt_tag.txt)
mkdir build
cd build
cmake .. \
@ -54,6 +55,7 @@ jobs:
- name: CPack
run: |
cd build
cmake .. -DINSTALL_DEVTEST=FALSE
cpack -G ZIP -B macos
- uses: actions/upload-artifact@v4

View File

@ -8,10 +8,11 @@ on:
- 'lib/**.cpp'
- 'src/**.[ch]'
- 'src/**.cpp'
- 'irr/**.[ch]'
- 'irr/**.cpp'
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/buildbot/**'
- 'misc/irrlichtmt_tag.txt'
- 'misc/*.manifest'
- '.github/workflows/windows.yml'
pull_request:
@ -23,48 +24,42 @@ on:
- '**/CMakeLists.txt'
- 'cmake/Modules/**'
- 'util/buildbot/**'
- 'misc/irrlichtmt_tag.txt'
- 'misc/*.manifest'
- '.github/workflows/windows.yml'
jobs:
mingw32:
name: "MinGW cross-compiler (32-bit)"
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 apt-get update && sudo apt-get install -y gettext
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/buildwin32.sh B
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"/minetest-*-win*
wine bin/minetest.exe --version
- uses: actions/upload-artifact@v4
with:
name: mingw32
path: B/build/*.zip
if-no-files-found: error
mingw64:
name: "MinGW cross-compiler (64-bit)"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install compiler
run: |
sudo apt-get update && sudo apt-get install -y gettext
sudo ./util/buildbot/download_toolchain.sh /usr
- name: Build
run: |
EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin64.sh B
- uses: actions/upload-artifact@v4
with:
name: mingw64
name: "mingw${{ matrix.bits }}"
path: B/build/*.zip
if-no-files-found: error
@ -97,10 +92,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- 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]
# Workaround for regression, see https://github.com/minetest/minetest/pull/14536
- name: Pin CMake to 3.28
uses: lukka/get-cmake@latest
with:
cmakeVersion: "~3.28.0"
- name: Restore from cache and run vcpkg
uses: lukka/run-vcpkg@v7

3
.gitignore vendored
View File

@ -68,6 +68,7 @@ AppDir
/clientmods/*
!/clientmods/preview/
/client/mod_storage/
/mod_data
## Configuration/log files
minetest.conf
@ -125,7 +126,7 @@ compile_commands.json
*.sln
.vs/
# Optional user provided library folder
# Old irrlichtmt. Still ignored to make bisecting easier.
lib/irrlichtmt
# Generated mod storage database

View File

@ -91,28 +91,10 @@ if(ANDROID)
endif()
set(IRRLICHTMT_BUILD_DIR "" CACHE PATH "Path to IrrlichtMt build directory.")
if(ANDROID)
# currently manually provided
elseif(NOT "${IRRLICHTMT_BUILD_DIR}" STREQUAL "")
find_package(IrrlichtMt QUIET
PATHS "${IRRLICHTMT_BUILD_DIR}"
NO_DEFAULT_PATH
)
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(TRUE)
message(STATUS "Using imported IrrlichtMt at subdirectory 'irr'")
if(BUILD_CLIENT)
# 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)
add_subdirectory(irr EXCLUDE_FROM_ALL)
if(NOT TARGET IrrlichtMt)
message(FATAL_ERROR "IrrlichtMt project is missing a CMake target?!")
@ -120,44 +102,7 @@ elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/irrlichtmt")
else()
add_library(IrrlichtMt::IrrlichtMt INTERFACE IMPORTED)
set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES
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
"You must install IrrlichMt as described in docs/compiling/\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(ANDROID)
# skipped for now
elseif(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.0mt15)
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")
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/irr/include")
endif()
endif()

View File

@ -1,8 +1,6 @@
ARG DOCKER_IMAGE=alpine:3.19
FROM $DOCKER_IMAGE AS dev
ENV IRRLICHT_VERSION master
ENV SPATIALINDEX_VERSION master
ENV LUAJIT_VERSION v2.1
RUN apk add --no-cache git build-base cmake curl-dev zlib-dev zstd-dev \
@ -20,7 +18,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 -b ${SPATIALINDEX_VERSION} && \
git clone --recursive https://github.com/libspatialindex/libspatialindex && \
cd libspatialindex && \
cmake -B build \
-DCMAKE_INSTALL_PREFIX=/usr/local && \
@ -30,9 +28,7 @@ RUN git clone --recursive https://github.com/jupp0r/prometheus-cpp && \
git clone --recursive https://luajit.org/git/luajit.git -b ${LUAJIT_VERSION} && \
cd luajit && \
make amalg && make install && \
cd /usr/src/ && \
git clone --depth=1 https://github.com/minetest/irrlicht -b ${IRRLICHT_VERSION} && \
cp -r irrlicht/include /usr/include/irrlichtmt
cd /usr/src/
FROM dev as builder
@ -48,6 +44,7 @@ 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 irr /usr/src/minetest/irr
COPY textures /usr/src/minetest/textures
WORKDIR /usr/src/minetest

View File

@ -127,9 +127,7 @@ Docker
------
- [Developing minetestserver with Docker](doc/developing/docker.md)
We provide a Dockerfile that can be used to build the server.
- [Running a server with Docker](doc/docker_server.md)
Version scheme
--------------

View File

@ -0,0 +1,18 @@
diff --git a/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/android/app/src/main/java/org/libsdl/app/SDLActivity.java
index fd5a056e3..83e3cf657 100644
--- 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 MINETEST
+ * 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

@ -75,7 +75,7 @@ task prepareAssets() {
from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders"
}
copy {
from "../native/deps/armeabi-v7a/Irrlicht/Shaders" into "${assetsFolder}/client/shaders/Irrlicht"
from "${projRoot}/irr/media/Shaders" into "${assetsFolder}/client/shaders/Irrlicht"
}
copy {
from "${projRoot}/fonts" include "*.ttf" into "${assetsFolder}/fonts"

View File

@ -20,7 +20,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
package net.minetest.minetest;
import android.app.NativeActivity;
import org.libsdl.app.SDLActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@ -32,6 +33,7 @@ import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.LinearLayout;
import android.content.res.Configuration;
import androidx.annotation.Keep;
import androidx.appcompat.app.AlertDialog;
@ -45,12 +47,29 @@ import java.util.Objects;
// This annotation prevents the minifier/Proguard from mangling them.
@Keep
@SuppressWarnings("unused")
public class GameActivity extends NativeActivity {
static {
System.loadLibrary("c++_shared");
System.loadLibrary("minetest");
public class GameActivity extends SDLActivity {
@Override
protected String getMainSharedObject() {
return getContext().getApplicationInfo().nativeLibraryDir + "/libminetest.so";
}
@Override
protected String getMainFunction() {
return "SDL_Main";
}
@Override
protected String[] getLibraries() {
return new String[] {
"minetest"
};
}
// 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 }
@ -59,32 +78,6 @@ public class GameActivity extends NativeActivity {
private String messageReturnValue = "";
private int selectionReturnValue = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
private void makeFullScreen() {
this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus)
makeFullScreen();
}
@Override
protected void onResume() {
super.onResume();
makeFullScreen();
}
private native void saveSettings();
@Override
@ -96,11 +89,6 @@ public class GameActivity extends NativeActivity {
saveSettings();
}
@Override
public void onBackPressed() {
// Ignore the back press so Minetest can handle it
}
public void showTextInputDialog(String hint, String current, int editType) {
runOnUiThread(() -> showTextInputDialogUI(hint, current, editType));
}
@ -265,4 +253,8 @@ public class GameActivity extends NativeActivity {
return langCode;
}
public boolean hasPhysicalKeyboard() {
return getContext().getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS;
}
}

View File

@ -0,0 +1,22 @@
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

@ -0,0 +1,650 @@
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

@ -0,0 +1,691 @@
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
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;
}
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

@ -0,0 +1,309 @@
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

@ -0,0 +1,86 @@
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 {
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 = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = mContext.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = mContext.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = mContext.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, mContext, 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

@ -0,0 +1,514 @@
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

@ -0,0 +1,854 @@
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.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

@ -0,0 +1,405 @@
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,7 +1,7 @@
<#if isLowMemory>
org.gradle.jvmargs=-Xmx4G -XX:MaxPermSize=2G -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
<#else>
org.gradle.jvmargs=-Xmx16G -XX:MaxPermSize=8G -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx16G -XX:+HeapDumpOnOutOfMemoryError
</#if>
org.gradle.daemon=true
org.gradle.parallel=true

View File

@ -12,7 +12,7 @@ android {
cmake {
arguments "-DANDROID_STL=c++_shared",
"-DENABLE_CURL=1", "-DENABLE_SOUND=1",
"-DENABLE_TOUCH=1", "-DENABLE_GETTEXT=1",
"-DENABLE_GETTEXT=1",
"-DBUILD_UNITTESTS=0", "-DENABLE_UPDATE_CHECKER=0"
}
}
@ -52,7 +52,7 @@ if (new File(depsDir, 'armeabi-v7a').exists()) {
task downloadDeps(type: Download) {
def depsZip = new File(buildDir, 'deps.zip')
src 'https://github.com/minetest/minetest_android_deps/releases/download/latest/deps.zip'
src 'https://github.com/minetest/minetest_android_deps/releases/download/latest/deps-lite.zip'
dest depsZip
overwrite false

View File

@ -490,6 +490,9 @@ 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

View File

@ -2,6 +2,9 @@
core.detached_inventories = {}
local create_detached_inventory_raw = core.create_detached_inventory_raw
core.create_detached_inventory_raw = nil
function core.create_detached_inventory(name, callbacks, player_name)
local stuff = {}
stuff.name = name
@ -15,10 +18,13 @@ function core.create_detached_inventory(name, callbacks, player_name)
end
stuff.mod_origin = core.get_current_modname() or "??"
core.detached_inventories[name] = stuff
return core.create_detached_inventory_raw(name, player_name)
return create_detached_inventory_raw(name, player_name)
end
local remove_detached_inventory_raw = core.remove_detached_inventory_raw
core.remove_detached_inventory_raw = nil
function core.remove_detached_inventory(name)
core.detached_inventories[name] = nil
return core.remove_detached_inventory_raw(name)
return remove_detached_inventory_raw(name)
end

View File

@ -37,6 +37,10 @@ core.features = {
blocking_pointability_type = true,
dynamic_add_media_startup = true,
dynamic_add_media_filepath = true,
lsystem_decoration_type = true,
item_meta_range = true,
node_interaction_actor = true,
moveresult_new_pos = true,
}
function core.has_feature(arg)

261
builtin/game/hud.lua Normal file
View File

@ -0,0 +1,261 @@
--[[
Register function to easily register new builtin hud elements
`def` is a table and contains the following fields:
elem_def the HUD element definition which can be changed with hud_replace_builtin
events (optional) additional event names on which the element will be updated
("hud_changed" will always be used.)
show_elem(player, flags, id)
(optional) a function to decide if the element should be shown to a player
It is called before the element gets updated.
update_def(player, elem_def)
(optional) a function to change the elem_def before it will be used.
(elem_def can be changed, since the table which got set by using
hud_replace_builtin isn't exposed to the API.)
update_elem(player, id)
(optional) a function to change the element after it has been updated
(Is not called when the element is first set or recreated.)
]]--
local registered_elements = {}
local update_events = {}
local function register_builtin_hud_element(name, def)
registered_elements[name] = def
for _, event in ipairs(def.events or {}) do
update_events[event] = update_events[event] or {}
table.insert(update_events[event], name)
end
end
-- Stores HUD ids for all players
local hud_ids = {}
-- Updates one element
-- In case the element is already added, it only calls the update_elem function from
-- registered_elements. (To recreate the element remove it first.)
local function update_element(player, player_hud_ids, elem_name, flags)
local def = registered_elements[elem_name]
local id = player_hud_ids[elem_name]
if def.show_elem and not def.show_elem(player, flags, id) then
if id then
player:hud_remove(id)
player_hud_ids[elem_name] = nil
end
return
end
if not id then
if def.update_def then
def.update_def(player, def.elem_def)
end
id = player:hud_add(def.elem_def)
player_hud_ids[elem_name] = id
return
end
if def.update_elem then
def.update_elem(player, id)
end
end
-- Updates all elements
-- If to_update is specified it will only update those elements.
local function update_hud(player, to_update)
local flags = player:hud_get_flags()
local playername = player:get_player_name()
hud_ids[playername] = hud_ids[playername] or {}
local player_hud_ids = hud_ids[playername]
if to_update then
for _, elem_name in ipairs(to_update) do
update_element(player, player_hud_ids, elem_name, flags)
end
else
for elem_name, _ in pairs(registered_elements) do
update_element(player, player_hud_ids, elem_name, flags)
end
end
end
local function player_event_handler(player, eventname)
assert(player:is_player())
if eventname == "hud_changed" then
update_hud(player)
return
end
-- Custom events
local to_update = update_events[eventname]
if to_update then
update_hud(player, to_update)
end
end
-- Returns true if successful, otherwise false,
-- but currently the return value is not documented in the Lua API.
function core.hud_replace_builtin(elem_name, elem_def)
assert(type(elem_def) == "table")
local registered = registered_elements[elem_name]
if not registered then
return false
end
registered.elem_def = table.copy(elem_def)
for playername, player_hud_ids in pairs(hud_ids) do
local player = core.get_player_by_name(playername)
local id = player_hud_ids[elem_name]
if player and id then
player:hud_remove(id)
player_hud_ids[elem_name] = nil
update_element(player, player_hud_ids, elem_name, player:hud_get_flags())
end
end
return true
end
local function cleanup_builtin_hud(player)
hud_ids[player:get_player_name()] = nil
end
-- Append "update_hud" as late as possible
-- This ensures that the HUD is hidden when the flags are updated in this callback
core.register_on_mods_loaded(function()
core.register_on_joinplayer(function(player)
update_hud(player)
end)
end)
core.register_on_leaveplayer(cleanup_builtin_hud)
core.register_playerevent(player_event_handler)
---- Builtin HUD Elements
--- Healthbar
-- Cache setting
local enable_damage = core.settings:get_bool("enable_damage")
local function scale_to_hud_max(player, field)
-- Scale "hp" or "breath" to the hud maximum dimensions
local current = player["get_" .. field](player)
local nominal
if field == "hp" then -- HUD is called health but field is hp
nominal = registered_elements.health.elem_def.item
else
nominal = registered_elements[field].elem_def.item
end
local max_display = math.max(player:get_properties()[field .. "_max"], current)
return math.ceil(current / max_display * nominal)
end
register_builtin_hud_element("health", {
elem_def = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "heart.png",
text2 = "heart_gone.png",
number = core.PLAYER_MAX_HP_DEFAULT,
item = core.PLAYER_MAX_HP_DEFAULT,
direction = 0,
size = {x = 24, y = 24},
offset = {x = (-10 * 24) - 25, y = -(48 + 24 + 16)},
},
events = {"properties_changed", "health_changed"},
show_elem = function(player, flags)
return flags.healthbar and enable_damage and
player:get_armor_groups().immortal ~= 1
end,
update_def = function(player, elem_def)
elem_def.item = elem_def.item or elem_def.number or core.PLAYER_MAX_HP_DEFAULT
elem_def.number = scale_to_hud_max(player, "hp")
end,
update_elem = function(player, id)
player:hud_change(id, "number", scale_to_hud_max(player, "hp"))
end,
})
--- Breathbar
-- Stores core.after calls for every player
local breathbar_removal_jobs = {}
register_builtin_hud_element("breath", {
elem_def = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "bubble.png",
text2 = "bubble_gone.png",
number = core.PLAYER_MAX_BREATH_DEFAULT * 2,
item = core.PLAYER_MAX_BREATH_DEFAULT * 2,
direction = 0,
size = {x = 24, y = 24},
offset = {x = 25, y= -(48 + 24 + 16)},
},
events = {"properties_changed", "breath_changed"},
show_elem = function(player, flags, id)
local show_breathbar = flags.breathbar and enable_damage and
player:get_armor_groups().immortal ~= 1
if id then
-- The element will not prematurely be removed by update_element
-- (but may still be instantly removed if the flag changed)
return show_breathbar
end
-- Don't add the element if the breath is full
local breath_relevant = player:get_breath() < player:get_properties().breath_max
return show_breathbar and breath_relevant
end,
update_def = function(player, elem_def)
elem_def.item = elem_def.item or elem_def.number or core.PLAYER_MAX_BREATH_DEFAULT
elem_def.number = scale_to_hud_max(player, "breath")
end,
update_elem = function(player, id)
player:hud_change(id, "number", scale_to_hud_max(player, "breath"))
local player_name = player:get_player_name()
local breath_relevant = player:get_breath() < player:get_properties().breath_max
if not breath_relevant then
if not breathbar_removal_jobs[player_name] then
-- The breathbar stays for some time and then gets removed.
breathbar_removal_jobs[player_name] = core.after(1, function()
local player = core.get_player_by_name(player_name)
local player_hud_ids = hud_ids[player_name]
if player and player_hud_ids and player_hud_ids.breath then
player:hud_remove(player_hud_ids.breath)
player_hud_ids.breath = nil
end
breathbar_removal_jobs[player_name] = nil
end)
end
else
-- Cancel removal
local job = breathbar_removal_jobs[player_name]
if job then
job:cancel()
breathbar_removal_jobs[player_name] = nil
end
end
end,
})
--- Minimap
register_builtin_hud_element("minimap", {
elem_def = {
type = "minimap",
position = {x = 1, y = 0},
alignment = {x = -1, y = 1},
offset = {x = -10, y = 10},
size = {x = 256, y = 256},
},
show_elem = function(player, flags)
-- Don't add a minimap for clients which already have it hardcoded in C++.
return flags.minimap and
core.get_player_information(player:get_player_name()).protocol_version >= 44
end,
})

View File

@ -35,7 +35,7 @@ assert(loadfile(gamepath .. "falling.lua"))(builtin_shared)
dofile(gamepath .. "features.lua")
dofile(gamepath .. "voxelarea.lua")
dofile(gamepath .. "forceloading.lua")
dofile(gamepath .. "statbars.lua")
dofile(gamepath .. "hud.lua")
dofile(gamepath .. "knockback.lua")
dofile(gamepath .. "async.lua")

View File

@ -22,14 +22,16 @@ local function vector_absmax(v)
return max(max(abs(v.x), abs(v.y)), abs(v.z))
end
core.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, unused_dir, damage)
core.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, dir, damage)
if player:get_hp() == 0 then
return -- RIP
end
-- Server::handleCommand_Interact() adds eye offset to one but not the other
-- so the direction is slightly off, calculate it ourselves
local dir = vector.subtract(player:get_pos(), hitter:get_pos())
if hitter then
-- Server::handleCommand_Interact() adds eye offset to one but not the other
-- so the direction is slightly off, calculate it ourselves
dir = vector.subtract(player:get_pos(), hitter:get_pos())
end
local d = vector.length(dir)
if d ~= 0.0 then
dir = vector.divide(dir, d)

View File

@ -97,3 +97,26 @@ function core.encode_png(width, height, data, compression)
return o_encode_png(width, height, data, compression or 6)
end
-- Helper that pushes a collisionMoveResult structure
if core.set_push_moveresult1 then
-- must match CollisionAxis in collision.h
local AXES = {"x", "y", "z"}
-- <=> script/common/c_content.cpp push_collision_move_result()
core.set_push_moveresult1(function(b0, b1, b2, axis, npx, npy, npz, v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z)
return {
touching_ground = b0,
collides = b1,
standing_on_object = b2,
collisions = {{
type = "node",
axis = AXES[axis],
node_pos = vector.new(npx, npy, npz),
new_pos = vector.new(v0x, v0y, v0z),
old_velocity = vector.new(v1x, v1y, v1z),
new_velocity = vector.new(v2x, v2y, v2z),
}},
}
end)
core.set_push_moveresult1 = nil
end

View File

@ -1,217 +0,0 @@
-- cache setting
local enable_damage = core.settings:get_bool("enable_damage")
local bar_definitions = {
hp = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "heart.png",
text2 = "heart_gone.png",
number = core.PLAYER_MAX_HP_DEFAULT,
item = core.PLAYER_MAX_HP_DEFAULT,
direction = 0,
size = {x = 24, y = 24},
offset = {x = (-10 * 24) - 25, y = -(48 + 24 + 16)},
},
breath = {
type = "statbar",
position = {x = 0.5, y = 1},
text = "bubble.png",
text2 = "bubble_gone.png",
number = core.PLAYER_MAX_BREATH_DEFAULT * 2,
item = core.PLAYER_MAX_BREATH_DEFAULT * 2,
direction = 0,
size = {x = 24, y = 24},
offset = {x = 25, y= -(48 + 24 + 16)},
},
minimap = {
type = "minimap",
position = {x = 1, y = 0},
alignment = {x = -1, y = 1},
offset = {x = -10, y = 10},
size = {x = 256 , y = 256},
},
}
local hud_ids = {}
local function scaleToHudMax(player, field)
-- Scale "hp" or "breath" to the hud maximum dimensions
local current = player["get_" .. field](player)
local nominal = bar_definitions[field].item
local max_display = math.max(player:get_properties()[field .. "_max"], current)
return math.ceil(current / max_display * nominal)
end
local function update_builtin_statbars(player)
local name = player:get_player_name()
if name == "" then
return
end
local flags = player:hud_get_flags()
if not hud_ids[name] then
hud_ids[name] = {}
-- flags are not transmitted to client on connect, we need to make sure
-- our current flags are transmitted by sending them actively
player:hud_set_flags(flags)
end
local hud = hud_ids[name]
local immortal = player:get_armor_groups().immortal == 1
if flags.healthbar and enable_damage and not immortal then
local number = scaleToHudMax(player, "hp")
if hud.id_healthbar == nil then
local hud_def = table.copy(bar_definitions.hp)
hud_def.number = number
hud.id_healthbar = player:hud_add(hud_def)
else
player:hud_change(hud.id_healthbar, "number", number)
end
elseif hud.id_healthbar then
player:hud_remove(hud.id_healthbar)
hud.id_healthbar = nil
end
local show_breathbar = flags.breathbar and enable_damage and not immortal
local breath = player:get_breath()
local breath_max = player:get_properties().breath_max
if show_breathbar and breath <= breath_max then
local number = scaleToHudMax(player, "breath")
if not hud.id_breathbar and breath < breath_max then
local hud_def = table.copy(bar_definitions.breath)
hud_def.number = number
hud.id_breathbar = player:hud_add(hud_def)
elseif hud.id_breathbar then
player:hud_change(hud.id_breathbar, "number", number)
end
end
if hud.id_breathbar and (not show_breathbar or breath == breath_max) then
core.after(1, function(player_name, breath_bar)
local player = core.get_player_by_name(player_name)
if player then
player:hud_remove(breath_bar)
end
end, name, hud.id_breathbar)
hud.id_breathbar = nil
end
-- Don't add a minimap for clients which already have it hardcoded in C++.
local show_minimap = flags.minimap and
minetest.get_player_information(name).protocol_version >= 44
if show_minimap and not hud.id_minimap then
hud.id_minimap = player:hud_add(bar_definitions.minimap)
elseif not show_minimap and hud.id_minimap then
player:hud_remove(hud.id_minimap)
hud.id_minimap = nil
end
end
local function cleanup_builtin_statbars(player)
local name = player:get_player_name()
if name == "" then
return
end
hud_ids[name] = nil
end
local function player_event_handler(player,eventname)
assert(player:is_player())
local name = player:get_player_name()
if name == "" or not hud_ids[name] then
return
end
if eventname == "health_changed" then
update_builtin_statbars(player)
if hud_ids[name].id_healthbar then
return true
end
end
if eventname == "breath_changed" then
update_builtin_statbars(player)
if hud_ids[name].id_breathbar then
return true
end
end
if eventname == "hud_changed" or eventname == "properties_changed" then
update_builtin_statbars(player)
return true
end
return false
end
function core.hud_replace_builtin(hud_name, definition)
if type(definition) ~= "table" then
return false
end
definition = table.copy(definition)
if hud_name == "health" then
definition.item = definition.item or definition.number or core.PLAYER_MAX_HP_DEFAULT
bar_definitions.hp = definition
for name, ids in pairs(hud_ids) do
local player = core.get_player_by_name(name)
if player and ids.id_healthbar then
player:hud_remove(ids.id_healthbar)
ids.id_healthbar = nil
update_builtin_statbars(player)
end
end
return true
end
if hud_name == "breath" then
definition.item = definition.item or definition.number or core.PLAYER_MAX_BREATH_DEFAULT
bar_definitions.breath = definition
for name, ids in pairs(hud_ids) do
local player = core.get_player_by_name(name)
if player and ids.id_breathbar then
player:hud_remove(ids.id_breathbar)
ids.id_breathbar = nil
update_builtin_statbars(player)
end
end
return true
end
if hud_name == "minimap" then
bar_definitions.minimap = definition
for name, ids in pairs(hud_ids) do
local player = core.get_player_by_name(name)
if player and ids.id_minimap then
player:hud_remove(ids.id_minimap)
ids.id_minimap = nil
update_builtin_statbars(player)
end
end
return true
end
return false
end
-- Append "update_builtin_statbars" as late as possible
-- This ensures that the HUD is hidden when the flags are updated in this callback
core.register_on_mods_loaded(function()
core.register_on_joinplayer(update_builtin_statbars)
end)
core.register_on_leaveplayer(cleanup_builtin_statbars)
core.register_playerevent(player_event_handler)

View File

@ -0,0 +1,84 @@
-- Table to keep track of callback executions
-- [i + 0] = count of expected patterns of index (i + 1)
-- [i + 1] = pattern to check
local PATTERN_NORMAL = { 4, "allow_%w", 2, "on_take", 1, "on_put", 1 }
local PATTERN_SWAP = { 8, "allow_%w", 4, "on_take", 2, "on_put", 2 }
local exec_listing = {} -- List of logged callbacks (e.g. "on_take", "allow_put")
-- Checks whether the logged callbacks equal the expected pattern
core.__helper_check_callbacks = function(expect_swap)
local exec_pattern = expect_swap and PATTERN_SWAP or PATTERN_NORMAL
local ok = #exec_listing == exec_pattern[1]
if ok then
local list_index = 1
for i = 2, #exec_pattern, 2 do
for n = 1, exec_pattern[i + 1] do
-- Match the list for "n" occurrences of the wanted callback name pattern
ok = exec_listing[list_index]:find(exec_pattern[i])
list_index = list_index + 1
if not ok then break end
end
if not ok then break end
end
end
if not ok then
print("Execution order mismatch!")
print("Expected patterns: ", dump(exec_pattern))
print("Got list: ", dump(exec_listing))
end
exec_listing = {}
return ok
end
-- Uncomment the other line for easier callback debugging
local log = function(...) end
--local log = print
minetest.register_allow_player_inventory_action(function(_, action, inv, info)
log("\tallow " .. action, info.count or info.stack:to_string())
if action == "move" then
-- testMoveFillStack
return info.count
end
if action == "take" or action == "put" then
assert(not info.stack:is_empty(), "Stack empty in: " .. action)
-- testMoveUnallowed
-- testSwapFromUnallowed
-- testSwapToUnallowed
if info.stack:get_name() == "default:takeput_deny" then
return 0
end
-- testMovePartial
if info.stack:get_name() == "default:takeput_max_5" then
return 5
end
-- testCallbacks
if info.stack:get_name():find("default:takeput_cb_%d") then
-- Log callback as executed
table.insert(exec_listing, "allow_" .. action)
return -- Unlimited
end
end
return -- Unlimited
end)
minetest.register_on_player_inventory_action(function(_, action, inv, info)
log("\ton " .. action, info.count or info.stack:to_string())
if action == "take" or action == "put" then
assert(not info.stack:is_empty(), action)
if info.stack:get_name():find("default:takeput_cb_%d") then
-- Log callback as executed
table.insert(exec_listing, "on_" .. action)
return
end
end
end)

View File

@ -26,7 +26,15 @@ do
core.print = nil -- don't pollute our namespace
end
end
math.randomseed(os.time())
do
-- Note that PUC Lua just calls srand() which is already initialized by C++,
-- but we don't want to rely on this implementation detail.
local seed = 1048576 * (os.time() % 1048576)
seed = seed + core.get_us_time() % 1048576
math.randomseed(seed)
end
minetest = core
-- Load other files

View File

@ -18,6 +18,26 @@
-- Global menu data
menudata = {}
-- located in user cache path, for remembering this like e.g. last update check
cache_settings = Settings(core.get_cache_path() .. DIR_DELIM .. "common.conf")
--- Checks if the given key contains a timestamp less than a certain age.
--- Pair this with a call to `cache_settings:set(key, tostring(os.time()))`
--- after successfully refreshing the cache.
--- @param key Name of entry in cache_settings
--- @param max_age Age to check against, in seconds
--- @return true if the max age is not reached
function check_cache_age(key, max_age)
local time_now = os.time()
local time_checked = tonumber(cache_settings:get(key)) or 0
return time_now - time_checked < max_age
end
function core.on_before_close()
-- called before the menu is closed, either exit or to join a game
cache_settings:write()
end
-- Local cached values
local min_supp_proto, max_supp_proto
@ -27,6 +47,16 @@ function common_update_cached_supp_proto()
end
common_update_cached_supp_proto()
-- Other global functions
function core.sound_stop(handle, ...)
return handle:stop(...)
end
function os.tmpname()
error('do not use') -- instead: core.get_temp_path()
end
-- Menu helper functions
local function render_client_count(n)
@ -140,11 +170,6 @@ function render_serverlist_row(spec)
return table.concat(details, ",")
end
---------------------------------------------------------------------------------
os.tmpname = function()
error('do not use') -- instead use core.get_temp_path()
end
--------------------------------------------------------------------------------
function menu_render_worldlist()
local retval = {}

View File

@ -0,0 +1,544 @@
--Minetest
--Copyright (C) 2018-24 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.
if not core.get_http_api then
return
end
contentdb = {
loading = false,
load_ok = false,
load_error = false,
-- Unordered preserves the original order of the ContentDB API,
-- before the package list is ordered based on installed state.
packages = {},
packages_full = {},
packages_full_unordered = {},
package_by_id = {},
aliases = {},
number_downloading = 0,
download_queue = {},
REASON_NEW = "new",
REASON_UPDATE = "update",
REASON_DEPENDENCY = "dependency",
}
local function get_download_url(package, reason)
local base_url = core.settings:get("contentdb_url")
local ret = base_url .. ("/packages/%s/releases/%d/download/"):format(
package.url_part, package.release)
if reason then
ret = ret .. "?reason=" .. reason
end
return ret
end
local function download_and_extract(param)
local package = param.package
local filename = core.get_temp_path(true)
if filename == "" or not core.download_file(param.url, filename) then
core.log("error", "Downloading " .. dump(param.url) .. " failed")
return {
msg = fgettext_ne("Failed to download \"$1\"", package.title)
}
end
local tempfolder = core.get_temp_path()
if tempfolder ~= "" and not core.extract_zip(filename, tempfolder) then
tempfolder = ""
end
os.remove(filename)
if tempfolder == "" then
return {
msg = fgettext_ne("Failed to extract \"$1\" " ..
"(insufficient disk space, unsupported file type or broken archive)",
package.title),
}
end
return {
path = tempfolder
}
end
local function start_install(package, reason)
local params = {
package = package,
url = get_download_url(package, reason),
}
contentdb.number_downloading = contentdb.number_downloading + 1
local function callback(result)
if result.msg then
gamedata.errormessage = result.msg
else
local path, msg = pkgmgr.install_dir(package.type, result.path, package.name, package.path)
core.delete_dir(result.path)
if not path then
gamedata.errormessage = fgettext_ne("Error installing \"$1\": $2", package.title, msg)
else
core.log("action", "Installed package to " .. path)
local conf_path
local name_is_title = false
if package.type == "mod" then
local actual_type = pkgmgr.get_folder_type(path)
if actual_type.type == "modpack" then
conf_path = path .. DIR_DELIM .. "modpack.conf"
else
conf_path = path .. DIR_DELIM .. "mod.conf"
end
elseif package.type == "game" then
conf_path = path .. DIR_DELIM .. "game.conf"
name_is_title = true
elseif package.type == "txp" then
conf_path = path .. DIR_DELIM .. "texture_pack.conf"
end
if conf_path then
local conf = Settings(conf_path)
if not conf:get("title") then
conf:set("title", package.title)
end
if not name_is_title then
conf:set("name", package.name)
end
if not conf:get("description") then
conf:set("description", package.short_description)
end
conf:set("author", package.author)
conf:set("release", package.release)
conf:write()
end
end
end
package.downloading = false
contentdb.number_downloading = contentdb.number_downloading - 1
local next = contentdb.download_queue[1]
if next then
table.remove(contentdb.download_queue, 1)
start_install(next.package, next.reason)
end
ui.update()
end
package.queued = false
package.downloading = true
if not core.handle_async(download_and_extract, params, callback) then
core.log("error", "ERROR: async event failed")
gamedata.errormessage = fgettext_ne("Failed to download $1", package.name)
return
end
end
function contentdb.queue_download(package, reason)
if package.queued or package.downloading then
return
end
local max_concurrent_downloads = tonumber(core.settings:get("contentdb_max_concurrent_downloads"))
if contentdb.number_downloading < math.max(max_concurrent_downloads, 1) then
start_install(package, reason)
else
table.insert(contentdb.download_queue, { package = package, reason = reason })
package.queued = true
end
end
function contentdb.get_package_by_id(id)
return contentdb.package_by_id[id]
end
local function get_raw_dependencies(package)
if package.type ~= "mod" then
return {}
end
if package.raw_deps then
return package.raw_deps
end
local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s"
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), core.urlencode(version.string))
local http = core.get_http_api()
local response = http.fetch_sync({ url = url })
if not response.succeeded then
core.log("error", "Unable to fetch dependencies for " .. package.url_part)
return
end
local data = core.parse_json(response.data) or {}
for id, raw_deps in pairs(data) do
local package2 = contentdb.package_by_id[id:lower()]
if package2 and not package2.raw_deps then
package2.raw_deps = raw_deps
for _, dep in pairs(raw_deps) do
local packages = {}
for i=1, #dep.packages do
packages[#packages + 1] = contentdb.package_by_id[dep.packages[i]:lower()]
end
dep.packages = packages
end
end
end
return package.raw_deps
end
function contentdb.has_hard_deps(package)
local raw_deps = get_raw_dependencies(package)
if not raw_deps then
return nil
end
for i=1, #raw_deps do
if not raw_deps[i].is_optional then
return true
end
end
return false
end
-- Recursively resolve dependencies, given the installed mods
local function resolve_dependencies_2(raw_deps, installed_mods, out)
local function resolve_dep(dep)
-- Check whether it's already installed
if installed_mods[dep.name] then
return {
is_optional = dep.is_optional,
name = dep.name,
installed = true,
}
end
-- Find exact name matches
local fallback
for _, package in pairs(dep.packages) do
if package.type ~= "game" then
if package.name == dep.name then
return {
is_optional = dep.is_optional,
name = dep.name,
installed = false,
package = package,
}
elseif not fallback then
fallback = package
end
end
end
-- Otherwise, find the first mod that fulfills it
if fallback then
return {
is_optional = dep.is_optional,
name = dep.name,
installed = false,
package = fallback,
}
end
return {
is_optional = dep.is_optional,
name = dep.name,
installed = false,
}
end
for _, dep in pairs(raw_deps) do
if not dep.is_optional and not out[dep.name] then
local result = resolve_dep(dep)
out[dep.name] = result
if result and result.package and not result.installed then
local raw_deps2 = get_raw_dependencies(result.package)
if raw_deps2 then
resolve_dependencies_2(raw_deps2, installed_mods, out)
end
end
end
end
return true
end
-- Resolve dependencies for a package, calls the recursive version.
function contentdb.resolve_dependencies(package, game)
assert(game)
local raw_deps = get_raw_dependencies(package)
local installed_mods = {}
local mods = {}
pkgmgr.get_game_mods(game, mods)
for _, mod in pairs(mods) do
installed_mods[mod.name] = true
end
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
installed_mods[mod.name] = true
end
local out = {}
if not resolve_dependencies_2(raw_deps, installed_mods, out) then
return nil
end
local retval = {}
for _, dep in pairs(out) do
retval[#retval + 1] = dep
end
table.sort(retval, function(a, b)
return a.name < b.name
end)
return retval
end
local function fetch_pkgs(params)
local version = core.get_version()
local base_url = core.settings:get("contentdb_url")
local url = base_url ..
"/api/packages/?type=mod&type=game&type=txp&protocol_version=" ..
core.get_max_supp_proto() .. "&engine_version=" .. core.urlencode(version.string)
for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do
item = item:trim()
if item ~= "" then
url = url .. "&hide=" .. core.urlencode(item)
end
end
local languages
local current_language = core.get_language()
if current_language ~= "" then
languages = { current_language, "en;q=0.8" }
else
languages = { "en" }
end
local http = core.get_http_api()
local response = http.fetch_sync({
url = url,
extra_headers = {
"Accept-Language: " .. table.concat(languages, ", ")
},
})
if not response.succeeded then
return
end
local packages = core.parse_json(response.data)
if not packages or #packages == 0 then
return
end
local aliases = {}
for _, package in pairs(packages) do
local name_len = #package.name
-- This must match what contentdb.update_paths() does!
package.id = package.author:lower() .. "/"
if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then
package.id = package.id .. package.name:sub(1, name_len - 5)
else
package.id = package.id .. package.name
end
package.url_part = core.urlencode(package.author) .. "/" .. core.urlencode(package.name)
if package.aliases then
for _, alias in ipairs(package.aliases) do
-- We currently don't support name changing
local suffix = "/" .. package.name
if alias:sub(-#suffix) == suffix then
aliases[alias:lower()] = package.id
end
end
end
end
return { packages = packages, aliases = aliases }
end
function contentdb.fetch_pkgs(callback)
contentdb.loading = true
core.handle_async(fetch_pkgs, nil, function(result)
if result then
contentdb.load_ok = true
contentdb.load_error = false
contentdb.packages = result.packages
contentdb.packages_full = result.packages
contentdb.packages_full_unordered = result.packages
contentdb.aliases = result.aliases
for _, package in ipairs(result.packages) do
contentdb.package_by_id[package.id] = package
end
else
contentdb.load_error = true
end
contentdb.loading = false
callback(result)
end)
end
function contentdb.update_paths()
local mod_hash = {}
pkgmgr.refresh_globals()
for _, mod in pairs(pkgmgr.global_mods:get_list()) do
local cdb_id = pkgmgr.get_contentdb_id(mod)
if cdb_id then
mod_hash[contentdb.aliases[cdb_id] or cdb_id] = mod
end
end
local game_hash = {}
pkgmgr.update_gamelist()
for _, game in pairs(pkgmgr.games) do
local cdb_id = pkgmgr.get_contentdb_id(game)
if cdb_id then
game_hash[contentdb.aliases[cdb_id] or cdb_id] = game
end
end
local txp_hash = {}
for _, txp in pairs(pkgmgr.get_texture_packs()) do
local cdb_id = pkgmgr.get_contentdb_id(txp)
if cdb_id then
txp_hash[contentdb.aliases[cdb_id] or cdb_id] = txp
end
end
for _, package in pairs(contentdb.packages_full) do
local content
if package.type == "mod" then
content = mod_hash[package.id]
elseif package.type == "game" then
content = game_hash[package.id]
elseif package.type == "txp" then
content = txp_hash[package.id]
end
if content then
package.path = content.path
package.installed_release = content.release or 0
else
package.path = nil
package.installed_release = nil
end
end
end
function contentdb.sort_packages()
local ret = {}
-- Add installed content
for _, pkg in ipairs(contentdb.packages_full_unordered) do
if pkg.path then
ret[#ret + 1] = pkg
end
end
-- Sort installed content first by "is there an update available?", then by title
table.sort(ret, function(a, b)
local a_updatable = a.installed_release < a.release
local b_updatable = b.installed_release < b.release
if a_updatable and not b_updatable then
return true
elseif b_updatable and not a_updatable then
return false
end
return a.title < b.title
end)
-- Add uninstalled content
for _, pkg in ipairs(contentdb.packages_full_unordered) do
if not pkg.path then
ret[#ret + 1] = pkg
end
end
contentdb.packages_full = ret
end
function contentdb.filter_packages(query, by_type)
if query == "" and by_type == nil then
contentdb.packages = contentdb.packages_full
return
end
local keywords = {}
for word in query:lower():gmatch("%S+") do
table.insert(keywords, word)
end
local function matches_keywords(package)
for k = 1, #keywords do
local keyword = keywords[k]
if string.find(package.name:lower(), keyword, 1, true) or
string.find(package.title:lower(), keyword, 1, true) or
string.find(package.author:lower(), keyword, 1, true) or
string.find(package.short_description:lower(), keyword, 1, true) then
return true
end
end
return false
end
contentdb.packages = {}
for _, package in pairs(contentdb.packages_full) do
if (query == "" or matches_keywords(package)) and
(by_type == nil or package.type == by_type) then
contentdb.packages[#contentdb.packages + 1] = package
end
end
end

View File

@ -0,0 +1,520 @@
--Minetest
--Copyright (C) 2018-20 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.
if not core.get_http_api then
function create_contentdb_dlg()
return messagebox("contentdb",
fgettext("ContentDB is not available when Minetest was compiled without cURL"))
end
return
end
-- Filter
local search_string = ""
local cur_page = 1
local num_per_page = 5
local filter_type = 1
local filter_types_titles = {
fgettext("All packages"),
fgettext("Games"),
fgettext("Mods"),
fgettext("Texture packs"),
}
-- Automatic package installation
local auto_install_spec = nil
local filter_types_type = {
nil,
"game",
"mod",
"txp",
}
local function install_or_update_package(this, package)
local install_parent
if package.type == "mod" then
install_parent = core.get_modpath()
elseif package.type == "game" then
install_parent = core.get_gamepath()
elseif package.type == "txp" then
install_parent = core.get_texturepath()
else
error("Unknown package type: " .. package.type)
end
if package.queued or package.downloading then
return
end
local function on_confirm()
local has_hard_deps = contentdb.has_hard_deps(package)
if has_hard_deps then
local dlg = create_install_dialog(package)
dlg:set_parent(this)
this:hide()
dlg:show()
elseif has_hard_deps == nil then
local dlg = messagebox("error_checking_deps",
fgettext("Error getting dependencies for package"))
dlg:set_parent(this)
this:hide()
dlg:show()
else
contentdb.queue_download(package, package.path and contentdb.REASON_UPDATE or contentdb.REASON_NEW)
end
end
if package.type == "mod" and #pkgmgr.games == 0 then
local dlg = messagebox("install_game",
fgettext("You need to install a game before you can install a mod"))
dlg:set_parent(this)
this:hide()
dlg:show()
elseif not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then
local dlg = create_confirm_overwrite(package, on_confirm)
dlg:set_parent(this)
this:hide()
dlg:show()
else
on_confirm()
end
end
-- Resolves the package specification stored in auto_install_spec into an actual package.
-- May only be called after the package list has been loaded successfully.
local function resolve_auto_install_spec()
assert(contentdb.load_ok)
if not auto_install_spec then
return nil
end
local spec = contentdb.aliases[auto_install_spec] or auto_install_spec
local resolved = nil
for _, pkg in ipairs(contentdb.packages_full_unordered) do
if pkg.id == spec then
resolved = pkg
break
end
end
if not resolved then
gamedata.errormessage = fgettext("The package $1 was not found.", auto_install_spec)
ui.update()
auto_install_spec = nil
end
return resolved
end
-- Installs the package specified by auto_install_spec.
-- Only does something if:
-- a. The package list has been loaded successfully.
-- b. The ContentDB dialog is currently visible.
local function do_auto_install()
if not contentdb.load_ok then
return
end
local pkg = resolve_auto_install_spec()
if not pkg then
return
end
local contentdb_dlg = ui.find_by_name("contentdb")
if not contentdb_dlg or contentdb_dlg.hidden then
return
end
install_or_update_package(contentdb_dlg, pkg)
auto_install_spec = nil
end
local function sort_and_filter_pkgs()
contentdb.update_paths()
contentdb.sort_packages()
contentdb.filter_packages(search_string, filter_types_type[filter_type])
local auto_install_pkg = resolve_auto_install_spec()
if auto_install_pkg then
local idx = table.indexof(contentdb.packages, auto_install_pkg)
if idx ~= -1 then
table.remove(contentdb.packages, idx)
table.insert(contentdb.packages, 1, auto_install_pkg)
end
end
end
local function load()
if contentdb.load_ok then
sort_and_filter_pkgs()
return
end
if contentdb.loading then
return
end
contentdb.fetch_pkgs(function(result)
if result then
sort_and_filter_pkgs()
do_auto_install()
end
ui.update()
end)
end
local function get_info_formspec(text)
local H = 9.5
return table.concat({
"formspec_version[6]",
"size[15.75,9.5]",
core.settings:get_bool("enable_touch") and "padding[0.01,0.01]" or "position[0.5,0.55]",
"label[4,4.35;", text, "]",
"container[0,", H - 0.8 - 0.375, "]",
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
"container_end[]",
})
end
local function get_formspec(dlgdata)
if contentdb.loading then
return get_info_formspec(fgettext("Loading..."))
end
if contentdb.load_error then
return get_info_formspec(fgettext("No packages could be retrieved"))
end
assert(contentdb.load_ok)
contentdb.update_paths()
dlgdata.pagemax = math.max(math.ceil(#contentdb.packages / num_per_page), 1)
if cur_page > dlgdata.pagemax then
cur_page = 1
end
local W = 15.75
local H = 9.5
local formspec = {
"formspec_version[6]",
"size[15.75,9.5]",
core.settings:get_bool("enable_touch") and "padding[0.01,0.01]" or "position[0.5,0.55]",
"style[status,downloading,queued;border=false]",
"container[0.375,0.375]",
"field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]",
"field_enter_after_edit[search_string;true]",
"image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]",
"image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]",
"dropdown[9.175,0;2.7875,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]",
"container_end[]",
-- Page nav buttons
"container[0,", H - 0.8 - 0.375, "]",
"button[0.375,0;5,0.8;back;", fgettext("Back to Main Menu"), "]",
"container[", W - 0.375 - 0.8*4 - 2, ",0]",
"image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]",
"image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]",
"style[pagenum;border=false]",
"button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]",
"image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]",
"image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]",
"container_end[]",
"container_end[]",
}
if contentdb.number_downloading > 0 then
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;downloading;"
if #contentdb.download_queue > 0 then
formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued",
contentdb.number_downloading, #contentdb.download_queue)
else
formspec[#formspec + 1] = fgettext("$1 downloading...", contentdb.number_downloading)
end
formspec[#formspec + 1] = "]"
else
local num_avail_updates = 0
for i=1, #contentdb.packages_full do
local package = contentdb.packages_full[i]
if package.path and package.installed_release < package.release and
not (package.downloading or package.queued) then
num_avail_updates = num_avail_updates + 1
end
end
if num_avail_updates == 0 then
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;status;"
formspec[#formspec + 1] = fgettext("No updates")
formspec[#formspec + 1] = "]"
else
formspec[#formspec + 1] = "button[12.5875,0.375;2.7875,0.8;update_all;"
formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates)
formspec[#formspec + 1] = "]"
end
end
if #contentdb.packages == 0 then
formspec[#formspec + 1] = "label[4,4.75;"
formspec[#formspec + 1] = fgettext("No results")
formspec[#formspec + 1] = "]"
end
-- download/queued tooltips always have the same message
local tooltip_colors = ";#dff6f5;#302c2e]"
formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors
formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors
local start_idx = (cur_page - 1) * num_per_page + 1
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
local package = contentdb.packages[i]
local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8)
formspec[#formspec + 1] = "container[0.375,"
formspec[#formspec + 1] = container_y
formspec[#formspec + 1] = "]"
-- image
formspec[#formspec + 1] = "image[0,0;1.5,1;"
formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package))
formspec[#formspec + 1] = "]"
-- title
formspec[#formspec + 1] = "label[1.875,0.1;"
formspec[#formspec + 1] = core.formspec_escape(
core.colorize(mt_color_green, package.title) ..
core.colorize("#BFBFBF", " by " .. package.author))
formspec[#formspec + 1] = "]"
-- buttons
local description_width = W - 2.625 - 2 * 0.7 - 2 * 0.15
local second_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir)
local third_base = "image_button[-2.4,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "container["
formspec[#formspec + 1] = W - 0.375*2
formspec[#formspec + 1] = ",0.1]"
if package.downloading then
formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;"
formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir)
formspec[#formspec + 1] = "cdb_downloading.png;3;400;]"
elseif package.queued then
formspec[#formspec + 1] = second_base
formspec[#formspec + 1] = "cdb_queued.png;queued;]"
elseif not package.path then
local elem_name = "install_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]"
formspec[#formspec + 1] = second_base .. "cdb_add.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors
else
if package.installed_release < package.release then
-- The install_ action also handles updating
local elem_name = "install_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]"
formspec[#formspec + 1] = third_base .. "cdb_update.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors
description_width = description_width - 0.7 - 0.15
end
local elem_name = "uninstall_" .. i .. ";"
formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]"
formspec[#formspec + 1] = second_base .. "cdb_clear.png;" .. elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors
end
local web_elem_name = "view_" .. i .. ";"
formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" ..
core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]"
formspec[#formspec + 1] = "tooltip[" .. web_elem_name ..
fgettext("View more information in a web browser") .. tooltip_colors
formspec[#formspec + 1] = "container_end[]"
-- description
formspec[#formspec + 1] = "textarea[1.855,0.3;"
formspec[#formspec + 1] = tostring(description_width)
formspec[#formspec + 1] = ",0.8;;;"
formspec[#formspec + 1] = core.formspec_escape(package.short_description)
formspec[#formspec + 1] = "]"
formspec[#formspec + 1] = "container_end[]"
end
return table.concat(formspec)
end
local function handle_submit(this, fields)
if fields.search or fields.key_enter_field == "search_string" then
search_string = fields.search_string:trim()
cur_page = 1
contentdb.filter_packages(search_string, filter_types_type[filter_type])
return true
end
if fields.clear then
search_string = ""
cur_page = 1
contentdb.filter_packages("", filter_types_type[filter_type])
return true
end
if fields.back then
this:delete()
return true
end
if fields.pstart then
cur_page = 1
return true
end
if fields.pend then
cur_page = this.data.pagemax
return true
end
if fields.pnext then
cur_page = cur_page + 1
if cur_page > this.data.pagemax then
cur_page = 1
end
return true
end
if fields.pback then
if cur_page == 1 then
cur_page = this.data.pagemax
else
cur_page = cur_page - 1
end
return true
end
if fields.type then
local new_type = table.indexof(filter_types_titles, fields.type)
if new_type ~= filter_type then
filter_type = new_type
cur_page = 1
contentdb.filter_packages(search_string, filter_types_type[filter_type])
return true
end
end
if fields.update_all then
for i=1, #contentdb.packages_full do
local package = contentdb.packages_full[i]
if package.path and package.installed_release < package.release and
not (package.downloading or package.queued) then
contentdb.queue_download(package, contentdb.REASON_UPDATE)
end
end
return true
end
local start_idx = (cur_page - 1) * num_per_page + 1
assert(start_idx ~= nil)
for i=start_idx, math.min(#contentdb.packages, start_idx+num_per_page-1) do
local package = contentdb.packages[i]
assert(package)
if fields["install_" .. i] then
install_or_update_package(this, package)
return true
end
if fields["uninstall_" .. i] then
local dlg = create_delete_content_dlg(package)
dlg:set_parent(this)
this:hide()
dlg:show()
return true
end
if fields["view_" .. i] then
local url = ("%s/packages/%s?protocol_version=%d"):format(
core.settings:get("contentdb_url"), package.url_part,
core.get_max_supp_proto())
core.open_url(url)
return true
end
end
return false
end
local function handle_events(event)
if event == "DialogShow" then
-- On touchscreen, don't show the "MINETEST" header behind the dialog.
mm_game_theme.set_engine(core.settings:get_bool("enable_touch"))
-- If ContentDB is already loaded, auto-install packages here.
do_auto_install()
return true
end
return false
end
--- Creates a ContentDB dialog.
---
--- @param type string | nil
--- Sets initial package filter. "game", "mod", "txp" or nil (no filter).
--- @param install_spec table | nil
--- ContentDB ID of package as returned by pkgmgr.get_contentdb_id().
--- Sets package to install or update automatically.
function create_contentdb_dlg(type, install_spec)
search_string = ""
cur_page = 1
if type then
-- table.indexof does not work on tables that contain `nil`
for i, v in pairs(filter_types_type) do
if v == type then
filter_type = i
break
end
end
else
filter_type = 1
end
-- Keep the old auto_install_spec if the caller doesn't specify one.
if install_spec then
auto_install_spec = install_spec
end
load()
return dialog_create("contentdb",
get_formspec,
handle_submit,
handle_events)
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
--Minetest
--Copyright (C) 2018-24 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 function get_formspec(data)
local selected_game, selected_game_idx = pkgmgr.find_by_gameid(core.settings:get("menu_last_game"))
if not selected_game_idx then
selected_game_idx = 1
selected_game = pkgmgr.games[1]
end
local game_list = {}
for i, game in ipairs(pkgmgr.games) do
game_list[i] = core.formspec_escape(game.title)
end
local package = data.package
local will_install_deps = data.will_install_deps
local deps_to_install = 0
local deps_not_found = 0
data.dependencies = contentdb.resolve_dependencies(package, selected_game)
local formatted_deps = {}
for _, dep in pairs(data.dependencies) do
formatted_deps[#formatted_deps + 1] = "#fff"
formatted_deps[#formatted_deps + 1] = core.formspec_escape(dep.name)
if dep.installed then
formatted_deps[#formatted_deps + 1] = "#ccf"
formatted_deps[#formatted_deps + 1] = fgettext("Already installed")
elseif dep.package then
formatted_deps[#formatted_deps + 1] = "#cfc"
formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author)
deps_to_install = deps_to_install + 1
else
formatted_deps[#formatted_deps + 1] = "#f00"
formatted_deps[#formatted_deps + 1] = fgettext("Not found")
deps_not_found = deps_not_found + 1
end
end
local message_bg = "#3333"
local message
if will_install_deps then
message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install)
else
message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install)
end
if deps_not_found > 0 then
message = fgettext("$1 required dependencies could not be found.", deps_not_found) ..
" " .. fgettext("Please check that the base game is correct.", deps_not_found) ..
"\n" .. message
message_bg = mt_color_orange
end
local formspec = {
"formspec_version[3]",
"size[7,7.85]",
"style[title;border=false]",
"box[0,0;7,0.5;#3333]",
"button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]",
"container[0.375,0.70]",
"label[0,0.25;", fgettext("Base Game:"), "]",
"dropdown[2,0;4.25,0.5;selected_game;", table.concat(game_list, ","), ";", selected_game_idx, "]",
"label[0,0.8;", fgettext("Dependencies:"), "]",
"tablecolumns[color;text;color;text]",
"table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]",
"container_end[]",
"checkbox[0.375,5.1;will_install_deps;",
fgettext("Install missing dependencies"), ";",
will_install_deps and "true" or "false", "]",
"box[0,5.4;7,1.2;", message_bg, "]",
"textarea[0.375,5.5;6.25,1;;;", message, "]",
"container[1.375,6.85]",
"button[0,0;2,0.8;install_all;", fgettext("Install"), "]",
"button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]",
"container_end[]",
}
return table.concat(formspec)
end
local function handle_submit(this, fields)
local data = this.data
if fields.cancel then
this:delete()
return true
end
if fields.will_install_deps ~= nil then
data.will_install_deps = core.is_yes(fields.will_install_deps)
return true
end
if fields.install_all then
contentdb.queue_download(data.package, contentdb.REASON_NEW)
if data.will_install_deps then
for _, dep in pairs(data.dependencies) do
if not dep.is_optional and not dep.installed and dep.package then
contentdb.queue_download(dep.package, contentdb.REASON_DEPENDENCY)
end
end
end
this:delete()
return true
end
if fields.selected_game then
for _, game in pairs(pkgmgr.games) do
if game.title == fields.selected_game then
core.settings:set("menu_last_game", game.id)
break
end
end
return true
end
return false
end
function create_install_dialog(package)
local dlg = dialog_create("install_dialog", get_formspec, handle_submit, nil)
dlg.data.dependencies = nil
dlg.data.package = package
dlg.data.will_install_deps = true
return dlg
end

View File

@ -0,0 +1,53 @@
--Minetest
--Copyright (C) 2018-24 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.
function get_formspec(data)
local package = data.package
return confirmation_formspec(
fgettext("\"$1\" already exists. Would you like to overwrite it?", package.name),
'install', fgettext("Overwrite"),
'cancel', fgettext("Cancel"))
end
local function handle_submit(this, fields)
local data = this.data
if fields.cancel then
this:delete()
return true
end
if fields.install then
this:delete()
data.callback()
return true
end
return false
end
function create_confirm_overwrite(package, callback)
assert(type(package) == "table")
assert(type(callback) == "function")
local dlg = dialog_create("data", get_formspec, handle_submit, nil)
dlg.data.package = package
dlg.data.callback = callback
return dlg
end

View File

@ -18,5 +18,9 @@
local path = core.get_mainmenu_path() .. DIR_DELIM .. "content"
dofile(path .. DIR_DELIM .. "pkgmgr.lua")
dofile(path .. DIR_DELIM .. "contentdb.lua")
dofile(path .. DIR_DELIM .. "update_detector.lua")
dofile(path .. DIR_DELIM .. "dlg_contentstore.lua")
dofile(path .. DIR_DELIM .. "screenshots.lua")
dofile(path .. DIR_DELIM .. "dlg_install.lua")
dofile(path .. DIR_DELIM .. "dlg_overwrite.lua")
dofile(path .. DIR_DELIM .. "dlg_contentdb.lua")

View File

@ -0,0 +1,78 @@
--Minetest
--Copyright (C) 2023-24 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.
-- Screenshot
local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb"
assert(core.create_dir(screenshot_dir))
local screenshot_downloading = {}
local screenshot_downloaded = {}
local function get_file_extension(path)
local parts = path:split(".")
return parts[#parts]
end
function get_screenshot(package)
if not package.thumbnail then
return defaulttexturedir .. "no_screenshot.png"
elseif screenshot_downloading[package.thumbnail] then
return defaulttexturedir .. "loading_screenshot.png"
end
-- Get tmp screenshot path
local ext = get_file_extension(package.thumbnail)
local filepath = screenshot_dir .. DIR_DELIM ..
("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext)
-- Return if already downloaded
local file = io.open(filepath, "r")
if file then
file:close()
return filepath
end
-- Show error if we've failed to download before
if screenshot_downloaded[package.thumbnail] then
return defaulttexturedir .. "error_screenshot.png"
end
-- Download
local function download_screenshot(params)
return core.download_file(params.url, params.dest)
end
local function callback(success)
screenshot_downloading[package.thumbnail] = nil
screenshot_downloaded[package.thumbnail] = true
if not success then
core.log("warning", "Screenshot download failed for some reason")
end
ui.update()
end
if core.handle_async(download_screenshot,
{ dest = filepath, url = package.thumbnail }, callback) then
screenshot_downloading[package.thumbnail] = true
else
core.log("error", "ERROR: async event failed")
return defaulttexturedir .. "error_screenshot.png"
end
return defaulttexturedir .. "loading_screenshot.png"
end

View File

@ -26,13 +26,23 @@ if not core.get_http_api then
end
assert(core.create_dir(core.get_cache_path() .. DIR_DELIM .. "cdb"))
local cache_file_path = core.get_cache_path() .. DIR_DELIM .. "cdb" .. DIR_DELIM .. "updates.json"
local has_fetched = false
local latest_releases
do
local tmp = core.get_once("cdb_latest_releases")
if tmp then
latest_releases = core.deserialize(tmp, true)
has_fetched = latest_releases ~= nil
if check_cache_age("cdb_updates_last_checked", 3 * 3600) then
local f = io.open(cache_file_path, "r")
local data = ""
if f then
data = f:read("*a")
f:close()
end
data = data ~= "" and core.parse_json(data) or nil
if type(data) == "table" then
latest_releases = data
has_fetched = true
end
end
end
@ -97,7 +107,8 @@ local function fetch()
return
end
latest_releases = lowercase_keys(releases)
core.set_once("cdb_latest_releases", core.serialize(latest_releases))
core.safe_file_write(cache_file_path, core.write_json(latest_releases))
cache_settings:set("cdb_updates_last_checked", tostring(os.time()))
if update_detector.get_count() > 0 then
local maintab = ui.find_by_name("maintab")

View File

@ -334,7 +334,7 @@ local function handle_buttons(this, fields)
if fields.btn_config_world_cdb then
this.data.list = nil
local dlg = create_store_dlg("mod")
local dlg = create_contentdb_dlg("mod")
dlg:set_parent(this)
this:hide()
dlg:show()

View File

@ -335,7 +335,7 @@ end
local function create_world_buttonhandler(this, fields)
if fields["world_create_open_cdb"] then
local dlg = create_store_dlg("game")
local dlg = create_contentdb_dlg("game")
dlg:set_parent(this.parent)
this:delete()
this.parent:hide()

View File

@ -15,15 +15,30 @@
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
---- IMPORTANT ----
-- This whole file can be removed after a while.
-- It was only directly useful for upgrades from 5.7.0 to 5.8.0, but
-- maybe some odd fellow directly upgrades from 5.6.1 to 5.9.0 in the future...
-- see <https://github.com/minetest/minetest/pull/13850> in case it's not obvious
---- ----
local SETTING_NAME = "no_mtg_notification"
function check_reinstall_mtg()
if core.settings:get_bool("no_mtg_notification") then
-- used to be in minetest.conf
if core.settings:get_bool(SETTING_NAME) then
cache_settings:set_bool(SETTING_NAME, true)
core.settings:remove(SETTING_NAME)
end
if cache_settings:get_bool(SETTING_NAME) then
return
end
local games = core.get_games()
for _, game in ipairs(games) do
if game.id == "minetest" then
core.settings:set_bool("no_mtg_notification", true)
cache_settings:set_bool(SETTING_NAME, true)
return
end
end
@ -37,7 +52,7 @@ function check_reinstall_mtg()
end
end
if not mtg_world_found then
core.settings:set_bool("no_mtg_notification", true)
cache_settings:set_bool(SETTING_NAME, true)
return
end
@ -78,7 +93,7 @@ local function buttonhandler(this, fields)
local maintab = ui.find_by_name("maintab")
local dlg = create_store_dlg(nil, "minetest/minetest")
local dlg = create_contentdb_dlg(nil, "minetest/minetest")
dlg:set_parent(maintab)
maintab:hide()
dlg:show()
@ -87,7 +102,7 @@ local function buttonhandler(this, fields)
end
if fields.dismiss then
core.settings:set_bool("no_mtg_notification", true)
cache_settings:set_bool("no_mtg_notification", true)
this:delete()
return true
end
@ -111,5 +126,3 @@ function create_reinstall_mtg_dlg()
buttonhandler, eventhandler)
return dlg
end

View File

@ -51,12 +51,13 @@ end
local function version_info_buttonhandler(this, fields)
if fields.version_check_remind then
-- Erase last known, user will be reminded again at next check
core.settings:set("update_last_known", "")
cache_settings:set("update_last_known", "")
this:delete()
return true
end
if fields.version_check_never then
core.settings:set("update_last_checked", "disabled")
-- clear checked URL
core.settings:set("update_information_url", "")
this:delete()
return true
end
@ -116,7 +117,7 @@ local function on_version_info_received(json)
return
end
local known_update = tonumber(core.settings:get("update_last_known")) or 0
local known_update = tonumber(cache_settings:get("update_last_known")) or 0
-- Format: MMNNPPP (Major, Minor, Patch)
local new_number = type(json.latest) == "table" and json.latest.version_code
@ -135,7 +136,7 @@ local function on_version_info_received(json)
return
end
core.settings:set("update_last_known", tostring(new_number))
cache_settings:set("update_last_known", tostring(new_number))
-- Show version info dialog (once)
maintab:hide()
@ -149,20 +150,20 @@ end
function check_new_version()
local url = core.settings:get("update_information_url")
if core.settings:get("update_last_checked") == "disabled" or
url == "" then
if url == "" then
-- Never show any updates
return
end
local time_now = os.time()
local time_checked = tonumber(core.settings:get("update_last_checked")) or 0
if time_now - time_checked < 2 * 24 * 3600 then
-- Check interval of 2 entire days
-- every 2 days
if check_cache_age("update_last_checked", 2 * 24 * 3600) then
return
end
cache_settings:set("update_last_checked", tostring(os.time()))
core.settings:set("update_last_checked", tostring(time_now))
-- Clean old leftovers (this can be removed after 5.9.0 or so)
core.settings:remove("update_last_checked")
core.settings:remove("update_last_known")
core.handle_async(function(params)
local http = core.get_http_api()

View File

@ -28,8 +28,6 @@ local basepath = core.get_builtin_path()
defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" ..
DIR_DELIM .. "pack" .. DIR_DELIM
dofile(menupath .. DIR_DELIM .. "misc.lua")
dofile(basepath .. "common" .. DIR_DELIM .. "filterlist.lua")
dofile(basepath .. "fstk" .. DIR_DELIM .. "buttonbar.lua")
dofile(basepath .. "fstk" .. DIR_DELIM .. "dialog.lua")

View File

@ -1,6 +0,0 @@
-- old non-method sound function
function core.sound_stop(handle, ...)
return handle:stop(...)
end

View File

@ -17,7 +17,7 @@
serverlistmgr = {
-- continent code we detected for ourselves
my_continent = core.get_once("continent"),
my_continent = nil,
-- list of locally favorites servers
favorites = nil,
@ -26,6 +26,15 @@ serverlistmgr = {
servers = nil,
}
do
if check_cache_age("geoip_last_checked", 3600) then
local tmp = cache_settings:get("geoip") or ""
if tmp:match("^[A-Z][A-Z]$") then
serverlistmgr.my_continent = tmp
end
end
end
--------------------------------------------------------------------------------
-- Efficient data structure for normalizing arbitrary scores attached to objects
-- e.g. {{"a", 3.14}, {"b", 3.14}, {"c", 20}, {"d", 0}}
@ -71,7 +80,8 @@ local WEIGHT_SORT = 2
-- how much the estimated latency contributes to the final ranking
local WEIGHT_LATENCY = 1
local function order_server_list(list)
--- @param list of servers, will be modified.
local function order_server_list_internal(list)
-- calculate the scores
local s1 = Normalizer:new()
local s2 = Normalizer:new()
@ -90,28 +100,58 @@ local function order_server_list(list)
s1 = s1:calc()
s2 = s2:calc()
-- make a shallow copy and pre-calculate ordering
local res, order = {}, {}
for i = 1, #list do
local fav = list[i]
res[i] = fav
local n = s1[fav] * WEIGHT_SORT + s2[fav] * WEIGHT_LATENCY
order[fav] = n
-- pre-calculate ordering
local order = {}
for _, fav in ipairs(list) do
order[fav] = s1[fav] * WEIGHT_SORT + s2[fav] * WEIGHT_LATENCY
end
-- now sort the list
table.sort(res, function(fav1, fav2)
table.sort(list, function(fav1, fav2)
return order[fav1] > order[fav2]
end)
end
return res
local function order_server_list(list)
-- split the list into two parts and sort them separately, to keep empty
-- servers at the bottom.
local nonempty, empty = {}, {}
for _, fav in ipairs(list) do
if (fav.clients or 0) > 0 then
table.insert(nonempty, fav)
else
table.insert(empty, fav)
end
end
order_server_list_internal(nonempty)
order_server_list_internal(empty)
table.insert_all(nonempty, empty)
return nonempty
end
local public_downloading = false
local geoip_downloading = false
--------------------------------------------------------------------------------
local function fetch_geoip()
local http = core.get_http_api()
local url = core.settings:get("serverlist_url") .. "/geoip"
local response = http.fetch_sync({ url = url })
if not response.succeeded then
return
end
local retval = core.parse_json(response.data)
if type(retval) ~= "table" then
return
end
return type(retval.continent) == "string" and retval.continent
end
function serverlistmgr.sync()
if not serverlistmgr.servers then
serverlistmgr.servers = {{
@ -129,37 +169,23 @@ function serverlistmgr.sync()
return
end
-- only fetched once per MT instance
if not serverlistmgr.my_continent and not geoip_downloading then
geoip_downloading = true
core.handle_async(
function(param)
local http = core.get_http_api()
local url = core.settings:get("serverlist_url") .. "/geoip"
local response = http.fetch_sync({ url = url })
if not response.succeeded then
return
end
local retval = core.parse_json(response.data)
return retval and type(retval.continent) == "string" and retval.continent
end,
nil,
function(result)
geoip_downloading = false
if not result then
return
end
serverlistmgr.my_continent = result
core.set_once("continent", result)
-- reorder list if we already have it
if serverlistmgr.servers then
serverlistmgr.servers = order_server_list(serverlistmgr.servers)
core.event_handler("Refresh")
end
core.handle_async(fetch_geoip, nil, function(result)
geoip_downloading = false
if not result then
return
end
)
serverlistmgr.my_continent = result
cache_settings:set("geoip", result)
cache_settings:set("geoip_last_checked", tostring(os.time()))
-- re-sort list if applicable
if serverlistmgr.servers then
serverlistmgr.servers = order_server_list(serverlistmgr.servers)
core.event_handler("Refresh")
end
end)
end
if public_downloading then
@ -167,6 +193,7 @@ function serverlistmgr.sync()
end
public_downloading = true
-- note: this isn't cached because it's way too dynamic
core.handle_async(
function(param)
local http = core.get_http_api()

View File

@ -158,7 +158,7 @@ return {
"style[label_button;border=false]" ..
"button[0.1,3.4;5.3,0.5;label_button;" ..
core.formspec_escape(version.project .. " " .. version.string) .. "]" ..
"button[1.5,4.1;2.5,0.8;homepage;minetest.net]" ..
"button_url[1.5,4.1;2.5,0.8;homepage;minetest.net;https://www.minetest.net/]" ..
"hypertext[5.5,0.25;9.75,6.6;credits;" .. minetest.formspec_escape(hypertext) .. "]"
-- Render information
@ -188,10 +188,6 @@ return {
end,
cbf_button_handler = function(this, fields, name, tabdata)
if fields.homepage then
core.open_url("https://www.minetest.net")
end
if fields.share_debug then
local path = core.get_user_path() .. DIR_DELIM .. "debug.txt"
core.share_file(path)

View File

@ -225,7 +225,7 @@ local function handle_buttons(tabview, fields, tabname, tabdata)
end
if fields.btn_contentdb then
local dlg = create_store_dlg()
local dlg = create_contentdb_dlg()
dlg:set_parent(tabview)
tabview:hide()
dlg:show()
@ -255,7 +255,7 @@ local function handle_buttons(tabview, fields, tabname, tabdata)
if fields.btn_mod_mgr_update then
local pkg = packages:get_list()[tabdata.selected_pkg]
local dlg = create_store_dlg(nil, pkgmgr.get_contentdb_id(pkg))
local dlg = create_contentdb_dlg(nil, pkgmgr.get_contentdb_id(pkg))
dlg:set_parent(tabview)
tabview:hide()
dlg:show()

View File

@ -264,7 +264,7 @@ local function main_button_handler(this, fields, name, tabdata)
if fields.game_open_cdb then
local maintab = ui.find_by_name("maintab")
local dlg = create_store_dlg("game")
local dlg = create_contentdb_dlg("game")
dlg:set_parent(maintab)
maintab:hide()
dlg:show()

View File

@ -1,5 +1,6 @@
_G.core = {get_once = function(_) end}
_G.core = {}
_G.unpack = table.unpack
_G.check_cache_age = function() return false end
_G.serverlistmgr = {}
dofile("builtin/common/vector.lua")

View File

@ -114,7 +114,11 @@ always_fly_fast (Always fly fast) bool true
# the place button.
#
# Requires: keyboard_mouse
repeat_place_time (Place repetition interval) float 0.25 0.16 2
repeat_place_time (Place repetition interval) float 0.25 0.15 2.0
# The minimum time in seconds it takes between digging nodes when holding
# the dig button.
repeat_dig_time (Dig repetition interval) float 0.15 0.15 2.0
# Automatically jump up single-node obstacles.
autojump (Automatic jumping) bool false
@ -149,20 +153,23 @@ invert_hotbar_mouse_wheel (Hotbar: Invert mouse wheel direction) bool false
[*Touchscreen]
# Enables touchscreen mode, allowing you to play the game with a touchscreen.
#
# Requires: !android
enable_touch (Enable touchscreen) bool true
# The length in pixels it takes for touchscreen interaction to start.
#
# Requires: touchscreen_gui
touchscreen_threshold (Touchscreen threshold) int 20 0 100
# Touchscreen sensitivity multiplier.
#
# Requires: touchscreen_gui
touchscreen_sensitivity (Touchscreen sensitivity) float 0.2 0.001 10.0
# The length in pixels after which a touch interaction is considered movement.
#
# Requires: touchscreen_gui
touchscreen_threshold (Movement threshold) int 20 0 100
# The delay in milliseconds after which a touch interaction is considered a long tap.
#
# Requires: touchscreen_gui
touch_long_tap_delay (Threshold for long taps) int 400 100 1000
# Use crosshair to select object instead of whole screen.
# If enabled, a crosshair will be shown and will be used for selecting object.
#
@ -181,6 +188,19 @@ fixed_virtual_joystick (Fixed virtual joystick) bool false
# Requires: touchscreen_gui
virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false
# The gesture for for punching players/entities.
# This can be overridden by games and mods.
#
# * short_tap
# Easy to use and well-known from other games that shall not be named.
#
# * long_tap
# Known from the classic Minetest mobile controls.
# Combat is more or less impossible.
#
# Requires: touchscreen_gui
touch_punch_gesture (Punch gesture) enum short_tap short_tap,long_tap
[Graphics and Audio]
@ -772,6 +792,7 @@ serverlist_url (Serverlist URL) string servers.minetest.net
enable_split_login_register (Enable split login/register) bool true
# URL to JSON file which provides information about the newest Minetest release
# If this is empty the engine will never check for updates.
update_information_url (Update information URL) string https://www.minetest.net/release_info.json
[*Server]
@ -1739,6 +1760,9 @@ deprecated_lua_api_handling (Deprecated Lua API handling) enum log none,log,erro
# Enable random user input (only used for testing).
random_input (Random input) bool false
# Enable random mod loading (mainly used for testing).
random_mod_load_order (Random mod load order) bool false
# Enable mod channels support.
enable_mod_channels (Mod channels) bool false
@ -1872,6 +1896,9 @@ texture_min_size (Base texture size) int 64 1 32768
# Systems with a low-end GPU (or no GPU) would benefit from smaller values.
client_mesh_chunk (Client Mesh Chunksize) int 1 1 16
# Enables debug and error-checking in the OpenGL driver.
opengl_debug (OpenGL debug) bool false
[**Sound]
# Comma-separated list of AL and ALC extensions that should not be used.
# Useful for testing. See al_extensions.[h,cpp] for details.
@ -2287,20 +2314,6 @@ show_advanced (Show advanced settings) bool false
# Changing this setting requires a restart.
enable_sound (Sound) bool true
# Unix timestamp (integer) of when the client last checked for an update
# Set this value to "disabled" to never check for updates.
update_last_checked (Last update check) string
# Version number which was last seen during an update check.
#
# Representation: MMMIIIPPP, where M=Major, I=Minor, P=Patch
# Ex: 5.5.0 is 005005000
update_last_known (Last known version update) int 0
# If this is set to true, the user will never (again) be shown the
# "reinstall Minetest Game" notification.
no_mtg_notification (Don't show "reinstall Minetest Game" notification) bool false
# Key for moving the player forward.
keymap_forward (Forward key) key KEY_KEY_W
@ -2498,9 +2511,9 @@ keymap_toggle_chat (Chat toggle key) key KEY_F2
keymap_console (Large chat console key) key KEY_F10
# Key for toggling the display of fog.
keymap_toggle_force_fog_off (Fog toggle key) key KEY_F3
keymap_toggle_fog (Fog toggle key) key KEY_F3
# Key for toggling the camera update. Only used for development
# Key for toggling the camera update. Only usable with 'debug' privilege.
keymap_toggle_update_camera (Camera update toggle key) key
# Key for toggling the display of debug info.

1
client/shaders/Irrlicht Symbolic link
View File

@ -0,0 +1 @@
../../irr/media/Shaders

View File

@ -1,7 +1,7 @@
uniform vec4 fogColor;
uniform lowp vec4 fogColor;
uniform float fogDistance;
uniform float fogShadingParameter;
varying vec3 eyeVec;
varying highp vec3 eyeVec;
varying lowp vec4 varColor;

View File

@ -1,8 +1,8 @@
uniform vec4 emissiveColor;
uniform lowp vec4 emissiveColor;
varying lowp vec4 varColor;
varying vec3 eyeVec;
varying highp vec3 eyeVec;
void main(void)
{

View File

@ -1,13 +1,12 @@
uniform sampler2D baseTexture;
uniform vec3 dayLight;
uniform vec4 fogColor;
uniform lowp vec4 fogColor;
uniform float fogDistance;
uniform float fogShadingParameter;
uniform vec3 eyePosition;
// The cameraOffset is the current center of the visible world.
uniform vec3 cameraOffset;
uniform highp vec3 cameraOffset;
uniform float animationTimer;
#ifdef ENABLE_DYNAMIC_SHADOWS
// shadow texture
@ -44,11 +43,8 @@ varying mediump vec2 varTexCoord;
#else
centroid varying vec2 varTexCoord;
#endif
varying vec3 eyeVec;
varying highp vec3 eyeVec;
varying float nightRatio;
varying vec3 tsEyeVec;
varying vec3 lightVec;
varying vec3 tsLightVec;
#ifdef ENABLE_DYNAMIC_SHADOWS

View File

@ -1,10 +1,9 @@
uniform mat4 mWorld;
// Color of the light emitted by the sun.
uniform vec3 dayLight;
uniform vec3 eyePosition;
// The cameraOffset is the current center of the visible world.
uniform vec3 cameraOffset;
uniform highp vec3 cameraOffset;
uniform float animationTimer;
varying vec3 vNormal;
@ -44,7 +43,7 @@ centroid varying vec2 varTexCoord;
varying float area_enable_parallax;
varying vec3 eyeVec;
varying highp vec3 eyeVec;
varying float nightRatio;
// Color of the light emitted by the light sources.
const vec3 artificialLight = vec3(1.04, 1.04, 1.04);

View File

@ -1,13 +1,12 @@
uniform sampler2D baseTexture;
uniform vec3 dayLight;
uniform vec4 fogColor;
uniform lowp vec4 fogColor;
uniform float fogDistance;
uniform float fogShadingParameter;
uniform vec3 eyePosition;
// The cameraOffset is the current center of the visible world.
uniform vec3 cameraOffset;
uniform highp vec3 cameraOffset;
uniform float animationTimer;
#ifdef ENABLE_DYNAMIC_SHADOWS
// shadow texture
@ -44,7 +43,7 @@ varying mediump vec2 varTexCoord;
#else
centroid varying vec2 varTexCoord;
#endif
varying vec3 eyeVec;
varying highp vec3 eyeVec;
varying float nightRatio;
varying float vIDiff;

View File

@ -1,10 +1,7 @@
uniform mat4 mWorld;
uniform vec3 dayLight;
uniform vec3 eyePosition;
uniform float animationTimer;
uniform vec4 emissiveColor;
uniform vec3 cameraOffset;
uniform lowp vec4 emissiveColor;
varying vec3 vNormal;
varying vec3 vPosition;
@ -33,7 +30,7 @@ centroid varying vec2 varTexCoord;
varying float perspective_factor;
#endif
varying vec3 eyeVec;
varying highp vec3 eyeVec;
varying float nightRatio;
// Color of the light emitted by the light sources.
const vec3 artificialLight = vec3(1.04, 1.04, 1.04);

View File

@ -1,4 +1,4 @@
uniform vec4 emissiveColor;
uniform lowp vec4 emissiveColor;
void main(void)
{

View File

@ -1,31 +1,29 @@
set(DEPS "${CMAKE_SOURCE_DIR}/android/native/deps/${ANDROID_ABI}")
add_library(IrrlichtMt::IrrlichtMt STATIC IMPORTED)
set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${DEPS}/Irrlicht/include)
set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES
IMPORTED_LOCATION ${DEPS}/Irrlicht/libIrrlichtMt.a)
set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES
INTERFACE_LINK_LIBRARIES "EGL;GLESv1_CM;GLESv2;${DEPS}/Irrlicht/libpng.a;${DEPS}/Irrlicht/libjpeg.a")
set(CURL_INCLUDE_DIR ${DEPS}/Curl/include)
set(CURL_LIBRARY ${DEPS}/Curl/libcurl.a;${DEPS}/Curl/libmbedcrypto.a;${DEPS}/Curl/libmbedtls.a;${DEPS}/Curl/libmbedx509.a)
set(FREETYPE_INCLUDE_DIR_ft2build ${DEPS}/Freetype/include/freetype2)
set(FREETYPE_INCLUDE_DIR_freetype2 ${FREETYPE_INCLUDE_DIR_ft2build}/freetype)
set(FREETYPE_INCLUDE_DIR_ft2build ${DEPS}/Freetype/include/freetype2)
set(FREETYPE_LIBRARY ${DEPS}/Freetype/libfreetype.a)
set(GETTEXT_INCLUDE_DIR ${DEPS}/Gettext/include;${DEPS}/Iconv/include)
set(GETTEXT_LIBRARY ${DEPS}/Gettext/libintl.a)
set(ICONV_LIBRARY ${DEPS}/Iconv/libiconv.a;${DEPS}/Iconv/libcharset.a)
set(JPEG_INCLUDE_DIR ${DEPS}/JPEG/include)
set(JPEG_LIBRARY ${DEPS}/JPEG/libjpeg.a)
set(LUA_INCLUDE_DIR ${DEPS}/LuaJIT/include)
set(LUA_LIBRARY ${DEPS}/LuaJIT/libluajit.a)
set(OGG_INCLUDE_DIR ${DEPS}/Vorbis/include)
set(OGG_LIBRARY ${DEPS}/Vorbis/libogg.a)
set(OPENAL_INCLUDE_DIR ${DEPS}/OpenAL-Soft/include)
set(OPENAL_LIBRARY ${DEPS}/OpenAL-Soft/libopenal.a;OpenSLES)
set(PNG_LIBRARY ${DEPS}/PNG/libpng.a)
set(PNG_PNG_INCLUDE_DIR ${DEPS}/PNG/include)
set(SQLITE3_INCLUDE_DIR ${DEPS}/SQLite/include)
set(SQLITE3_LIBRARY ${DEPS}/SQLite/libsqlite3.a)
set(VORBIS_INCLUDE_DIR ${DEPS}/Vorbis/include)
set(VORBISFILE_LIBRARY ${DEPS}/Vorbis/libvorbisfile.a)
set(VORBIS_LIBRARY ${DEPS}/Vorbis/libvorbis.a)
set(VORBISFILE_LIBRARY ${DEPS}/Vorbis/libvorbisfile.a)
set(ZSTD_INCLUDE_DIR ${DEPS}/Zstd/include)
set(ZSTD_LIBRARY ${DEPS}/Zstd/libzstd.a)
set(SDL2_INCLUDE_DIRS ${DEPS}/SDL2/include/SDL2)
set(SDL2_LIBRARIES ${DEPS}/SDL2/libSDL2.a)

View File

@ -1,18 +0,0 @@
# Locate IrrlichtMt headers on system.
find_path(IRRLICHT_INCLUDE_DIR NAMES irrlicht.h
DOC "Path to the directory with IrrlichtMt includes"
PATHS
/usr/local/include/irrlichtmt
/usr/include/irrlichtmt
/system/develop/headers/irrlichtmt #Haiku
PATH_SUFFIXES "include/irrlichtmt"
)
# Handholding for users
if(IRRLICHT_INCLUDE_DIR AND (NOT IS_DIRECTORY "${IRRLICHT_INCLUDE_DIR}" OR
NOT EXISTS "${IRRLICHT_INCLUDE_DIR}/irrlicht.h"))
message(WARNING "IRRLICHT_INCLUDE_DIR was set to ${IRRLICHT_INCLUDE_DIR} "
"but irrlicht.h does not exist inside. The path will not be used.")
unset(IRRLICHT_INCLUDE_DIR CACHE)
endif()

View File

@ -702,8 +702,18 @@ Methods:
```lua
{
speed = float,
speed_climb = float,
speed_crouch = float,
speed_fast = float,
speed_walk = float,
acceleration_default = float,
acceleration_air = float,
acceleration_fast = float,
jump = float,
gravity = float,
liquid_fluidity = float,
liquid_fluidity_smooth = float,
liquid_sink = float,
sneak = boolean,
sneak_glitch = boolean,
new_move = boolean,
@ -719,7 +729,8 @@ Methods:
* `get_breath()`
* returns the player's breath
* `get_movement_acceleration()`
* returns acceleration of the player in different environments:
* returns acceleration of the player in different environments
(note: does not take physics overrides into account):
```lua
{
@ -730,7 +741,8 @@ Methods:
```
* `get_movement_speed()`
* returns player's speed in different environments:
* returns player's speed in different environments
(note: does not take physics overrides into account):
```lua
{
@ -743,7 +755,8 @@ Methods:
```
* `get_movement()`
* returns player's movement in different environments:
* returns player's movement in different environments
(note: does not take physics overrides into account):
```lua
{

View File

@ -38,7 +38,6 @@ General options and their default values:
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 by default (requires support by IrrlichtMt)
Library specific options:
@ -55,8 +54,6 @@ Library specific options:
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

View File

@ -6,10 +6,9 @@
| ---------- | ------- | ---------- |
| GCC | 7.5+ | or Clang 7.0.1+ |
| CMake | 3.5+ | |
| IrrlichtMt | - | Custom version of Irrlicht, see https://github.com/minetest/irrlicht |
| libjpeg | - | (via IrrlichtMt) |
| libpng | - | (via IrrlichtMt) |
| SDL | 2.x | (via IrrlichtMt) |
| libjpeg | - | |
| libpng | - | |
| SDL | 2.x | |
| Freetype | 2.0+ | |
| SQLite3 | 3+ | |
| Zlib | - | |
@ -22,27 +21,27 @@
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 gettext libsdl2-dev
sudo apt install g++ make libc6-dev cmake libpng-dev libjpeg-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 gettext libsdl2-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 gettext SDL2-devel
sudo dnf install make automake gcc gcc-c++ kernel-devel cmake libcurl-devel openal-soft-devel libpng-devel libjpeg-devel libvorbis-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 gettext SDL2-devel
For openSUSE users:
sudo zypper install gcc cmake libjpeg8-devel libpng16-devel openal-soft-devel libcurl-devel sqlite3-devel luajit-devel libzstd-devel Mesa-libGL-devel libXi-devel libvorbis-devel freetype2-devel SDL2-devel
sudo zypper install gcc gcc-c++ cmake libjpeg8-devel libpng16-devel openal-soft-devel libcurl-devel sqlite3-devel luajit-devel libzstd-devel Mesa-libGL-devel libvorbis-devel freetype2-devel SDL2-devel
For Arch users:
sudo pacman -S --needed base-devel libcurl-gnutls cmake libxi libpng sqlite libogg libvorbis openal freetype2 jsoncpp gmp luajit leveldb ncurses zstd gettext sdl2
sudo pacman -S --needed base-devel libcurl-gnutls cmake libpng sqlite libogg libvorbis openal freetype2 jsoncpp gmp luajit leveldb ncurses zstd gettext sdl2
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 gettext sdl2-dev
sudo apk add build-base cmake libpng-dev jpeg-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 gettext sdl2-dev
For Void users:
sudo xbps-install cmake libpng-devel jpeg-devel libXi-devel mesa sqlite-devel libogg-devel libvorbis-devel libopenal-devel libcurl-devel freetype-devel zlib-devel gmp-devel jsoncpp-devel LuaJIT-devel libzstd-devel gettext SDL2-devel
sudo xbps-install cmake libpng-devel jpeg-devel mesa sqlite-devel libogg-devel libvorbis-devel libopenal-devel libcurl-devel freetype-devel zlib-devel gmp-devel jsoncpp-devel LuaJIT-devel libzstd-devel gettext SDL2-devel
## Download
@ -73,24 +72,12 @@ Download source (this is the URL to the latest of source repository, which might
git clone --depth 1 https://github.com/minetest/minetest.git
cd minetest
Download IrrlichtMt to `lib/irrlichtmt`, it will be used to satisfy the IrrlichtMt dependency that way:
git clone --depth 1 --branch "$(cat misc/irrlichtmt_tag.txt)" 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 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:
@ -109,12 +96,3 @@ Run it:
- 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`)

View File

@ -20,12 +20,6 @@ git clone --depth 1 https://github.com/minetest/minetest.git
cd minetest
```
Download Minetest's fork of Irrlicht:
```bash
git clone --depth 1 --branch "$(cat misc/irrlichtmt_tag.txt)" https://github.com/minetest/irrlicht.git lib/irrlichtmt
```
## Build
```bash

View File

@ -17,12 +17,6 @@ After you successfully built vcpkg you can easily install the required libraries
vcpkg install zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp gettext sdl2 --triplet x64-windows
```
- **Don't forget about IrrlichtMt.** The easiest way is to clone it to `lib/irrlichtmt`:
```bat
for /F %i in (misc\irrlichtmt_tag.txt) do git clone --depth 1 --branch %i https://github.com/minetest/irrlicht.git lib\irrlichtmt
```
- `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.

View File

@ -16,6 +16,7 @@ Notable pages:
- [Developing minetestserver with Docker](docker.md)
- [Android tips & tricks](android.md)
- [OS/library compatibility policy](os-compatibility.md)
- [Miscellaneous](misc.md)
## IRC

View File

@ -4,7 +4,7 @@
We will be using a tool called "perf", which you can get by installing `perf` or `linux-perf` or `linux-tools-common`.
For best results build Minetest and Irrlicht with debug symbols
To get usable results you need to build Minetest with debug symbols
(`-DCMAKE_BUILD_TYPE=RelWithDebInfo` or `-DCMAKE_BUILD_TYPE=Debug`).
Run the client (or server) like this and do whatever you wanted to test:
@ -17,3 +17,22 @@ This will leave a file called "perf.data".
You can open this file with perf built-in tools but much more interesting
is the visualization using a GUI tool: **[Hotspot](https://github.com/KDAB/hotspot)**.
It will give you flamegraphs, per-thread, per-function views and much more.
### Remote Profiling
Attach perf to your running server, press *^C* to stop:
```bash
perf record -z --call-graph dwarf -F 400 -p "$(pidof minetestserver)"
```
Collect a copy of the required libraries/executables:
```bash
perf buildid-list | grep -Eo '/[^ ]+(minetestserver|\.so)[^ ]*$' | \
tar -cvahf debug.tgz --files-from=- --ignore-failed-read
```
Give both files to the developer and also provide:
* Linux distribution and version
* commit the source was built from and/or modified source code (if applicable)
Hotspot will resolve symbols correctly when pointing the sysroot option at the collected libs.

View File

@ -0,0 +1,109 @@
OS/library compatibility policy
===============================
This document describes how we decide which minimum versions of operating systems, C++ standards,
libraries, build tools (CMake) or compilers Minetest requires.
Most important is that we do not increase our minimum requirements without a reason or use case.
A reason can be as simple as "cleaning up legacy support code", but it needs to exist.
As most development happens on Linux the first measure is to check the version of the component in question on:
* the oldest still-supported **Ubuntu** (End of Standard Support)
* the oldest still-supported **Debian** (*not* LTS)
* optional: the second newest **RHEL (derivative)**
Generally this leads to versions about 5 years old and works as a reasonable result for BSDs and other platforms too.
Needless to say that any new requirements need to work on our other platforms too, as listed below.
### Windows
We currently support Windows 8 or later.
Despite requiring explicit support code in numerous places there doesn't seem to be a strong case
for dropping older Windows versions. We will likely only do it once SDL2 does so.
Note that we're constrained by the versions [vcpkg](https://vcpkg.io/en/packages) offers, for the MSVC build.
### macOS
We currently support macOS 10.14 (Mojave) or later.
Since we do not have any macOS developer we can only do some shallow testing in CI.
So this is subject to change basically whenever Github throws
[a new version](https://github.com/actions/runner-images?tab=readme-ov-file#available-images) at us, or for other reasons.
### Android
We currently support Android 5.0 (API 21) or later.
There's usually no reason to raise this unless the NDK drops older versions.
*Note*: You can check the Google Play Console to see what our user base is running.
## Other parts
**Compilers**: gcc, clang and MSVC (exceptions exist)
**OpenGL** is an entirely different beast, there is no formal consensus on changing the requirements
and neither do we have an exact set of requirements.
We still support OpenGL 1.4 without shaders (fixed-pipeline), which could be considered very unreasonable in 2024.
OpenGL ES 2.0 is supported for the sake of mobile platforms.
It has been [proposed](https://irc.minetest.net/minetest-dev/2022-08-18) moving to OpenGL 2.x or 3.0 with shaders required.
General **system requirements** are not bounded either.
Being able to play Minetest on a recent low-end phone is a reasonable target.
## On totality
These rules are not absolute and there can be exceptions.
But consider how much trouble it would be to chase down a new version of a component on an old distro:
* C++ standard library: probably impossible without breaking your system(?)
* compiler: very annoying
* CMake: somewhat annoying
* some ordinary library: reasonably easy
The rules can be seen more relaxed for optional dependencies, but remember to be reasonable.
Sound is optional at build-time but nobody would call an engine build without sound complete.
In general also consider:
* Is the proposition important enough to warrant a new dependency?
* Can we make it easier for users to build the library together with Minetest?
* Maybe even vendor the library?
* Or could the engine include a transparent fallback implementation?
The SpatialIndex support is a good example for the latter. It is only used to speed up some (relatively unimportant)
API feature, but there's no loss of functionality if you don't have it.
## A concrete example
(as of April 2024)
```
Situation: someone wants C++20 to use std::span
MSVC supports it after some version, should be fine as long as it builds in CI
gcc with libstdc++ 10 or later
clang with libc++ 7 or later (note: no mainstream Linux distros use this)
Debian 11 has libstdc++ 10
Ubuntu 20.04 LTS has libstdc++ 9
(optional) Rocky Linux 8 has libstdc++ 8
Windows, Android and macOS are probably okay
Verdict: not possible. maybe next year.
Possible alternative: use a library that provides a polyfill for std::span
```
## Links
* Ubuntu support table: https://wiki.ubuntu.com/Releases
* Debian support table: https://wiki.debian.org/LTS
* Release table of a RHEL derivative: https://en.wikipedia.org/wiki/AlmaLinux#Releases
* Android API levels: https://apilevels.com/
* C++ standard support information: https://en.cppreference.com/w/cpp/compiler_support
* Distribution-independent package search: https://repology.org/ or https://pkgs.org/

46
doc/docker_server.md Normal file
View File

@ -0,0 +1,46 @@
# Docker Server
We provide Minetest server Docker images using the GitHub container registry.
Images are built on each commit and available using the following tag scheme:
* `ghcr.io/minetest/minetest:master` (latest build)
* `ghcr.io/minetest/minetest:<tag>` (specific Git tag)
* `ghcr.io/minetest/minetest:latest` (latest Git tag, which is the stable release)
See [here](https://github.com/minetest/minetest/pkgs/container/minetest) for all available tags.
For a quick test you can easily run:
```shell
docker run ghcr.io/minetest/minetest:master
```
To use it in a production environment, you should use volumes bound to the Docker host to persist data and modify the configuration:
```shell
docker create -v /home/minetest/data/:/var/lib/minetest/ -v /home/minetest/conf/:/etc/minetest/ ghcr.io/minetest/minetest:master
```
You may also want to use [Docker Compose](https://docs.docker.com/compose):
```yaml
---
version: "2"
services:
minetest_server:
image: ghcr.io/minetest/minetest:master
restart: always
networks:
- default
volumes:
- /home/minetest/data/:/var/lib/minetest/
- /home/minetest/conf/:/etc/minetest/
ports:
- "30000:30000/udp"
- "127.0.0.1:30000:30000/tcp"
```
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](https://docs.docker.com) before use.

View File

@ -659,8 +659,9 @@ The mask is applied using binary AND.
#### `[sheet:<w>x<h>:<x>,<y>`
Retrieves a tile at position x,y from the base image
which it assumes to be a tilesheet with dimensions w,h.
Retrieves a tile at position x, y (in tiles, 0-indexed)
from the base image, which it assumes to be a tilesheet
with dimensions w, h (in tiles).
#### `[colorize:<color>:<ratio>`
@ -1411,8 +1412,7 @@ Look for examples in `games/devtest` or `games/minetest_game`.
* `liquid`
* The cubic source node for a liquid.
* Faces bordering to the same node are never rendered.
* Connects to node specified in `liquid_alternative_flowing`.
* You *must* set `liquid_alternative_source` to the node's own name.
* Connects to node specified in `liquid_alternative_flowing` if specified.
* Use `backface_culling = false` for the tiles you want to make
visible when inside the node.
* `flowingliquid`
@ -1719,6 +1719,8 @@ Displays text on the HUD.
* `scale`: Defines the bounding rectangle of the text.
A value such as `{x=100, y=100}` should work.
* `text`: The text to be displayed in the HUD element.
Supports `minetest.translate` (always)
and `minetest.colorize` (since protocol version 44)
* `number`: An integer containing the RGB value of the color used to draw the
text. Specify `0xFFFFFF` for white text, `0xFF0000` for red, and so on.
* `alignment`: The alignment of the text.
@ -2560,6 +2562,8 @@ Some of the values in the key-value store are handled specially:
0 = default, 1 = left / up, 2 = middle, 3 = right / down
The default currently is the same as right/down.
Example: 6 = 2 + 1*4 = middle,up
* `range`: Overrides the pointing range
Example: `meta:set_float("range", 4.2)`
Example:
@ -3015,6 +3019,16 @@ Elements
centered on `H`. With the new coordinate system, `H` will modify the height.
* `label` is the text on the button
### `button_url[<X>,<Y>;<W>,<H>;<name>;<label>;<url>]`
* Clickable button. When clicked, fields will be sent and the user will be given the
option to open the URL in a browser.
* With the old coordinate system, buttons are a set height, but will be vertically
centered on `H`. With the new coordinate system, `H` will modify the height.
* To make this into an `image_button`, you can use formspec styling.
* `label` is the text on the button.
* `url` must be a valid web URL, starting with `http://` or `https://`.
### `image_button[<X>,<Y>;<W>,<H>;<texture name>;<name>;<label>]`
* `texture name` is the filename of an image
@ -3041,6 +3055,11 @@ Elements
* When clicked, fields will be sent and the form will quit.
* Same as `button` in all other respects.
### `button_url_exit[<X>,<Y>;<W>,<H>;<name>;<label>;<url>]`
* When clicked, fields will be sent and the form will quit.
* Same as `button_url` in all other respects.
### `image_button_exit[<X>,<Y>;<W>,<H>;<texture name>;<name>;<label>]`
* When clicked, fields will be sent and the form will quit.
@ -3516,10 +3535,12 @@ Markup language used in `hypertext[]` elements uses tags that look like HTML tag
The markup language is currently unstable and subject to change. Use with caution.
Some tags can enclose text, they open with `<tagname>` and close with `</tagname>`.
Tags can have attributes, in that case, attributes are in the opening tag in
form of a key/value separated with equal signs. Attribute values should not be quoted.
form of a key/value separated with equal signs.
Attribute values should be quoted using either " or '.
If you want to insert a literal greater-than sign or a backslash into the text,
you must escape it by preceding it with a backslash.
If you want to insert a literal greater-than, less-than, or a backslash into the text,
you must escape it by preceding it with a backslash. In a quoted attribute value, you
can insert a literal quote mark by preceding it with a backslash.
These are the technically basic tags but see below for usual tags. Base tags are:
@ -3536,11 +3557,13 @@ Changes the style of the text.
Sets global style.
Global only styles:
* `background`: Text background, a `colorspec` or `none`.
* `margin`: Page margins in pixel.
* `valign`: Text vertical alignment (`top`, `middle`, `bottom`).
Inheriting styles (affects child elements):
* `color`: Default text color. Given color is a `colorspec`.
* `hovercolor`: Color of <action> tags when mouse is over.
* `size`: Default text size.
@ -3554,6 +3577,7 @@ tags appear.
`<tag name=... color=... hovercolor=... font=... size=...>`
Defines or redefines tag style. This can be used to define new tags.
* `name`: Name of the tag to define or change.
* `color`: Text color. Given color is a `colorspec`.
* `hovercolor`: Text color when element hovered (only for `action` tags). Given color is a `colorspec`.
@ -3586,6 +3610,7 @@ Other tags can be added using `<tag ...>` tag.
Make that text a clickable text triggering an action.
* `name`: Name of the action (mandatory).
* `url`: URL to open when the action is triggered (optional).
When clicked, the formspec is send to the server. The value of the text field
sent to `on_player_receive_fields` will be "action:" concatenated to the action
@ -3640,6 +3665,19 @@ Player Inventory lists
* Is not created automatically, use `InvRef:set_size`
* Is only used to enhance the empty hand's tool capabilities
ItemStack transaction order
---------------------------
This list describes the situation for non-empty ItemStacks in both slots
that cannot be stacked at all, hence triggering an ItemStack swap operation.
Put/take callbacks on empty ItemStack are not executed.
1. The "allow take" and "allow put" callbacks are each run once for the source
and destination inventory.
2. The allowed ItemStacks are exchanged.
3. The "on take" callbacks are run for the source and destination inventories
4. The "on put" callbacks are run for the source and destination inventories
Colors
======
@ -4071,25 +4109,31 @@ Two functions are provided to translate strings: `minetest.translate` and
avoid clashes with other mods.
This function must be given a number of arguments equal to the number of
arguments the translated string expects.
Arguments are literal strings -- they will not be translated, so if you want
them to be, they need to come as outputs of `minetest.translate` as well.
Arguments are literal strings -- they will not be translated.
For instance, suppose we want to translate "@1 Wool" with "@1" being replaced
by the translation of "Red". We can do the following:
For instance, suppose we want to greet players when they join. We can do the
following:
```lua
local S = minetest.get_translator()
S("@1 Wool", S("Red"))
```
```lua
local S = minetest.get_translator("hello")
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
minetest.chat_send_player(name, S("Hello @1, how are you today?", name))
end)
```
This will be displayed as "Red Wool" on old clients and on clients that do
not have localization enabled. However, if we have for instance a translation
file named `wool.fr.tr` containing the following:
When someone called "CoolGuy" joins the game with an old client or a client
that does not have localization enabled, they will see `Hello CoolGuy, how are
you today?`
@1 Wool=Laine @1
Red=Rouge
However, if we have for instance a translation file named `hello.de.tr`
containing the following:
this will be displayed as "Laine Rouge" on clients with a French locale.
# textdomain: hello
Hello @1, how are you today?=Hallo @1, wie geht es dir heute?
and CoolGuy has set a German locale, they will see `Hallo CoolGuy, wie geht es
dir heute?`
Operations on translated strings
--------------------------------
@ -4533,6 +4577,12 @@ Can specify a probability of a node randomly appearing when placed.
This decoration type is intended to be used for multi-node sized discrete
structures, such as trees, cave spikes, rocks, and so on.
`lsystem`
-----------
Generates a L-system tree at the position where the decoration is placed.
Uses the same L-system as `minetest.spawn_tree`, but is faster than using it manually.
The `treedef` field in the decoration definition is used for the tree definition.
@ -5076,6 +5126,9 @@ Collision info passed to `on_step` (`moveresult` argument):
axis = string, -- "x", "y" or "z"
node_pos = vector, -- if type is "node"
object = ObjectRef, -- if type is "object"
-- The position of the entity when the collision occurred.
-- Available since feature "moveresult_new_pos".
new_pos = vector,
old_velocity = vector,
new_velocity = vector,
},
@ -5285,6 +5338,12 @@ Utilities
* `minetest.get_worldpath()`: returns e.g. `"/home/user/.minetest/world"`
* Useful for storing custom data
* `minetest.get_mod_data_path()`: returns e.g. `"/home/user/.minetest/mod_data/mymod"`
* Useful for storing custom data *independently of worlds*.
* Must be called during mod load time.
* Can read or write to this directory at any time.
* It's possible that multiple Minetest instances are running at the same
time, which may lead to corruption if you are not careful.
* `minetest.is_singleplayer()`
* `minetest.features`: Table containing API feature flags
@ -5376,6 +5435,15 @@ Utilities
dynamic_add_media_startup = true,
-- dynamic_add_media supports `filename` and `filedata` parameters (5.9.0)
dynamic_add_media_filepath = true,
-- L-system decoration type (5.9.0)
lsystem_decoration_type = true,
-- Overrideable pointing range using the itemstack meta key `"range"` (5.9.0)
item_meta_range = true,
-- Allow passing an optional "actor" ObjectRef to the following functions:
-- minetest.place_node, minetest.dig_node, minetest.punch_node (5.9.0)
node_interaction_actor = true,
-- "new_pos" field in entity moveresult (5.9.0)
moveresult_new_pos = true,
}
```
@ -5668,7 +5736,7 @@ Call these functions only at load time!
* Called when a player is punched
* Note: This callback is invoked even if the punched player is dead.
* `player`: ObjectRef - Player that was punched
* `hitter`: ObjectRef - Player that hit
* `hitter`: ObjectRef - Player that hit. Can be nil.
* `time_from_last_punch`: Meant for disallowing spamming of clicks
(can be nil).
* `tool_capabilities`: Capability table of used item (can be nil)
@ -5747,8 +5815,8 @@ Call these functions only at load time!
* Return `true` to mark the command as handled, which means that the default
handlers will be prevented.
* `minetest.register_on_player_receive_fields(function(player, formname, fields))`
* Called when the server received input from `player` in a formspec with
the given `formname`. Specifically, this is called on any of the
* Called when the server received input from `player`.
Specifically, this is called on any of the
following events:
* a button was pressed,
* Enter was pressed while the focus was on a text field
@ -5759,6 +5827,9 @@ Call these functions only at load time!
* an entry was double-clicked in a textlist or table,
* a scrollbar was moved, or
* the form was actively closed by the player.
* `formname` is the name passed to `minetest.show_formspec`.
Special case: The empty string refers to the player inventory
(the formspec set by the `set_inventory_formspec` player method).
* Fields are sent for formspec elements which define a field. `fields`
is a table containing each formspecs element value (as string), with
the `name` parameter as index for each. The value depends on the
@ -5944,8 +6015,9 @@ handler.
Chat
----
* `minetest.chat_send_all(text)`
* `minetest.chat_send_player(name, text)`
* `minetest.chat_send_all(text)`: send chat message to all players
* `minetest.chat_send_player(name, text)`: send chat message to specific player
* `name`: Name of the player
* `minetest.format_chat_message(name, message)`
* Used by the server to format a chat message, based on the setting `chat_message_format`.
Refer to the documentation of the setting for a list of valid placeholders.
@ -5957,13 +6029,14 @@ Environment access
------------------
* `minetest.set_node(pos, node)`
* `minetest.add_node(pos, node)`: alias to `minetest.set_node`
* Set node at position `pos`
* Set node at position `pos`.
* Any existing metadata is deleted.
* `node`: table `{name=string, param1=number, param2=number}`
* If param1 or param2 is omitted, it's set to `0`.
If param1 or param2 is omitted, it's set to `0`.
* e.g. `minetest.set_node({x=0, y=10, z=0}, {name="default:wood"})`
* `minetest.add_node(pos, node)`: alias to `minetest.set_node`
* `minetest.bulk_set_node({pos1, pos2, pos3, ...}, node)`
* Set node on all positions set in the first argument.
* Set the same node at all positions in the first argument.
* e.g. `minetest.bulk_set_node({{x=0, y=1, z=1}, {x=1, y=2, z=2}}, {name="default:stone"})`
* For node specification or position syntax see `minetest.set_node` call
* Faster than set_node due to single call, but still considerably slower
@ -5973,16 +6046,17 @@ Environment access
For setting a cube, this is 1.3x faster than set_node whereas LVM is 20
times faster.
* `minetest.swap_node(pos, node)`
* Set node at position, but don't remove metadata
* `minetest.remove_node(pos)`
* By default it does the same as `minetest.set_node(pos, {name="air"})`
* Swap node at position with another.
* This keeps the metadata intact and will not run con-/destructor callbacks.
* `minetest.remove_node(pos)`: Remove a node
* Equivalent to `minetest.set_node(pos, {name="air"})`, but a bit faster.
* `minetest.get_node(pos)`
* Returns the node at the given position as table in the format
`{name="node_name", param1=0, param2=0}`,
returns `{name="ignore", param1=0, param2=0}` for unloaded areas.
* Returns the node at the given position as table in the same format as `set_node`.
* This function never returns `nil` and instead returns
`{name="ignore", param1=0, param2=0}` for unloaded areas.
* `minetest.get_node_or_nil(pos)`
* Same as `get_node` but returns `nil` for unloaded areas.
* Note that areas may still contain "ignore" despite being loaded.
* Note that even loaded areas can contain "ignore" nodes.
* `minetest.get_node_light(pos[, timeofday])`
* Gets the light value at the given position. Note that the light value
"inside" the node at the given position is returned, so you usually want
@ -6006,13 +6080,16 @@ Environment access
* Returns a number between `0` and `15`
* Currently it's the same as `math.floor(param1 / 16)`, except that it
ensures compatibility.
* `minetest.place_node(pos, node)`
* `minetest.place_node(pos, node[, placer])`
* Place node with the same effects that a player would cause
* `minetest.dig_node(pos)`
* `placer`: The ObjectRef that places the node (optional)
* `minetest.dig_node(pos[, digger])`
* Dig node with the same effects that a player would cause
* `digger`: The ObjectRef that digs the node (optional)
* Returns `true` if successful, `false` on failure (e.g. protected location)
* `minetest.punch_node(pos)`
* `minetest.punch_node(pos[, puncher])`
* Punch node with the same effects that a player would cause
* `puncher`: The ObjectRef that punches the node (optional)
* `minetest.spawn_falling_node(pos)`
* Change node into falling node
* Returns `true` and the ObjectRef of the spawned entity if successful, `false` on failure
@ -6034,20 +6111,21 @@ Environment access
* Returns `ObjectRef`, or `nil` if failed
* Items can be added also to unloaded and non-generated blocks.
* `minetest.get_player_by_name(name)`: Get an `ObjectRef` to a player
* `minetest.get_objects_inside_radius(pos, radius)`: returns a list of
ObjectRefs.
* Returns nothing in case of error (player offline, doesn't exist, ...).
* `minetest.get_objects_inside_radius(pos, radius)`
* returns a list of ObjectRefs.
* `radius`: using a Euclidean metric
* `minetest.get_objects_in_area(pos1, pos2)`: returns a list of
ObjectRefs.
* `pos1` and `pos2` are the min and max positions of the area to search.
* `minetest.set_timeofday(val)`
* `minetest.get_objects_in_area(pos1, pos2)`
* returns a list of ObjectRefs.
* `pos1` and `pos2` are the min and max positions of the area to search.
* `minetest.set_timeofday(val)`: set time of day
* `val` is between `0` and `1`; `0` for midnight, `0.5` for midday
* `minetest.get_timeofday()`
* `minetest.get_timeofday()`: get time of day
* `minetest.get_gametime()`: returns the time, in seconds, since the world was
created. The time is not available (`nil`) before the first server step.
* `minetest.get_day_count()`: returns number days elapsed since world was
created.
* accounts for time changes.
* Time changes are accounted for.
* `minetest.find_node_near(pos, radius, nodenames, [search_center])`: returns
pos or `nil`.
* `radius`: using a maximum metric
@ -6350,7 +6428,8 @@ Formspec
* `minetest.show_formspec(playername, formname, formspec)`
* `playername`: name of player to show formspec
* `formname`: name passed to `on_player_receive_fields` callbacks.
It should follow the `"modname:<whatever>"` naming convention
It should follow the `"modname:<whatever>"` naming convention.
`formname` must not be empty.
* `formspec`: formspec to display
* `minetest.close_formspec(playername, formname)`
* `playername`: name of player to close formspec
@ -7258,8 +7337,41 @@ Global tables
All callbacks registered with [Global callback registration functions] are added
to corresponding `minetest.registered_*` tables.
For historical reasons, the use of an -s suffix in these names is inconsistent.
* `minetest.registered_on_chat_messages`
* `minetest.registered_on_chatcommands`
* `minetest.registered_globalsteps`
* `minetest.registered_on_punchnodes`
* `minetest.registered_on_placenodes`
* `minetest.registered_on_dignodes`
* `minetest.registered_on_generateds`
* `minetest.registered_on_newplayers`
* `minetest.registered_on_dieplayers`
* `minetest.registered_on_respawnplayers`
* `minetest.registered_on_prejoinplayers`
* `minetest.registered_on_joinplayers`
* `minetest.registered_on_leaveplayers`
* `minetest.registered_on_player_receive_fields`
* `minetest.registered_on_cheats`
* `minetest.registered_on_crafts`
* `minetest.registered_craft_predicts`
* `minetest.registered_on_item_eats`
* `minetest.registered_on_item_pickups`
* `minetest.registered_on_punchplayers`
* `minetest.registered_on_authplayers`
* `minetest.registered_on_player_inventory_actions`
* `minetest.registered_allow_player_inventory_actions`
* `minetest.registered_on_rightclickplayers`
* `minetest.registered_on_mods_loaded`
* `minetest.registered_on_shutdown`
* `minetest.registered_on_protection_violation`
* `minetest.registered_on_priv_grant`
* `minetest.registered_on_priv_revoke`
* `minetest.registered_can_bypass_userlimit`
* `minetest.registered_on_modchannel_message`
* `minetest.registered_on_liquid_transformed`
* `minetest.registered_on_mapblocks_changed`
Class reference
===============
@ -7355,6 +7467,7 @@ An `InvRef` is a reference to an inventory.
* returns `false` on error (e.g. invalid `listname` or `size`)
* `get_width(listname)`: get width of a list
* `set_width(listname, width)`: set width of list; currently used for crafting
* returns `false` on error (e.g. invalid `listname` or `width`)
* `get_stack(listname, i)`: get a copy of stack index `i` in list
* `set_stack(listname, i, stack)`: copy `stack` to index `i` in list
* `get_list(listname)`: returns full list (list of `ItemStack`s)
@ -7723,11 +7836,11 @@ child will follow movement and rotation of that bone.
* no-op if object is attached
* `punch(puncher, time_from_last_punch, tool_capabilities, dir)`
* punches the object, triggering all consequences a normal punch would have
* `puncher`: another `ObjectRef` which punched the object
* `puncher`: another `ObjectRef` which punched the object or `nil`
* `dir`: direction vector of punch
* Other arguments: See `on_punch` for entities
* All arguments except `puncher` can be `nil`, in which case a default
value will be used
* Arguments `time_from_last_punch`, `tool_capabilities`, and `dir`
will be replaced with a default value when the caller sets them to `nil`.
* `right_click(clicker)`:
* simulates using the 'place/use' key on the object
* triggers all consequences as if a real player had done this
@ -7889,8 +8002,12 @@ child will follow movement and rotation of that bone.
* Fourth column: subject looking to the right
* Fifth column: subject viewed from above
* Sixth column: subject viewed from below
* `get_entity_name()` (**Deprecated**: Will be removed in a future version, use the field `self.name` instead)
* `get_luaentity()`: returns the object's associated luaentity table
* `get_luaentity()`:
* Returns the object's associated luaentity table, if there is one
* Otherwise returns `nil` (e.g. for players)
* `get_entity_name()`:
* **Deprecated**: Will be removed in a future version,
use `:get_luaentity().name` instead.
#### Player only (no-op for other objects)
@ -7983,13 +8100,18 @@ child will follow movement and rotation of that bone.
* `set_physics_override(override_table)`
* Overrides the physics attributes of the player
* `override_table` is a table with the following fields:
* `speed`: multiplier to default movement speed and acceleration values (default: `1`)
* `jump`: multiplier to default jump value (default: `1`)
* `gravity`: multiplier to default gravity value (default: `1`)
* `speed`: multiplier to *all* movement speed (`speed_*`) and
acceleration (`acceleration_*`) values (default: `1`)
* `speed_walk`: multiplier to default walk speed value (default: `1`)
* Note: The actual walk speed is the product of `speed` and `speed_walk`
* `speed_climb`: multiplier to default climb speed value (default: `1`)
* Note: The actual climb speed is the product of `speed` and `speed_climb`
* `speed_crouch`: multiplier to default sneak speed value (default: `1`)
* Note: The actual sneak speed is the product of `speed` and `speed_crouch`
* `speed_fast`: multiplier to default speed value in Fast Mode (default: `1`)
* Note: The actual fast speed is the product of `speed` and `speed_fast`
* `jump`: multiplier to default jump value (default: `1`)
* `gravity`: multiplier to default gravity value (default: `1`)
* `liquid_fluidity`: multiplier to liquid movement resistance value
(for nodes with `liquid_move_physics`); the higher this value, the lower the
resistance to movement. At `math.huge`, the resistance is zero and you can
@ -8007,6 +8129,8 @@ child will follow movement and rotation of that bone.
* `acceleration_air`: multiplier to acceleration
when jumping or falling (default: `1`)
* Note: The actual acceleration is the product of `speed` and `acceleration_air`
* `acceleration_fast`: multiplier to acceleration in Fast Mode (default: `1`)
* Note: The actual acceleration is the product of `speed` and `acceleration_fast`
* `sneak`: whether player can sneak (default: `true`)
* `sneak_glitch`: whether player can use the new move code replications
of the old sneak side-effects: sneak ladders and 2 node sneak jump
@ -8133,14 +8257,13 @@ child will follow movement and rotation of that bone.
`"default"` uses the classic Minetest sun and moon tinting.
Will use tonemaps, if set to `"default"`. (default: `"default"`)
* `fog`: A table with following optional fields:
* `fog_distance`: integer, set an upper bound the client's viewing_range (inluding range_all).
By default, fog_distance is controlled by the client's viewing_range, and this field is not set.
Any value >= 0 sets the desired upper bound for the client's viewing_range and disables range_all.
Any value < 0, resets the behavior to being client-controlled.
* `fog_distance`: integer, set an upper bound for the client's viewing_range.
Any value >= 0 sets the desired upper bound for viewing_range,
disables range_all and prevents disabling fog (F3 key by default).
Any value < 0 resets the behavior to being client-controlled.
(default: -1)
* `fog_start`: float, override the client's fog_start.
Fraction of the visible distance at which fog starts to be rendered.
By default, fog_start is controlled by the client's `fog_start` setting, and this field is not set.
Any value between [0.0, 0.99] set the fog_start as a fraction of the viewing_range.
Any value < 0, resets the behavior to being client-controlled.
(default: -1)
@ -8609,7 +8732,8 @@ Player properties need to be saved manually.
-- "mesh" uses the defined mesh model.
-- "wielditem" is used for dropped items.
-- (see builtin/game/item_entity.lua).
-- For this use 'wield_item = itemname' (Deprecated: 'textures = {itemname}').
-- For this use 'wield_item = itemname'.
-- Setting 'textures = {itemname}' has the same effect, but is deprecated.
-- If the item has a 'wield_image' the object will be an extrusion of
-- that, otherwise:
-- If 'itemname' is a cubic node or nodebox the object will appear
@ -8636,8 +8760,8 @@ Player properties need to be saved manually.
-- "cube" uses 6 textures just like a node, but all 6 must be defined.
-- "sprite" uses 1 texture.
-- "upright_sprite" uses 2 textures: {front, back}.
-- "wielditem" expects 'textures = {itemname}' (see 'visual' above).
-- "mesh" requires one texture for each mesh buffer/material (in order)
-- Deprecated usage of "wielditem" expects 'textures = {itemname}' (see 'visual' above).
colors = {},
-- Number of required colors depends on visual
@ -8959,6 +9083,7 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and
range = 4.0,
-- Range of node and object pointing that is possible with this item held
-- Can be overridden with itemstack meta.
liquids_pointable = false,
-- If true, item can point to all liquid nodes (`liquidtype ~= "none"`),
@ -9039,19 +9164,20 @@ Used by `minetest.register_node`, `minetest.register_craftitem`, and
-- Otherwise should be name of node which the client immediately places
-- upon digging. Server will always update with actual result shortly.
touch_interaction = {
-- Only affects touchscreen clients.
-- Defines the meaning of short and long taps with the item in hand.
-- The fields in this table have two valid values:
-- * "long_dig_short_place" (long tap = dig, short tap = place)
-- * "short_dig_long_place" (short tap = dig, long tap = place)
-- The field to be used is selected according to the current
-- `pointed_thing`.
pointed_nothing = "long_dig_short_place",
pointed_node = "long_dig_short_place",
pointed_object = "short_dig_long_place",
touch_interaction = <TouchInteractionMode> OR {
pointed_nothing = <TouchInteractionMode>,
pointed_node = <TouchInteractionMode>,
pointed_object = <TouchInteractionMode>,
},
-- Only affects touchscreen clients.
-- Defines the meaning of short and long taps with the item in hand.
-- If specified as a table, the field to be used is selected according to
-- the current `pointed_thing`.
-- There are three possible TouchInteractionMode values:
-- * "user" (meaning depends on client-side settings)
-- * "long_dig_short_place" (long tap = dig, short tap = place)
-- * "short_dig_long_place" (short tap = dig, long tap = place)
-- The default value is "user".
sound = {
-- Definition of item sounds to be played at various events.
@ -9264,10 +9390,8 @@ Used by `minetest.register_node`.
-- flowing version (`liquid_alternative_flowing`) and
-- source version (`liquid_alternative_source`) of a liquid.
--
-- Specifically, these fields are required if any of these is true:
-- * `liquidtype ~= "none" or
-- * `drawtype == "liquid" or
-- * `drawtype == "flowingliquid"
-- Specifically, these fields are required if `liquidtype ~= "none"` or
-- `drawtype == "flowingliquid"`.
--
-- Liquids consist of up to two nodes: source and flowing.
--
@ -9553,6 +9677,7 @@ Used by `minetest.register_node`.
on_receive_fields = function(pos, formname, fields, sender),
-- fields = {name1 = value1, name2 = value2, ...}
-- formname should be the empty string; you **must not** use formname.
-- Called when an UI form (e.g. sign text input) returns data.
-- See minetest.register_on_player_receive_fields for more info.
-- default: nil
@ -10134,7 +10259,7 @@ See [Decoration types]. Used by `minetest.register_decoration`.
```lua
{
deco_type = "simple",
-- Type. "simple" or "schematic" supported
-- Type. "simple", "schematic" or "lsystem" supported
place_on = "default:dirt_with_grass",
-- Node (or list of nodes) that the decoration can be placed on
@ -10287,6 +10412,12 @@ See [Decoration types]. Used by `minetest.register_decoration`.
-- Effect is inverted for "all_ceilings" decorations.
-- Ignored by 'y_min', 'y_max' and 'spawn_by' checks, which always refer
-- to the 'place_on' node.
----- L-system-type parameters
treedef = {},
-- Same as for `minetest.spawn_tree`.
-- See section [L-system trees] for more details.
}
```

View File

@ -55,10 +55,6 @@ Functions
* Android only. Shares file using the share popup
* `core.get_version()` (possible in async calls)
* returns current core version
* `core.set_once(key, value)`:
* save a string value that persists even if menu is closed
* `core.get_once(key)`:
* get a string value saved by above function, or `nil`
@ -97,8 +93,8 @@ Filesystem
registered in the core (possible in async calls)
* `core.get_cache_path()` -> path of cache
* `core.get_temp_path([param])` (possible in async calls)
* `param`=true: returns path to a temporary file
otherwise: returns path to the temporary folder
* `param`=true: returns path to a newly created temporary file
* otherwise: returns path to a newly created temporary folder
HTTP Requests

View File

@ -76,3 +76,26 @@ minetest.register_entity("callbacks:callback_step", {
message("on_step callback entity: on_step! pos="..spos(self).."; dtime="..dtime)
end,
})
-- Callback punch with nil puncher
minetest.register_entity("callbacks:callback_puncher", {
initial_properties = {
visual = "upright_sprite",
textures = { "callbacks_callback_entity.png" },
infotext = "Callback entity for nil puncher test.",
},
on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir, damage)
if puncher then
puncher:punch(nil, time_from_last_punch, tool_capabilities, dir)
self.object:punch(nil, time_from_last_punch, tool_capabilities, dir)
else
message(
"Callback entity: on_punch with nil puncher "..
"pos="..spos(self).."; "..
"time_from_last_punch="..time_from_last_punch.."; "..
"tool_capabilities="..dump(tool_capabilities).."; "..
"dir="..dump(dir).."; damage="..damage)
end
end,
})

View File

@ -1,3 +1,4 @@
dofile(minetest.get_modpath("callbacks").."/items.lua")
dofile(minetest.get_modpath("callbacks").."/nodes.lua")
dofile(minetest.get_modpath("callbacks").."/entities.lua")
dofile(minetest.get_modpath("callbacks").."/players.lua")

View File

@ -0,0 +1,11 @@
local message = function(msg)
minetest.log("action", "[callbacks] "..msg)
minetest.chat_send_all(msg)
end
core.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, dir, damage)
if not hitter then
message("Player "..player:get_player_name().." punched without hitter.")
end
end)

View File

@ -29,12 +29,16 @@ minetest.register_node("chest:chest", {
return inv:is_empty("main")
end,
allow_metadata_inventory_put = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow put' event for " .. stack:to_string())
return stack:get_count()
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow put' (10) event for " .. stack:to_string())
return 10
end,
allow_metadata_inventory_take = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow take' event for " .. stack:to_string())
return stack:get_count()
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow take' (20) event for " .. stack:to_string())
return 20
end,
allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
print_to_everything("Chest: ".. player:get_player_name() .. " triggered 'allow move' (30) event")
return 30
end,
on_metadata_inventory_put = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " put " .. stack:to_string())
@ -42,4 +46,7 @@ minetest.register_node("chest:chest", {
on_metadata_inventory_take = function(pos, listname, index, stack, player)
print_to_everything("Chest: ".. player:get_player_name() .. " took " .. stack:to_string())
end,
on_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player)
print_to_everything("Chest: ".. player:get_player_name() .. " moved " .. count)
end,
})

View File

@ -1,12 +1,15 @@
local color = minetest.colorize
-- \208\176 is a cyrillic small a
local unsafe_url = minetest.formspec_escape("https://u:p@wikipedi\208\176.org:1233/heIIoll?a=b#c")
local clip_fs = [[
style_type[label,button,image_button,item_image_button,
tabheader,scrollbar,table,animated_image
,field,textarea,checkbox,dropdown;noclip=%c]
label[0,0;A clipping test]
button[0,1;3,0.8;clip_button;A clipping test]
button_url[0,1;3,0.8;clip_button;A clipping test;]] .. unsafe_url .. [[]
image_button[0,2;3,0.8;testformspec_button_image.png;clip_image_button;A clipping test]
item_image_button[0,3;3,0.8;testformspec:item;clip_item_image_button;A clipping test]
tabheader[0,4.7;3,0.63;clip_tabheader;Clip,Test,Text,Tabs;1;false;false]
@ -61,14 +64,14 @@ local inv_style_fs = [[
list[current_player;main;.5,7;8,4]
]]
local hypertext_basic = [[
local hypertext_basic = [[A hypertext element
<bigger>Normal test</bigger>
This is a normal text.
<bigger><mono>style</mono> test</bigger>
<style color=#FFFF00>Yellow text.</style> <style color=#FF0000>Red text.</style>
<style size=24>Size 24.</style> <style size=16>Size 16</style>. <style size=12>Size 12.</style>
<style font=normal>Normal font.</style> <style font=mono>Mono font.</style>
<style color="#FFFF00">Yellow text.</style> <style color='#FF0000'>Red text.</style>
<style size="24">Size 24.</style> <style size=16>Size 16</style>. <style size=12>Size 12.</style>
<style font="normal">Normal font.</style> <style font=mono>Mono font.</style>
<bigger>Tag test</bigger>
<normal>normal</normal>
@ -85,19 +88,20 @@ This is a normal text.
<bigger>Custom tag test</bigger>
<tag name=t_green color=green>
<tag name=t_hover hovercolor=yellow>
<tag name=t_size size=24>
<tag name=t_mono font=mono>
<tag name=t_multi color=green font=mono size=24>
<tag name="t_hover" hovercolor=yellow>
<tag name="t_size" size=24>
<tag name="t_mono" font=mono>
<tag name="t_multi" color=green font=mono size=24>
<t_green>color=green</t_green>
Action: <action name=color><t_green>color=green</t_green></action>
Action: <action name=hovercolor><t_hover>hovercolor=yellow</t_hover></action>
Action: <action name="color"><t_green>color=green</t_green></action>
Action: <action name="hovercolor"><t_hover>hovercolor=yellow</t_hover></action>
Action URL: <action name="open" url="https://example.com/?a=b#c">open URL</action>
<t_size>size=24</t_size>
<t_mono>font=mono</t_mono>
<t_multi>color=green font=mono size=24</t_multi>
<bigger><mono>action</mono> test</bigger>
<action name=action_test>action</action>
<action name="action_test">action</action>
<bigger><mono>img</mono> test</bigger>
Normal:
@ -145,7 +149,7 @@ local hypertext_fs = "hypertext[0,0;11,9;hypertext;"..minetest.formspec_escape(h
local style_fs = [[
style[one_btn1;bgcolor=red;textcolor=yellow;bgcolor_hovered=orange;
bgcolor_pressed=purple]
button[0,0;2.5,0.8;one_btn1;Button]
button_url_exit[0,0;2.5,0.8;one_btn1;Button;]] .. unsafe_url .. [[]
style[one_btn2;border=false;textcolor=cyan] ]]..
"button[0,1.05;2.5,0.8;one_btn2;Text " .. color("#FF0", "Yellow") .. [[]

View File

@ -8,6 +8,8 @@ local font_states = {
{4, "Monospace font"},
{5, "Bold and monospace font"},
{7, "ZOMG all the font styles"},
{7, "Colors test! " .. minetest.colorize("green", "Green") ..
minetest.colorize("red", "\nRed") .. " END"},
}

View File

@ -90,3 +90,18 @@ minetest.register_craftitem("testitems:image_meta", {
return itemstack
end,
})
minetest.register_craftitem("testitems:telescope_stick", {
description = S("Telescope Stick (Increases range on use.)"),
inventory_image = "testitems_telescope_stick.png",
on_use = function(itemstack, player)
local meta = itemstack:get_meta()
local range = meta:get_float("range") + 1.2
if range > 10 then
range = 0
end
meta:set_float("range", range)
minetest.chat_send_player(player:get_player_name(), "Telescope Stick range set to "..range)
return itemstack
end,
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

View File

@ -78,6 +78,32 @@ minetest.register_node("testnodes:4dir_nodebox", {
groups = {dig_immediate=3},
})
minetest.register_node("testnodes:4dir_nodebox_stair", {
description = S("4dir Nodebox Stair Test Node").."\n"..
S("param2 = 4dir rotation (0..3)"),
tiles = {
"testnodes_1f.png",
"testnodes_2f.png",
"testnodes_3f.png",
"testnodes_4f.png",
"testnodes_5f.png",
"testnodes_6f.png",
},
drawtype = "nodebox",
paramtype = "light",
paramtype2 = "4dir",
node_box = {
type = "fixed",
fixed = {
{-0.5, -0.5, -0.5, 0.5, 0, 0.5},
{-0.5, 0, 0, 0.5, 0.5, 0.5},
},
},
groups = { dig_immediate = 3 },
})
minetest.register_node("testnodes:wallmounted", {
description = S("Wallmounted Test Node").."\n"..
S("param2 = wallmounted rotation (0..7)"),

View File

@ -33,6 +33,25 @@ minetest.register_node("testnodes:anim", {
groups = { dig_immediate = 2 },
})
minetest.register_node("testnodes:fill_positioning", {
description = S("Fill Modifier Test Node") .. "\n" ..
S("The node should have the same look as " ..
"testnodes:fill_positioning_reference."),
drawtype = "glasslike",
paramtype = "light",
tiles = {"[fill:16x16:#ffffff^[fill:6x6:1,1:#00ffdc" ..
"^[fill:6x6:1,9:#00ffdc^[fill:6x6:9,1:#00ffdc^[fill:6x6:9,9:#00ffdc"},
groups = {dig_immediate = 3},
})
minetest.register_node("testnodes:fill_positioning_reference", {
description = S("Fill Modifier Test Node Reference"),
drawtype = "glasslike",
paramtype = "light",
tiles = {"testnodes_fill_positioning_reference.png"},
groups = {dig_immediate = 3},
})
-- Node texture transparency test
local alphas = { 64, 128, 191 }
@ -69,6 +88,19 @@ for a=1,#alphas do
})
end
minetest.register_node("testnodes:alpha_compositing", {
description = S("Alpha Compositing Test Node") .. "\n" ..
S("A regular grid should be visible where each cell contains two " ..
"texels with the same colour.") .. "\n" ..
S("Alpha compositing is gamma-incorrect for backwards compatibility."),
drawtype = "glasslike",
paramtype = "light",
tiles = {"testnodes_alpha_compositing_bottom.png^" ..
"testnodes_alpha_compositing_top.png"},
use_texture_alpha = "blend",
groups = {dig_immediate = 3},
})
-- Generate PNG textures
local function mandelbrot(w, h, iterations)

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

View File

@ -184,6 +184,7 @@ dofile(modpath .. "/itemstack_equals.lua")
dofile(modpath .. "/content_ids.lua")
dofile(modpath .. "/metadata.lua")
dofile(modpath .. "/raycast.lua")
dofile(modpath .. "/inventory.lua")
--------------

View File

@ -0,0 +1,73 @@
local item_with_meta = ItemStack({name = "air", meta = {test = "abc"}})
local test_list = {
ItemStack("air"),
ItemStack(""),
ItemStack(item_with_meta),
}
local function compare_lists(a, b)
if not a or not b or #a ~= #b then
return false
end
for i=1, #a do
if not ItemStack(a[i]):equals(ItemStack(b[i])) then
return false
end
end
return true
end
local function test_inventory()
local inv = minetest.create_detached_inventory("test")
inv:set_lists({test = {""}})
assert(inv:get_list("test"))
assert(inv:get_size("test") == 1)
assert(inv:set_size("test", 3))
assert(not inv:set_size("test", -1))
assert(inv:get_width("test") == 0)
assert(inv:set_width("test", 3))
assert(not inv:set_width("test", -1))
inv:set_stack("test", 1, "air")
inv:set_stack("test", 3, item_with_meta)
assert(not inv:is_empty("test"))
assert(compare_lists(inv:get_list("test"), test_list))
assert(inv:add_item("test", "air") == ItemStack())
assert(inv:add_item("test", item_with_meta) == ItemStack())
assert(inv:get_stack("test", 1) == ItemStack("air 2"))
assert(inv:room_for_item("test", "air 99"))
inv:set_stack("test", 2, "air 99")
assert(not inv:room_for_item("test", "air 99"))
inv:set_stack("test", 2, "")
assert(inv:contains_item("test", "air"))
assert(not inv:contains_item("test", "air 99"))
assert(inv:contains_item("test", item_with_meta, true))
-- Items should be removed in reverse and combine with first stack removed
assert(inv:remove_item("test", "air") == item_with_meta)
item_with_meta:set_count(2)
assert(inv:remove_item("test", "air 2") == item_with_meta)
assert(inv:remove_item("test", "air") == ItemStack("air"))
assert(inv:is_empty("test"))
-- Failure of set_list(s) should not change inventory
local before = inv:get_list("test")
pcall(inv.set_lists, inv, {test = true})
pcall(inv.set_list, inv, "test", true)
local after = inv:get_list("test")
assert(compare_lists(before, after))
local location = inv:get_location()
assert(minetest.remove_detached_inventory("test"))
assert(not minetest.get_inventory(location))
end
unittests.register("test_inventory", test_inventory)

10
irr/.editorconfig Executable file
View File

@ -0,0 +1,10 @@
root = true
[*]
charset = utf-8
[*.{cpp,h,txt,cmake,fsh,vsh}]
indent_size = 4
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true

310
irr/.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,310 @@
name: build
# build on c/cpp changes or workflow changes
on:
- push
- pull_request
jobs:
linux-gl:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install g++ cmake libxi-dev libgl1-mesa-dev libpng-dev libjpeg-dev zlib1g-dev -qyy
- name: Build
run: |
cmake . -DUSE_SDL2=OFF
make VERBOSE=1 -j2
- name: Test
run: |
ctest --output-on-failure
- name: Package
run: |
make DESTDIR=$PWD/_install install
tar -c -I "gzip -9" -f irrlicht-linux.tar.gz -C ./_install/usr/local .
- uses: actions/upload-artifact@v4
with:
name: irrlicht-linux
path: ./irrlicht-linux.tar.gz
linux-gles:
# Xvfb test is broken on 20.04 for unknown reasons (not our bug)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install g++ cmake libxi-dev libgles2-mesa-dev libpng-dev libjpeg-dev zlib1g-dev xvfb -qyy
- name: Build
run: |
cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=OFF -DENABLE_OPENGL=OFF -DENABLE_GLES2=ON
make -j2
- name: Test (headless)
run: |
cd bin/Linux
./AutomatedTest null
- name: Test (Xvfb)
run: |
cd bin/Linux
LIBGL_ALWAYS_SOFTWARE=true xvfb-run ./AutomatedTest ogles2
linux-sdl:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install g++ cmake libsdl2-dev libpng-dev libjpeg-dev zlib1g-dev -qyy
- name: Build
run: |
cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=ON -DCMAKE_BUILD_TYPE=Debug
make -j2
- name: Test (headless)
run: |
cd bin/Linux
./AutomatedTest null
linux-sdl-gl3:
# Xvfb test is broken on 20.04 for unknown reasons (not our bug)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install g++ cmake libsdl2-dev libpng-dev libjpeg-dev zlib1g-dev xvfb -qyy
- name: Build
run: |
cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=ON -DENABLE_OPENGL=OFF -DENABLE_OPENGL3=ON
make -j2
- name: Test (headless)
run: |
cd bin/Linux
./AutomatedTest null
- name: Test (Xvfb)
run: |
cd bin/Linux
LIBGL_ALWAYS_SOFTWARE=true xvfb-run ./AutomatedTest opengl3
linux-sdl-gles2:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install g++ cmake libsdl2-dev libpng-dev libjpeg-dev zlib1g-dev xvfb -qyy
- name: Build
run: |
cmake . -DBUILD_EXAMPLES=1 -DUSE_SDL2=ON -DENABLE_OPENGL=OFF -DENABLE_GLES2=ON
make -j2
- name: Test (headless)
run: |
cd bin/Linux
./AutomatedTest null
- name: Test (Xvfb)
run: |
cd bin/Linux
LIBGL_ALWAYS_SOFTWARE=true xvfb-run ./AutomatedTest ogles2
mingw:
name: "MinGW ${{matrix.config.variant}}${{matrix.config.extras}}"
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
config:
- {variant: win32, arch: i686}
- {variant: win64, arch: x86_64}
- {variant: win32, arch: i686, extras: "-sdl"}
- {variant: win64, arch: x86_64, extras: "-sdl"}
steps:
- uses: actions/checkout@v4
- name: Install compiler
run: |
sudo apt-get update && sudo apt-get install cmake -qyy
./scripts/ci-get-mingw.sh
- name: Build
run: |
./scripts/ci-build-mingw.sh package
env:
CC: ${{matrix.config.arch}}-w64-mingw32-clang
CXX: ${{matrix.config.arch}}-w64-mingw32-clang++
extras: ${{matrix.config.extras}}
- uses: actions/upload-artifact@v4
with:
name: irrlicht-${{matrix.config.variant}}${{matrix.config.extras}}
path: ./irrlicht-${{matrix.config.variant}}${{matrix.config.extras}}.zip
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
brew update --auto-update
brew install cmake libpng jpeg
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
HOMEBREW_NO_INSTALL_CLEANUP: 1
- name: Build
run: |
cmake . -DCMAKE_FIND_FRAMEWORK=LAST -DBUILD_EXAMPLES=1
make -j3
- name: Test (headless)
run: |
./bin/OSX/AutomatedTest null
macos-sdl:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
brew update --auto-update
brew install cmake libpng jpeg sdl2
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
HOMEBREW_NO_INSTALL_CLEANUP: 1
- name: Build
run: |
cmake . -DCMAKE_FIND_FRAMEWORK=LAST -DBUILD_EXAMPLES=1 -DUSE_SDL2=1
make -j3
msvc:
name: VS 2019 ${{ matrix.config.arch }} ${{ matrix.sdl.label }}
runs-on: windows-2019
env:
VCPKG_VERSION: 8eb57355a4ffb410a2e94c07b4dca2dffbee8e50
# 2023.10.19
vcpkg_packages: zlib libpng libjpeg-turbo
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
sdl:
-
use: FALSE
label: '(no SDL)'
vcpkg_packages: opengl-registry
-
use: TRUE
label: '(with SDL)'
vcpkg_packages: sdl2
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore from cache and run vcpkg
uses: lukka/run-vcpkg@v7
with:
vcpkgArguments: ${{env.vcpkg_packages}} ${{matrix.sdl.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}} `
-DUSE_SDL2=${{matrix.sdl.use}} `
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" `
-DCMAKE_BUILD_TYPE=Release .
- name: Build
run: cmake --build . --config Release
- name: Create artifact folder
run: |
mkdir artifact/
mkdir artifact/lib/
- name: Move dlls into artifact folder
run: move bin\Win32-VisualStudio\Release\* artifact\lib\
- name: Move includes into artifact folder
run: move include artifact/
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: msvc-${{ matrix.config.arch }}-${{matrix.sdl.use}}
path: artifact/
android:
name: Android ${{ matrix.arch }}
runs-on: ubuntu-20.04
env:
ndk_version: "r25c"
ANDROID_NDK: ${{ github.workspace }}/android-ndk
strategy:
matrix:
arch: [armeabi-v7a, arm64-v8a, x86, x86_64]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install deps
run: |
sudo rm /var/lib/man-db/auto-update
sudo apt-get update
sudo apt-get install -qyy wget unzip zip gcc-multilib make cmake
- name: Cache NDK
id: cache-ndk
uses: actions/cache@v4
with:
key: android-ndk-${{ env.ndk_version }}-linux
path: ${{ env.ANDROID_NDK }}
- name: Install NDK
run: |
wget --progress=bar:force "http://dl.google.com/android/repository/android-ndk-${ndk_version}-linux.zip"
unzip -q "android-ndk-${ndk_version}-linux.zip"
rm "android-ndk-${ndk_version}-linux.zip"
mv "android-ndk-${ndk_version}" "${ANDROID_NDK}"
if: ${{ steps.cache-ndk.outputs.cache-hit != 'true' }}
- name: Build
run: ./scripts/ci-build-android.sh ${{ matrix.arch }}
#- name: Upload Artifact
# uses: actions/upload-artifact@v4
# with:
# name: irrlicht-android-${{ matrix.arch }}
# path: ${{ runner.temp }}/pkg/${{ matrix.arch }}

25
irr/.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
CMakeFiles
CMakeCache.txt
cmake_install.cmake
install_manifest.txt
IrrlichtMtConfig.cmake
IrrlichtMtConfigVersion.cmake
IrrlichtMtTargets.cmake
CTestTestfile.cmake
Makefile
libs/*
*.so*
*.a
*.exe
*.dll
bin/Linux
scripts/gl2ext.h
scripts/glcorearb.h
scripts/glext.h
*.vcxproj*
*.dir/
*.sln
*visualstudio/
# vscode cmake plugin
build/*

24
irr/CMakeLists.txt Normal file
View File

@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.12)
project(Irrlicht LANGUAGES CXX)
message(STATUS "*** Building IrrlichtMt ***")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type: Debug or Release" FORCE)
endif()
# FIXME: tests need to be moved to MT if we want to keep them
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
#enable_testing()
add_subdirectory(src)
#add_subdirectory(test)
#option(BUILD_EXAMPLES "Build example applications" FALSE)
#if(BUILD_EXAMPLES)
# add_subdirectory(examples)
#endif()

26
irr/LICENSE Normal file
View File

@ -0,0 +1,26 @@
Copyright (C) 2002-2012 Nikolaus Gebhardt
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Please note that the Irrlicht Engine is based in part on the work of the
Independent JPEG Group, the zlib, libPng and aesGladman. This means that if you use
the Irrlicht Engine in your product, you must acknowledge somewhere in your
documentation that you've used the IJPG code. It would also be nice to mention
that you use the Irrlicht Engine, the zlib, libPng and aesGladman. See the
corresponding license files for further informations. It is also possible to disable
usage of those additional libraries by defines in the IrrCompileConfig.h header and
recompiling the engine.

64
irr/README.md Normal file
View File

@ -0,0 +1,64 @@
IrrlichtMt version 1.9
======================
IrrlichtMt is the 3D engine of [Minetest](https://github.com/minetest).
It is based on the [Irrlicht Engine](https://irrlicht.sourceforge.io/) but is now developed independently.
It is intentionally not compatible to upstream and is planned to be eventually absorbed into Minetest.
Build
-----
The build system is CMake.
The following libraries are required to be installed:
* zlib, libPNG, libJPEG
* OpenGL
* or on mobile: OpenGL ES (can be optionally enabled on desktop too)
* on Unix: X11
* SDL2 (see below)
Aside from standard search options (`ZLIB_INCLUDE_DIR`, `ZLIB_LIBRARY`, ...) the following options are available:
* `ENABLE_OPENGL` - Enable OpenGL driver
* `ENABLE_OPENGL3` (default: `OFF`) - Enable OpenGL 3+ driver
* `ENABLE_GLES1` - Enable OpenGL ES driver, legacy
* `ENABLE_GLES2` - Enable OpenGL ES 2+ driver
* `USE_SDL2` (default: platform-dependent, usually `ON`) - Use SDL2 instead of older native device code
However, IrrlichtMt cannot be built or installed separately.
Platforms
---------
We aim to support these platforms:
* Windows via MinGW
* Linux (GL or GLES)
* macOS
* Android
This doesn't mean other platforms don't work or won't be supported, if you find something that doesn't work contributions are welcome.
License
-------
The license of the Irrlicht Engine is based on the zlib/libpng license and applies to this fork, too.
The Irrlicht Engine License
===========================
Copyright (C) 2002-2012 Nikolaus Gebhardt
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgement in the product documentation would be
appreciated but is not required.
2. Altered source versions must be clearly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

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