mirror of https://github.com/minetest/minetest.git
Merge branch 'master' into doc-refactor-2
This commit is contained in:
commit
408bb3e60f
|
@ -28,43 +28,38 @@ on:
|
|||
- '.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,6 +92,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# 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
|
||||
with:
|
||||
|
|
|
@ -94,10 +94,7 @@ endif()
|
|||
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(irr EXCLUDE_FROM_ALL)
|
||||
unset(BUILD_SHARED_LIBS CACHE)
|
||||
|
||||
if(NOT TARGET IrrlichtMt)
|
||||
message(FATAL_ERROR "IrrlichtMt project is missing a CMake target?!")
|
||||
|
|
|
@ -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)) {
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -0,0 +1,542 @@
|
|||
--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\" (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
|
|
@ -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
|
@ -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
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -93,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()
|
||||
|
@ -126,5 +126,3 @@ function create_reinstall_mtg_dlg()
|
|||
buttonhandler, eventhandler)
|
||||
return dlg
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -153,8 +153,6 @@ 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
|
||||
|
||||
# Touchscreen sensitivity multiplier.
|
||||
|
@ -1895,6 +1893,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.
|
||||
|
|
|
@ -25,3 +25,5 @@ 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)
|
||||
|
|
|
@ -37,7 +37,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:
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ Notable pages:
|
|||
|
||||
* [Developing minetestserver with Docker](docker.md)
|
||||
* [Android Tips & Tricks](android.md)
|
||||
* [OS/library Compatability Policy](os-compatibility.md)
|
||||
* [Miscellaneous](misc.md)
|
||||
|
||||
## IRC
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/
|
|
@ -1698,6 +1698,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.
|
||||
|
@ -5917,8 +5919,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.
|
||||
|
@ -5929,13 +5932,14 @@ handler.
|
|||
## 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
|
||||
|
@ -5945,16 +5949,17 @@ handler.
|
|||
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
|
||||
|
@ -6009,20 +6014,21 @@ handler.
|
|||
* 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
|
||||
|
|
|
@ -64,7 +64,7 @@ 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.
|
||||
|
||||
|
|
|
@ -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"},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)"),
|
||||
|
|
|
@ -1,82 +1,24 @@
|
|||
cmake_minimum_required(VERSION 3.12)
|
||||
|
||||
set(IRRLICHTMT_REVISION 15)
|
||||
project(Irrlicht LANGUAGES CXX)
|
||||
|
||||
project(Irrlicht
|
||||
VERSION 1.9.0.${IRRLICHTMT_REVISION}
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
message(STATUS "*** Building IrrlichtMt ${PROJECT_VERSION} ***")
|
||||
message(STATUS "*** Building IrrlichtMt ***")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
if(ANDROID)
|
||||
set(sysname Android)
|
||||
elseif(APPLE)
|
||||
set(sysname OSX)
|
||||
elseif(MSVC)
|
||||
set(sysname Win32-VisualStudio)
|
||||
elseif(WIN32)
|
||||
set(sysname Win32-gcc)
|
||||
else()
|
||||
set(sysname Linux)
|
||||
endif()
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib/${sysname})
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin/${sysname})
|
||||
|
||||
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()
|
||||
#enable_testing()
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(test)
|
||||
#add_subdirectory(test)
|
||||
|
||||
option(BUILD_EXAMPLES "Build example applications" FALSE)
|
||||
if(BUILD_EXAMPLES)
|
||||
add_subdirectory(examples)
|
||||
endif()
|
||||
|
||||
# Export a file that describes the targets that IrrlichtMt creates.
|
||||
# The file is placed in the location FILE points to, where CMake can easily
|
||||
# locate it by pointing CMAKE_PREFIX_PATH to this project root.
|
||||
export(EXPORT IrrlichtMt-export
|
||||
FILE "${CMAKE_CURRENT_BINARY_DIR}/cmake/IrrlichtMtTargets.cmake"
|
||||
NAMESPACE IrrlichtMt::
|
||||
)
|
||||
|
||||
# Installation of headers.
|
||||
install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/"
|
||||
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/irrlichtmt"
|
||||
)
|
||||
|
||||
# Installation of CMake target and configuration files.
|
||||
install(EXPORT IrrlichtMt-export
|
||||
FILE IrrlichtMtTargets.cmake
|
||||
NAMESPACE IrrlichtMt::
|
||||
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/IrrlichtMt"
|
||||
)
|
||||
|
||||
include(CMakePackageConfigHelpers)
|
||||
configure_package_config_file("${PROJECT_SOURCE_DIR}/Config.cmake.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/cmake/IrrlichtMtConfig.cmake"
|
||||
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/IrrlichtMt"
|
||||
NO_SET_AND_CHECK_MACRO
|
||||
NO_CHECK_REQUIRED_COMPONENTS_MACRO
|
||||
)
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/cmake/IrrlichtMtConfigVersion.cmake"
|
||||
COMPATIBILITY AnyNewerVersion
|
||||
)
|
||||
|
||||
install(FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/cmake/IrrlichtMtConfig.cmake"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/cmake/IrrlichtMtConfigVersion.cmake"
|
||||
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/IrrlichtMt"
|
||||
)
|
||||
#option(BUILD_EXAMPLES "Build example applications" FALSE)
|
||||
#if(BUILD_EXAMPLES)
|
||||
# add_subdirectory(examples)
|
||||
#endif()
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
|
||||
if(NOT TARGET IrrlichtMt::IrrlichtMt)
|
||||
# private dependency only explicitly needed with static libs
|
||||
if(@USE_SDL2@ AND NOT @BUILD_SHARED_LIBS@)
|
||||
find_dependency(SDL2)
|
||||
endif()
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/IrrlichtMtTargets.cmake")
|
||||
endif()
|
|
@ -18,37 +18,13 @@ The following libraries are required to be installed:
|
|||
* SDL2 (see below)
|
||||
|
||||
Aside from standard search options (`ZLIB_INCLUDE_DIR`, `ZLIB_LIBRARY`, ...) the following options are available:
|
||||
* `BUILD_SHARED_LIBS` (default: `ON`) - Build IrrlichtMt as a shared library
|
||||
* `BUILD_EXAMPLES` (default: `OFF`) - Build example applications
|
||||
* `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
|
||||
|
||||
e.g. on a Linux system you might want to build for local use like this:
|
||||
|
||||
git clone https://github.com/minetest/irrlicht
|
||||
cd irrlicht
|
||||
cmake . -DBUILD_SHARED_LIBS=OFF
|
||||
make -j$(nproc)
|
||||
|
||||
This will put an IrrlichtMtTargets.cmake file into the cmake directory in the current build directory, and it can then be imported from another project by pointing `find_package()` to the build directory, or by setting the `CMAKE_PREFIX_PATH` variable to that same path.
|
||||
|
||||
on Windows system:
|
||||
|
||||
It is highly recommended to use vcpkg as package manager.
|
||||
|
||||
After you successfully built vcpkg you can easily install the required libraries:
|
||||
|
||||
vcpkg install zlib libjpeg-turbo libpng sdl2 --triplet x64-windows
|
||||
|
||||
Run the following script in PowerShell:
|
||||
|
||||
git clone https://github.com/minetest/irrlicht
|
||||
cd irrlicht
|
||||
cmake -B build -G "Visual Studio 17 2022" -A "Win64" -DCMAKE_TOOLCHAIN_FILE=[vcpkg-root]/scripts/buildsystems/vcpkg.cmake -DBUILD_SHARED_LIBS=OFF
|
||||
cmake --build build --config Release
|
||||
However, IrrlichtMt cannot be built or installed separately.
|
||||
|
||||
Platforms
|
||||
---------
|
||||
|
|
|
@ -79,9 +79,6 @@ enum EEVENT_TYPE
|
|||
*/
|
||||
EET_USER_EVENT,
|
||||
|
||||
//! Pass on raw events from the OS
|
||||
EET_SYSTEM_EVENT,
|
||||
|
||||
//! Application state events like a resume, pause etc.
|
||||
EET_APPLICATION_EVENT,
|
||||
|
||||
|
@ -187,17 +184,6 @@ enum ETOUCH_INPUT_EVENT
|
|||
ETIE_COUNT
|
||||
};
|
||||
|
||||
enum ESYSTEM_EVENT_TYPE
|
||||
{
|
||||
//! From Android command handler for native activity messages
|
||||
ESET_ANDROID_CMD = 0,
|
||||
|
||||
// TODO: for example ESET_WINDOWS_MESSAGE for win32 message loop events
|
||||
|
||||
//! No real event, but to get number of event types
|
||||
ESET_COUNT
|
||||
};
|
||||
|
||||
//! Enumeration for a commonly used application state events (it's useful mainly for mobile devices)
|
||||
enum EAPPLICATION_EVENT_TYPE
|
||||
{
|
||||
|
@ -528,25 +514,6 @@ struct SEvent
|
|||
size_t UserData2;
|
||||
};
|
||||
|
||||
// Raw events from the OS
|
||||
struct SSystemEvent
|
||||
{
|
||||
//! Android command handler native activity messages.
|
||||
struct SAndroidCmd
|
||||
{
|
||||
//! APP_CMD_ enums defined in android_native_app_glue.h from the Android NDK
|
||||
s32 Cmd;
|
||||
};
|
||||
|
||||
// TOOD: more structs for iphone, Windows, X11, etc.
|
||||
|
||||
ESYSTEM_EVENT_TYPE EventType;
|
||||
union
|
||||
{
|
||||
struct SAndroidCmd AndroidCmd;
|
||||
};
|
||||
};
|
||||
|
||||
// Application state event
|
||||
struct SApplicationEvent
|
||||
{
|
||||
|
@ -567,7 +534,6 @@ struct SEvent
|
|||
struct SJoystickEvent JoystickEvent;
|
||||
struct SLogEvent LogEvent;
|
||||
struct SUserEvent UserEvent;
|
||||
struct SSystemEvent SystemEvent;
|
||||
struct SApplicationEvent ApplicationEvent;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,12 +4,8 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#define IRRCALLCONV __stdcall
|
||||
#else
|
||||
#define IRRCALLCONV
|
||||
#endif
|
||||
// these are obsolete and never pre-defined
|
||||
|
||||
#define IRRCALLCONV
|
||||
|
||||
#ifndef IRRLICHT_API
|
||||
#define IRRLICHT_API
|
||||
#endif
|
||||
|
|
|
@ -180,10 +180,7 @@ public:
|
|||
virtual bool isFullscreen() const = 0;
|
||||
|
||||
//! Checks if the window could possibly be visible.
|
||||
//! Currently, this only returns false when the activity is stopped on
|
||||
//! Android. Note that for Android activities, "stopped" means something
|
||||
//! different than you might expect (and also something different than
|
||||
//! "paused"). Read the Android lifecycle documentation.
|
||||
/** If this returns false, you should not do any rendering. */
|
||||
virtual bool isWindowVisible() const { return true; };
|
||||
|
||||
//! Get the current color format of the window
|
||||
|
|
|
@ -45,10 +45,11 @@ struct SIrrlichtCreationParameters
|
|||
#endif
|
||||
PrivateData(0),
|
||||
#ifdef IRR_MOBILE_PATHS
|
||||
OGLES2ShaderPath("media/Shaders/")
|
||||
OGLES2ShaderPath("media/Shaders/"),
|
||||
#else
|
||||
OGLES2ShaderPath("../../media/Shaders/")
|
||||
OGLES2ShaderPath("../../media/Shaders/"),
|
||||
#endif
|
||||
DriverDebug(false)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -224,6 +225,9 @@ struct SIrrlichtCreationParameters
|
|||
/** This is about the shaders which can be found in media/Shaders by default. It's only necessary
|
||||
to set when using OGL-ES 2.0 */
|
||||
irr::io::path OGLES2ShaderPath;
|
||||
|
||||
//! Enable debug and error checks in video driver.
|
||||
bool DriverDebug;
|
||||
};
|
||||
|
||||
} // end namespace irr
|
||||
|
|
|
@ -6,13 +6,18 @@
|
|||
|
||||
#include "irrTypes.h"
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
|
||||
/* HACK: import these string methods from MT's util/string.h */
|
||||
extern std::wstring utf8_to_wide(std::string_view input);
|
||||
extern std::string wide_to_utf8(std::wstring_view input);
|
||||
/* */
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace core
|
||||
|
@ -905,8 +910,7 @@ inline size_t multibyteToWString(stringw &destination, const core::stringc &sour
|
|||
|
||||
inline size_t utf8ToWString(stringw &destination, const char *source)
|
||||
{
|
||||
std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
|
||||
destination = conv.from_bytes(source);
|
||||
destination = utf8_to_wide(source);
|
||||
return destination.size();
|
||||
}
|
||||
|
||||
|
@ -917,8 +921,7 @@ inline size_t utf8ToWString(stringw &destination, const stringc &source)
|
|||
|
||||
inline size_t wStringToUTF8(stringc &destination, const wchar_t *source)
|
||||
{
|
||||
std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
|
||||
destination = conv.to_bytes(source);
|
||||
destination = wide_to_utf8(source);
|
||||
return destination.size();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
// Copyright (C) 2002-2011 Nikolaus Gebhardt
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#include "CAndroidAssetReader.h"
|
||||
|
||||
#include "CReadFile.h"
|
||||
#include "coreutil.h"
|
||||
#include "CAndroidAssetFileArchive.h"
|
||||
#include "CIrrDeviceAndroid.h"
|
||||
#include "os.h" // for logging (just keep it in even when not needed right now as it's used all the time)
|
||||
|
||||
#include <android_native_app_glue.h>
|
||||
#include <android/native_activity.h>
|
||||
#include <android/log.h>
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace io
|
||||
{
|
||||
|
||||
CAndroidAssetFileArchive::CAndroidAssetFileArchive(AAssetManager *assetManager, bool ignoreCase, bool ignorePaths) :
|
||||
CFileList("/asset", ignoreCase, ignorePaths), AssetManager(assetManager)
|
||||
{
|
||||
}
|
||||
|
||||
CAndroidAssetFileArchive::~CAndroidAssetFileArchive()
|
||||
{
|
||||
}
|
||||
|
||||
//! get the archive type
|
||||
E_FILE_ARCHIVE_TYPE CAndroidAssetFileArchive::getType() const
|
||||
{
|
||||
return EFAT_ANDROID_ASSET;
|
||||
}
|
||||
|
||||
const IFileList *CAndroidAssetFileArchive::getFileList() const
|
||||
{
|
||||
// The assert_manager can not read directory names, so
|
||||
// getFileList returns only files in folders which have been added.
|
||||
return this;
|
||||
}
|
||||
|
||||
//! opens a file by file name
|
||||
IReadFile *CAndroidAssetFileArchive::createAndOpenFile(const io::path &filename)
|
||||
{
|
||||
CAndroidAssetReader *reader = new CAndroidAssetReader(AssetManager, filename);
|
||||
|
||||
if (reader->isOpen())
|
||||
return reader;
|
||||
|
||||
reader->drop();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//! opens a file by index
|
||||
IReadFile *CAndroidAssetFileArchive::createAndOpenFile(u32 index)
|
||||
{
|
||||
const io::path &filename(getFullFileName(index));
|
||||
if (filename.empty())
|
||||
return 0;
|
||||
|
||||
return createAndOpenFile(filename);
|
||||
}
|
||||
|
||||
void CAndroidAssetFileArchive::addDirectoryToFileList(const io::path &dirname_)
|
||||
{
|
||||
io::path dirname(dirname_);
|
||||
fschar_t lastChar = dirname.lastChar();
|
||||
if (lastChar == '/' || lastChar == '\\')
|
||||
dirname.erase(dirname.size() - 1);
|
||||
|
||||
// os::Printer::log("addDirectoryToFileList:", dirname.c_str(), ELL_DEBUG);
|
||||
if (findFile(dirname, true) >= 0)
|
||||
return; // was already added
|
||||
|
||||
AAssetDir *dir = AAssetManager_openDir(AssetManager, core::stringc(dirname).c_str());
|
||||
if (!dir)
|
||||
return;
|
||||
|
||||
// add directory itself
|
||||
addItem(dirname, 0, 0, /*isDir*/ true, getFileCount());
|
||||
|
||||
// add all files in folder.
|
||||
// Note: AAssetDir_getNextFileName does not return directory names (neither does any other NDK function)
|
||||
while (const char *filename = AAssetDir_getNextFileName(dir)) {
|
||||
core::stringc full_filename = dirname == "" ? filename
|
||||
: dirname + "/" + filename;
|
||||
|
||||
// We can't get the size without opening the file - so for performance
|
||||
// reasons we set the file size to 0.
|
||||
// TODO: Does this really cost so much performance that it's worth losing this information? Dirs are usually just added once at startup...
|
||||
addItem(full_filename, /*offet*/ 0, /*size*/ 0, /*isDir*/ false, getFileCount());
|
||||
// os::Printer::log("addItem:", full_filename.c_str(), ELL_DEBUG);
|
||||
}
|
||||
AAssetDir_close(dir);
|
||||
}
|
||||
|
||||
} // end namespace io
|
||||
} // end namespace irr
|
|
@ -1,58 +0,0 @@
|
|||
// Copyright (C) 2012 Joerg Henrichs
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IReadFile.h"
|
||||
#include "IFileArchive.h"
|
||||
#include "CFileList.h"
|
||||
|
||||
#include <android/native_activity.h>
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace io
|
||||
{
|
||||
|
||||
/*!
|
||||
Android asset file system written August 2012 by J.Henrichs (later reworked by others).
|
||||
*/
|
||||
class CAndroidAssetFileArchive : public virtual IFileArchive,
|
||||
virtual CFileList
|
||||
{
|
||||
public:
|
||||
//! constructor
|
||||
CAndroidAssetFileArchive(AAssetManager *assetManager, bool ignoreCase, bool ignorePaths);
|
||||
|
||||
//! destructor
|
||||
virtual ~CAndroidAssetFileArchive();
|
||||
|
||||
//! opens a file by file name
|
||||
virtual IReadFile *createAndOpenFile(const io::path &filename);
|
||||
|
||||
//! opens a file by index
|
||||
virtual IReadFile *createAndOpenFile(u32 index);
|
||||
|
||||
//! returns the list of files
|
||||
virtual const IFileList *getFileList() const;
|
||||
|
||||
//! get the archive type
|
||||
virtual E_FILE_ARCHIVE_TYPE getType() const;
|
||||
|
||||
//! Add a directory to read files from. Since the Android
|
||||
//! API does not return names of directories, they need to
|
||||
//! be added manually.
|
||||
virtual void addDirectoryToFileList(const io::path &filename);
|
||||
|
||||
//! return the name (id) of the file Archive
|
||||
const io::path &getArchiveName() const override { return Path; }
|
||||
|
||||
protected:
|
||||
//! Android's asset manager
|
||||
AAssetManager *AssetManager;
|
||||
|
||||
}; // CAndroidAssetFileArchive
|
||||
|
||||
} // end namespace io
|
||||
} // end namespace irr
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright (C) 2002-2011 Nikolaus Gebhardt
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#include "CAndroidAssetReader.h"
|
||||
|
||||
#include "CReadFile.h"
|
||||
#include "coreutil.h"
|
||||
#include "CAndroidAssetReader.h"
|
||||
#include "CIrrDeviceAndroid.h"
|
||||
|
||||
#include <android_native_app_glue.h>
|
||||
#include <android/native_activity.h>
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace io
|
||||
{
|
||||
|
||||
CAndroidAssetReader::CAndroidAssetReader(AAssetManager *assetManager, const io::path &filename) :
|
||||
AssetManager(assetManager), Filename(filename)
|
||||
{
|
||||
Asset = AAssetManager_open(AssetManager,
|
||||
core::stringc(filename).c_str(),
|
||||
AASSET_MODE_RANDOM);
|
||||
}
|
||||
|
||||
CAndroidAssetReader::~CAndroidAssetReader()
|
||||
{
|
||||
if (Asset)
|
||||
AAsset_close(Asset);
|
||||
}
|
||||
|
||||
size_t CAndroidAssetReader::read(void *buffer, size_t sizeToRead)
|
||||
{
|
||||
int readBytes = AAsset_read(Asset, buffer, sizeToRead);
|
||||
if (readBytes >= 0)
|
||||
return size_t(readBytes);
|
||||
return 0; // direct fd access is not possible (for example, if the asset is compressed).
|
||||
}
|
||||
|
||||
bool CAndroidAssetReader::seek(long finalPos, bool relativeMovement)
|
||||
{
|
||||
off_t status = AAsset_seek(Asset, finalPos, relativeMovement ? SEEK_CUR : SEEK_SET);
|
||||
|
||||
return status + 1;
|
||||
}
|
||||
|
||||
long CAndroidAssetReader::getSize() const
|
||||
{
|
||||
return AAsset_getLength(Asset);
|
||||
}
|
||||
|
||||
long CAndroidAssetReader::getPos() const
|
||||
{
|
||||
return AAsset_getLength(Asset) - AAsset_getRemainingLength(Asset);
|
||||
}
|
||||
|
||||
const io::path &CAndroidAssetReader::getFileName() const
|
||||
{
|
||||
return Filename;
|
||||
}
|
||||
|
||||
} // end namespace io
|
||||
} // end namespace irr
|
|
@ -1,64 +0,0 @@
|
|||
// Copyright (C) 2012 Joerg Henrichs
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IReadFile.h"
|
||||
|
||||
struct AAssetManager;
|
||||
struct AAsset;
|
||||
struct ANativeActivity;
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace io
|
||||
{
|
||||
|
||||
class CAndroidAssetReader : public virtual IReadFile
|
||||
{
|
||||
public:
|
||||
CAndroidAssetReader(AAssetManager *assetManager, const io::path &filename);
|
||||
|
||||
virtual ~CAndroidAssetReader();
|
||||
|
||||
//! Reads an amount of bytes from the file.
|
||||
/** \param buffer Pointer to buffer where read bytes are written to.
|
||||
\param sizeToRead Amount of bytes to read from the file.
|
||||
\return How many bytes were read. */
|
||||
virtual size_t read(void *buffer, size_t sizeToRead);
|
||||
|
||||
//! Changes position in file
|
||||
/** \param finalPos Destination position in the file.
|
||||
\param relativeMovement If set to true, the position in the file is
|
||||
changed relative to current position. Otherwise the position is changed
|
||||
from beginning of file.
|
||||
\return True if successful, otherwise false. */
|
||||
virtual bool seek(long finalPos, bool relativeMovement = false);
|
||||
|
||||
//! Get size of file.
|
||||
/** \return Size of the file in bytes. */
|
||||
virtual long getSize() const;
|
||||
|
||||
//! Get the current position in the file.
|
||||
/** \return Current position in the file in bytes. */
|
||||
virtual long getPos() const;
|
||||
|
||||
//! Get name of file.
|
||||
/** \return File name as zero terminated character string. */
|
||||
virtual const io::path &getFileName() const;
|
||||
|
||||
/** Return true if the file could be opened. */
|
||||
bool isOpen() const { return Asset != NULL; }
|
||||
|
||||
private:
|
||||
//! Android's asset manager
|
||||
AAssetManager *AssetManager;
|
||||
|
||||
// An asset, i.e. file
|
||||
AAsset *Asset;
|
||||
path Filename;
|
||||
};
|
||||
|
||||
} // end namespace io
|
||||
} // end namespace irr
|
|
@ -1,828 +0,0 @@
|
|||
// Copyright (C) 2002-2007 Nikolaus Gebhardt
|
||||
// Copyright (C) 2007-2011 Christian Stehno
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#include "CIrrDeviceAndroid.h"
|
||||
|
||||
#include "os.h"
|
||||
#include "CFileSystem.h"
|
||||
#include "CAndroidAssetReader.h"
|
||||
#include "CAndroidAssetFileArchive.h"
|
||||
#include "CKeyEventWrapper.h"
|
||||
#include "CEGLManager.h"
|
||||
#include "ISceneManager.h"
|
||||
#include "IGUIEnvironment.h"
|
||||
#include "CEGLManager.h"
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace video
|
||||
{
|
||||
IVideoDriver *createOGLES1Driver(const SIrrlichtCreationParameters ¶ms,
|
||||
io::IFileSystem *io, video::IContextManager *contextManager);
|
||||
|
||||
IVideoDriver *createOGLES2Driver(const SIrrlichtCreationParameters ¶ms,
|
||||
io::IFileSystem *io, video::IContextManager *contextManager);
|
||||
}
|
||||
}
|
||||
|
||||
namespace irr
|
||||
{
|
||||
|
||||
CIrrDeviceAndroid::CIrrDeviceAndroid(const SIrrlichtCreationParameters ¶m) :
|
||||
CIrrDeviceStub(param), Accelerometer(0), Gyroscope(0), Initialized(false),
|
||||
Stopped(true), Paused(true), Focused(false), JNIEnvAttachedToVM(0)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
setDebugName("CIrrDeviceAndroid");
|
||||
#endif
|
||||
|
||||
// Get the interface to the native Android activity.
|
||||
Android = (android_app *)(param.PrivateData);
|
||||
|
||||
// Set the private data so we can use it in any static callbacks.
|
||||
Android->userData = this;
|
||||
|
||||
// Set the default command handler. This is a callback function that the Android
|
||||
// OS invokes to send the native activity messages.
|
||||
Android->onAppCmd = handleAndroidCommand;
|
||||
|
||||
createKeyMap();
|
||||
|
||||
// Create a sensor manager to receive touch screen events from the java activity.
|
||||
SensorManager = ASensorManager_getInstance();
|
||||
SensorEventQueue = ASensorManager_createEventQueue(SensorManager, Android->looper, LOOPER_ID_USER, 0, 0);
|
||||
Android->onInputEvent = handleInput;
|
||||
|
||||
// Create EGL manager.
|
||||
ContextManager = new video::CEGLManager();
|
||||
|
||||
os::Printer::log("Waiting for Android activity window to be created.", ELL_DEBUG);
|
||||
|
||||
do {
|
||||
s32 Events = 0;
|
||||
android_poll_source *Source = 0;
|
||||
|
||||
while ((ALooper_pollAll((!Initialized || isWindowActive()) ? 0 : -1, 0, &Events, (void **)&Source)) >= 0) {
|
||||
if (Source)
|
||||
Source->process(Android, Source);
|
||||
}
|
||||
} while (!Initialized);
|
||||
}
|
||||
|
||||
CIrrDeviceAndroid::~CIrrDeviceAndroid()
|
||||
{
|
||||
if (GUIEnvironment) {
|
||||
GUIEnvironment->drop();
|
||||
GUIEnvironment = 0;
|
||||
}
|
||||
|
||||
if (SceneManager) {
|
||||
SceneManager->drop();
|
||||
SceneManager = 0;
|
||||
}
|
||||
|
||||
if (VideoDriver) {
|
||||
VideoDriver->drop();
|
||||
VideoDriver = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::run()
|
||||
{
|
||||
if (!Initialized)
|
||||
return false;
|
||||
|
||||
os::Timer::tick();
|
||||
|
||||
s32 id;
|
||||
s32 Events = 0;
|
||||
android_poll_source *Source = 0;
|
||||
|
||||
while ((id = ALooper_pollAll(0, 0, &Events, (void **)&Source)) >= 0) {
|
||||
if (Source)
|
||||
Source->process(Android, Source);
|
||||
|
||||
// if a sensor has data, we'll process it now.
|
||||
if (id == LOOPER_ID_USER) {
|
||||
ASensorEvent sensorEvent;
|
||||
while (ASensorEventQueue_getEvents(SensorEventQueue, &sensorEvent, 1) > 0) {
|
||||
switch (sensorEvent.type) {
|
||||
case ASENSOR_TYPE_ACCELEROMETER:
|
||||
SEvent accEvent;
|
||||
accEvent.EventType = EET_ACCELEROMETER_EVENT;
|
||||
accEvent.AccelerometerEvent.X = sensorEvent.acceleration.x;
|
||||
accEvent.AccelerometerEvent.Y = sensorEvent.acceleration.y;
|
||||
accEvent.AccelerometerEvent.Z = sensorEvent.acceleration.z;
|
||||
|
||||
postEventFromUser(accEvent);
|
||||
break;
|
||||
|
||||
case ASENSOR_TYPE_GYROSCOPE:
|
||||
SEvent gyroEvent;
|
||||
gyroEvent.EventType = EET_GYROSCOPE_EVENT;
|
||||
gyroEvent.GyroscopeEvent.X = sensorEvent.vector.x;
|
||||
gyroEvent.GyroscopeEvent.Y = sensorEvent.vector.y;
|
||||
gyroEvent.GyroscopeEvent.Z = sensorEvent.vector.z;
|
||||
|
||||
postEventFromUser(gyroEvent);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Initialized)
|
||||
break;
|
||||
}
|
||||
|
||||
return Initialized;
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::yield()
|
||||
{
|
||||
struct timespec ts = {0, 1};
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::sleep(u32 timeMs, bool pauseTimer)
|
||||
{
|
||||
const bool wasStopped = Timer ? Timer->isStopped() : true;
|
||||
|
||||
struct timespec ts;
|
||||
ts.tv_sec = (time_t)(timeMs / 1000);
|
||||
ts.tv_nsec = (long)(timeMs % 1000) * 1000000;
|
||||
|
||||
if (pauseTimer && !wasStopped)
|
||||
Timer->stop();
|
||||
|
||||
nanosleep(&ts, NULL);
|
||||
|
||||
if (pauseTimer && !wasStopped)
|
||||
Timer->start();
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::setWindowCaption(const wchar_t *text)
|
||||
{
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isWindowActive() const
|
||||
{
|
||||
return (Focused && !Paused && !Stopped);
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isWindowFocused() const
|
||||
{
|
||||
return Focused;
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isWindowMinimized() const
|
||||
{
|
||||
return !Focused;
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isWindowVisible() const
|
||||
{
|
||||
return !Stopped;
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::closeDevice()
|
||||
{
|
||||
ANativeActivity_finish(Android->activity);
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::setResizable(bool resize)
|
||||
{
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::minimizeWindow()
|
||||
{
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::maximizeWindow()
|
||||
{
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::restoreWindow()
|
||||
{
|
||||
}
|
||||
|
||||
core::position2di CIrrDeviceAndroid::getWindowPosition()
|
||||
{
|
||||
return core::position2di(0, 0);
|
||||
}
|
||||
|
||||
E_DEVICE_TYPE CIrrDeviceAndroid::getType() const
|
||||
{
|
||||
return EIDT_ANDROID;
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::handleAndroidCommand(android_app *app, int32_t cmd)
|
||||
{
|
||||
CIrrDeviceAndroid *device = (CIrrDeviceAndroid *)app->userData;
|
||||
|
||||
SEvent event;
|
||||
event.EventType = EET_SYSTEM_EVENT;
|
||||
event.SystemEvent.EventType = ESET_ANDROID_CMD;
|
||||
event.SystemEvent.AndroidCmd.Cmd = cmd;
|
||||
if (device->postEventFromUser(event))
|
||||
return;
|
||||
|
||||
switch (cmd) {
|
||||
case APP_CMD_INPUT_CHANGED:
|
||||
os::Printer::log("Android command APP_CMD_INPUT_CHANGED", ELL_DEBUG);
|
||||
break;
|
||||
case APP_CMD_WINDOW_RESIZED:
|
||||
os::Printer::log("Android command APP_CMD_WINDOW_RESIZED", ELL_DEBUG);
|
||||
break;
|
||||
case APP_CMD_WINDOW_REDRAW_NEEDED:
|
||||
os::Printer::log("Android command APP_CMD_WINDOW_REDRAW_NEEDED", ELL_DEBUG);
|
||||
break;
|
||||
case APP_CMD_SAVE_STATE:
|
||||
os::Printer::log("Android command APP_CMD_SAVE_STATE", ELL_DEBUG);
|
||||
break;
|
||||
case APP_CMD_CONTENT_RECT_CHANGED:
|
||||
os::Printer::log("Android command APP_CMD_CONTENT_RECT_CHANGED", ELL_DEBUG);
|
||||
break;
|
||||
case APP_CMD_CONFIG_CHANGED:
|
||||
os::Printer::log("Android command APP_CMD_CONFIG_CHANGED", ELL_DEBUG);
|
||||
break;
|
||||
case APP_CMD_LOW_MEMORY:
|
||||
os::Printer::log("Android command APP_CMD_LOW_MEMORY", ELL_DEBUG);
|
||||
break;
|
||||
case APP_CMD_START:
|
||||
os::Printer::log("Android command APP_CMD_START", ELL_DEBUG);
|
||||
device->Stopped = false;
|
||||
break;
|
||||
case APP_CMD_INIT_WINDOW:
|
||||
os::Printer::log("Android command APP_CMD_INIT_WINDOW", ELL_DEBUG);
|
||||
device->getExposedVideoData().OGLESAndroid.Window = app->window;
|
||||
|
||||
if (device->CreationParams.WindowSize.Width == 0 || device->CreationParams.WindowSize.Height == 0) {
|
||||
device->CreationParams.WindowSize.Width = ANativeWindow_getWidth(app->window);
|
||||
device->CreationParams.WindowSize.Height = ANativeWindow_getHeight(app->window);
|
||||
}
|
||||
|
||||
device->getContextManager()->initialize(device->CreationParams, device->ExposedVideoData);
|
||||
device->getContextManager()->generateSurface();
|
||||
device->getContextManager()->generateContext();
|
||||
device->getContextManager()->activateContext(device->getContextManager()->getContext());
|
||||
|
||||
if (!device->Initialized) {
|
||||
io::CAndroidAssetFileArchive *assets = new io::CAndroidAssetFileArchive(device->Android->activity->assetManager, false, false);
|
||||
assets->addDirectoryToFileList("");
|
||||
device->FileSystem->addFileArchive(assets);
|
||||
assets->drop();
|
||||
|
||||
device->createDriver();
|
||||
|
||||
if (device->VideoDriver)
|
||||
device->createGUIAndScene();
|
||||
}
|
||||
device->Initialized = true;
|
||||
break;
|
||||
case APP_CMD_TERM_WINDOW:
|
||||
os::Printer::log("Android command APP_CMD_TERM_WINDOW", ELL_DEBUG);
|
||||
device->getContextManager()->destroySurface();
|
||||
break;
|
||||
case APP_CMD_GAINED_FOCUS:
|
||||
os::Printer::log("Android command APP_CMD_GAINED_FOCUS", ELL_DEBUG);
|
||||
device->Focused = true;
|
||||
break;
|
||||
case APP_CMD_LOST_FOCUS:
|
||||
os::Printer::log("Android command APP_CMD_LOST_FOCUS", ELL_DEBUG);
|
||||
device->Focused = false;
|
||||
break;
|
||||
case APP_CMD_DESTROY:
|
||||
os::Printer::log("Android command APP_CMD_DESTROY", ELL_DEBUG);
|
||||
if (device->JNIEnvAttachedToVM) {
|
||||
device->JNIEnvAttachedToVM = 0;
|
||||
device->Android->activity->vm->DetachCurrentThread();
|
||||
}
|
||||
device->Initialized = false;
|
||||
break;
|
||||
case APP_CMD_PAUSE:
|
||||
os::Printer::log("Android command APP_CMD_PAUSE", ELL_DEBUG);
|
||||
device->Paused = true;
|
||||
break;
|
||||
case APP_CMD_STOP:
|
||||
os::Printer::log("Android command APP_CMD_STOP", ELL_DEBUG);
|
||||
device->Stopped = true;
|
||||
break;
|
||||
case APP_CMD_RESUME:
|
||||
os::Printer::log("Android command APP_CMD_RESUME", ELL_DEBUG);
|
||||
device->Paused = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
s32 CIrrDeviceAndroid::handleInput(android_app *app, AInputEvent *androidEvent)
|
||||
{
|
||||
CIrrDeviceAndroid *device = (CIrrDeviceAndroid *)app->userData;
|
||||
s32 status = 0;
|
||||
|
||||
switch (AInputEvent_getType(androidEvent)) {
|
||||
case AINPUT_EVENT_TYPE_MOTION: {
|
||||
SEvent event;
|
||||
event.EventType = EET_TOUCH_INPUT_EVENT;
|
||||
|
||||
s32 eventAction = AMotionEvent_getAction(androidEvent);
|
||||
s32 eventType = eventAction & AMOTION_EVENT_ACTION_MASK;
|
||||
|
||||
#if 0
|
||||
// Useful for debugging. We might have to pass some of those infos on at some point.
|
||||
// but preferably device independent (so iphone can use same irrlicht flags).
|
||||
int32_t flags = AMotionEvent_getFlags(androidEvent);
|
||||
os::Printer::log("flags: ", core::stringc(flags).c_str(), ELL_DEBUG);
|
||||
int32_t metaState = AMotionEvent_getMetaState(androidEvent);
|
||||
os::Printer::log("metaState: ", core::stringc(metaState).c_str(), ELL_DEBUG);
|
||||
int32_t edgeFlags = AMotionEvent_getEdgeFlags(androidEvent);
|
||||
os::Printer::log("edgeFlags: ", core::stringc(flags).c_str(), ELL_DEBUG);
|
||||
#endif
|
||||
|
||||
bool touchReceived = true;
|
||||
|
||||
switch (eventType) {
|
||||
case AMOTION_EVENT_ACTION_DOWN:
|
||||
case AMOTION_EVENT_ACTION_POINTER_DOWN:
|
||||
event.TouchInput.Event = ETIE_PRESSED_DOWN;
|
||||
break;
|
||||
case AMOTION_EVENT_ACTION_MOVE:
|
||||
event.TouchInput.Event = ETIE_MOVED;
|
||||
break;
|
||||
case AMOTION_EVENT_ACTION_UP:
|
||||
case AMOTION_EVENT_ACTION_POINTER_UP:
|
||||
case AMOTION_EVENT_ACTION_CANCEL:
|
||||
event.TouchInput.Event = ETIE_LEFT_UP;
|
||||
break;
|
||||
default:
|
||||
touchReceived = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (touchReceived) {
|
||||
// Process all touches for move action.
|
||||
if (event.TouchInput.Event == ETIE_MOVED) {
|
||||
s32 pointerCount = AMotionEvent_getPointerCount(androidEvent);
|
||||
|
||||
for (s32 i = 0; i < pointerCount; ++i) {
|
||||
event.TouchInput.ID = AMotionEvent_getPointerId(androidEvent, i);
|
||||
event.TouchInput.X = AMotionEvent_getX(androidEvent, i);
|
||||
event.TouchInput.Y = AMotionEvent_getY(androidEvent, i);
|
||||
event.TouchInput.touchedCount = AMotionEvent_getPointerCount(androidEvent);
|
||||
|
||||
device->postEventFromUser(event);
|
||||
}
|
||||
} else // Process one touch for other actions.
|
||||
{
|
||||
s32 pointerIndex = (eventAction & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
|
||||
|
||||
event.TouchInput.ID = AMotionEvent_getPointerId(androidEvent, pointerIndex);
|
||||
event.TouchInput.X = AMotionEvent_getX(androidEvent, pointerIndex);
|
||||
event.TouchInput.Y = AMotionEvent_getY(androidEvent, pointerIndex);
|
||||
event.TouchInput.touchedCount = AMotionEvent_getPointerCount(androidEvent);
|
||||
|
||||
device->postEventFromUser(event);
|
||||
}
|
||||
|
||||
status = 1;
|
||||
}
|
||||
} break;
|
||||
case AINPUT_EVENT_TYPE_KEY: {
|
||||
SEvent event;
|
||||
event.EventType = EET_KEY_INPUT_EVENT;
|
||||
|
||||
int32_t keyCode = AKeyEvent_getKeyCode(androidEvent);
|
||||
// os::Printer::log("keyCode: ", core::stringc(keyCode).c_str(), ELL_DEBUG);
|
||||
|
||||
int32_t keyAction = AKeyEvent_getAction(androidEvent);
|
||||
int32_t keyMetaState = AKeyEvent_getMetaState(androidEvent);
|
||||
|
||||
if (keyCode >= 0 && (u32)keyCode < device->KeyMap.size())
|
||||
event.KeyInput.Key = device->KeyMap[keyCode];
|
||||
else
|
||||
event.KeyInput.Key = KEY_UNKNOWN;
|
||||
event.KeyInput.SystemKeyCode = (u32)keyCode;
|
||||
if (keyAction == AKEY_EVENT_ACTION_DOWN)
|
||||
event.KeyInput.PressedDown = true;
|
||||
else if (keyAction == AKEY_EVENT_ACTION_UP)
|
||||
event.KeyInput.PressedDown = false;
|
||||
else if (keyAction == AKEY_EVENT_ACTION_MULTIPLE) {
|
||||
// TODO: Multiple duplicate key events have occurred in a row,
|
||||
// or a complex string is being delivered. The repeat_count
|
||||
// property of the key event contains the number of times the
|
||||
// given key code should be executed.
|
||||
// I guess this might necessary for more complicated i18n key input,
|
||||
// but don't see yet how to handle this correctly.
|
||||
}
|
||||
|
||||
/* no use for meta keys so far.
|
||||
if ( keyMetaState & AMETA_ALT_ON
|
||||
|| keyMetaState & AMETA_ALT_LEFT_ON
|
||||
|| keyMetaState & AMETA_ALT_RIGHT_ON )
|
||||
;
|
||||
// what is a sym?
|
||||
if ( keyMetaState & AMETA_SYM_ON )
|
||||
;
|
||||
*/
|
||||
if (keyMetaState & AMETA_SHIFT_ON || keyMetaState & AMETA_SHIFT_LEFT_ON || keyMetaState & AMETA_SHIFT_RIGHT_ON)
|
||||
event.KeyInput.Shift = true;
|
||||
else
|
||||
event.KeyInput.Shift = false;
|
||||
event.KeyInput.Control = false;
|
||||
|
||||
// Having memory allocations + going through JNI for each key-press is pretty bad (slow).
|
||||
// So we do it only for those keys which are likely text-characters and avoid it for all other keys.
|
||||
// So it's fast for keys like game controller input and special keys. And text keys are typically
|
||||
// only used or entering text and not for gaming on Android, so speed likely doesn't matter there too much.
|
||||
if (event.KeyInput.Key > 0) {
|
||||
// TODO:
|
||||
// Not sure why we have to attach a JNIEnv here, but it won't work when doing that in the constructor or
|
||||
// trying to use the activity->env. My best guess is that the event-handling happens in an own thread.
|
||||
// It means JNIEnvAttachedToVM will never get detached as I don't know a safe way where to do that
|
||||
// (we could attach & detach each time, but that would probably be slow)
|
||||
// Also - it has to be each time as it get's invalid when the application mode changes.
|
||||
if (device->Initialized && device->Android && device->Android->activity && device->Android->activity->vm) {
|
||||
JavaVMAttachArgs attachArgs;
|
||||
attachArgs.version = JNI_VERSION_1_6;
|
||||
attachArgs.name = 0;
|
||||
attachArgs.group = NULL;
|
||||
|
||||
// Not a big problem calling it each time - it's a no-op when the thread already is attached.
|
||||
// And we have to do that as someone else can have detached the thread in the meantime.
|
||||
jint result = device->Android->activity->vm->AttachCurrentThread(&device->JNIEnvAttachedToVM, &attachArgs);
|
||||
if (result == JNI_ERR) {
|
||||
os::Printer::log("AttachCurrentThread for the JNI environment failed.", ELL_WARNING);
|
||||
device->JNIEnvAttachedToVM = 0;
|
||||
}
|
||||
|
||||
if (device->JNIEnvAttachedToVM) {
|
||||
jni::CKeyEventWrapper *keyEventWrapper = new jni::CKeyEventWrapper(device->JNIEnvAttachedToVM, keyAction, keyCode);
|
||||
event.KeyInput.Char = keyEventWrapper->getUnicodeChar(keyMetaState);
|
||||
delete keyEventWrapper;
|
||||
}
|
||||
}
|
||||
if (event.KeyInput.Key == KEY_BACK) {
|
||||
event.KeyInput.Char = 0x08; // same key-code as on other operating systems. Otherwise we have to handle too much system specific stuff in the editbox.
|
||||
}
|
||||
// os::Printer::log("char-code: ", core::stringc((int)event.KeyInput.Char).c_str(), ELL_DEBUG);
|
||||
|
||||
} else {
|
||||
// os::Printer::log("keyCode: ", core::stringc(keyCode).c_str(), ELL_DEBUG);
|
||||
event.KeyInput.Char = 0;
|
||||
}
|
||||
|
||||
status = device->postEventFromUser(event);
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::createDriver()
|
||||
{
|
||||
switch (CreationParams.DriverType) {
|
||||
case video::EDT_OGLES1:
|
||||
#ifdef _IRR_COMPILE_WITH_OGLES1_
|
||||
VideoDriver = video::createOGLES1Driver(CreationParams, FileSystem, ContextManager);
|
||||
#else
|
||||
os::Printer::log("No OpenGL ES 1.0 support compiled in.", ELL_ERROR);
|
||||
#endif
|
||||
break;
|
||||
case video::EDT_OGLES2:
|
||||
#ifdef _IRR_COMPILE_WITH_OGLES2_
|
||||
VideoDriver = video::createOGLES2Driver(CreationParams, FileSystem, ContextManager);
|
||||
#else
|
||||
os::Printer::log("No OpenGL ES 2.0 support compiled in.", ELL_ERROR);
|
||||
#endif
|
||||
break;
|
||||
case video::EDT_NULL:
|
||||
VideoDriver = video::createNullDriver(FileSystem, CreationParams.WindowSize);
|
||||
break;
|
||||
case video::EDT_OPENGL:
|
||||
os::Printer::log("This driver is not available in Android. Try OpenGL ES 1.0 or ES 2.0.", ELL_ERROR);
|
||||
break;
|
||||
default:
|
||||
os::Printer::log("Unable to create video driver of unknown type.", ELL_ERROR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
video::SExposedVideoData &CIrrDeviceAndroid::getExposedVideoData()
|
||||
{
|
||||
return ExposedVideoData;
|
||||
}
|
||||
|
||||
void CIrrDeviceAndroid::createKeyMap()
|
||||
{
|
||||
KeyMap.set_used(223);
|
||||
|
||||
KeyMap[0] = KEY_UNKNOWN; // AKEYCODE_UNKNOWN
|
||||
KeyMap[1] = KEY_LBUTTON; // AKEYCODE_SOFT_LEFT
|
||||
KeyMap[2] = KEY_RBUTTON; // AKEYCODE_SOFT_RIGHT
|
||||
KeyMap[3] = KEY_HOME; // AKEYCODE_HOME
|
||||
KeyMap[4] = KEY_CANCEL; // AKEYCODE_BACK
|
||||
KeyMap[5] = KEY_UNKNOWN; // AKEYCODE_CALL
|
||||
KeyMap[6] = KEY_UNKNOWN; // AKEYCODE_ENDCALL
|
||||
KeyMap[7] = KEY_KEY_0; // AKEYCODE_0
|
||||
KeyMap[8] = KEY_KEY_1; // AKEYCODE_1
|
||||
KeyMap[9] = KEY_KEY_2; // AKEYCODE_2
|
||||
KeyMap[10] = KEY_KEY_3; // AKEYCODE_3
|
||||
KeyMap[11] = KEY_KEY_4; // AKEYCODE_4
|
||||
KeyMap[12] = KEY_KEY_5; // AKEYCODE_5
|
||||
KeyMap[13] = KEY_KEY_6; // AKEYCODE_6
|
||||
KeyMap[14] = KEY_KEY_7; // AKEYCODE_7
|
||||
KeyMap[15] = KEY_KEY_8; // AKEYCODE_8
|
||||
KeyMap[16] = KEY_KEY_9; // AKEYCODE_9
|
||||
KeyMap[17] = KEY_UNKNOWN; // AKEYCODE_STAR
|
||||
KeyMap[18] = KEY_UNKNOWN; // AKEYCODE_POUND
|
||||
KeyMap[19] = KEY_UP; // AKEYCODE_DPAD_UP
|
||||
KeyMap[20] = KEY_DOWN; // AKEYCODE_DPAD_DOWN
|
||||
KeyMap[21] = KEY_LEFT; // AKEYCODE_DPAD_LEFT
|
||||
KeyMap[22] = KEY_RIGHT; // AKEYCODE_DPAD_RIGHT
|
||||
KeyMap[23] = KEY_SELECT; // AKEYCODE_DPAD_CENTER
|
||||
KeyMap[24] = KEY_VOLUME_DOWN; // AKEYCODE_VOLUME_UP
|
||||
KeyMap[25] = KEY_VOLUME_UP; // AKEYCODE_VOLUME_DOWN
|
||||
KeyMap[26] = KEY_UNKNOWN; // AKEYCODE_POWER
|
||||
KeyMap[27] = KEY_UNKNOWN; // AKEYCODE_CAMERA
|
||||
KeyMap[28] = KEY_CLEAR; // AKEYCODE_CLEAR
|
||||
KeyMap[29] = KEY_KEY_A; // AKEYCODE_A
|
||||
KeyMap[30] = KEY_KEY_B; // AKEYCODE_B
|
||||
KeyMap[31] = KEY_KEY_C; // AKEYCODE_C
|
||||
KeyMap[32] = KEY_KEY_D; // AKEYCODE_D
|
||||
KeyMap[33] = KEY_KEY_E; // AKEYCODE_E
|
||||
KeyMap[34] = KEY_KEY_F; // AKEYCODE_F
|
||||
KeyMap[35] = KEY_KEY_G; // AKEYCODE_G
|
||||
KeyMap[36] = KEY_KEY_H; // AKEYCODE_H
|
||||
KeyMap[37] = KEY_KEY_I; // AKEYCODE_I
|
||||
KeyMap[38] = KEY_KEY_J; // AKEYCODE_J
|
||||
KeyMap[39] = KEY_KEY_K; // AKEYCODE_K
|
||||
KeyMap[40] = KEY_KEY_L; // AKEYCODE_L
|
||||
KeyMap[41] = KEY_KEY_M; // AKEYCODE_M
|
||||
KeyMap[42] = KEY_KEY_N; // AKEYCODE_N
|
||||
KeyMap[43] = KEY_KEY_O; // AKEYCODE_O
|
||||
KeyMap[44] = KEY_KEY_P; // AKEYCODE_P
|
||||
KeyMap[45] = KEY_KEY_Q; // AKEYCODE_Q
|
||||
KeyMap[46] = KEY_KEY_R; // AKEYCODE_R
|
||||
KeyMap[47] = KEY_KEY_S; // AKEYCODE_S
|
||||
KeyMap[48] = KEY_KEY_T; // AKEYCODE_T
|
||||
KeyMap[49] = KEY_KEY_U; // AKEYCODE_U
|
||||
KeyMap[50] = KEY_KEY_V; // AKEYCODE_V
|
||||
KeyMap[51] = KEY_KEY_W; // AKEYCODE_W
|
||||
KeyMap[52] = KEY_KEY_X; // AKEYCODE_X
|
||||
KeyMap[53] = KEY_KEY_Y; // AKEYCODE_Y
|
||||
KeyMap[54] = KEY_KEY_Z; // AKEYCODE_Z
|
||||
KeyMap[55] = KEY_COMMA; // AKEYCODE_COMMA
|
||||
KeyMap[56] = KEY_PERIOD; // AKEYCODE_PERIOD
|
||||
KeyMap[57] = KEY_MENU; // AKEYCODE_ALT_LEFT
|
||||
KeyMap[58] = KEY_MENU; // AKEYCODE_ALT_RIGHT
|
||||
KeyMap[59] = KEY_LSHIFT; // AKEYCODE_SHIFT_LEFT
|
||||
KeyMap[60] = KEY_RSHIFT; // AKEYCODE_SHIFT_RIGHT
|
||||
KeyMap[61] = KEY_TAB; // AKEYCODE_TAB
|
||||
KeyMap[62] = KEY_SPACE; // AKEYCODE_SPACE
|
||||
KeyMap[63] = KEY_UNKNOWN; // AKEYCODE_SYM
|
||||
KeyMap[64] = KEY_UNKNOWN; // AKEYCODE_EXPLORER
|
||||
KeyMap[65] = KEY_UNKNOWN; // AKEYCODE_ENVELOPE
|
||||
KeyMap[66] = KEY_RETURN; // AKEYCODE_ENTER
|
||||
KeyMap[67] = KEY_BACK; // AKEYCODE_DEL
|
||||
KeyMap[68] = KEY_OEM_3; // AKEYCODE_GRAVE
|
||||
KeyMap[69] = KEY_MINUS; // AKEYCODE_MINUS
|
||||
KeyMap[70] = KEY_UNKNOWN; // AKEYCODE_EQUALS
|
||||
KeyMap[71] = KEY_UNKNOWN; // AKEYCODE_LEFT_BRACKET
|
||||
KeyMap[72] = KEY_UNKNOWN; // AKEYCODE_RIGHT_BRACKET
|
||||
KeyMap[73] = KEY_UNKNOWN; // AKEYCODE_BACKSLASH
|
||||
KeyMap[74] = KEY_UNKNOWN; // AKEYCODE_SEMICOLON
|
||||
KeyMap[75] = KEY_UNKNOWN; // AKEYCODE_APOSTROPHE
|
||||
KeyMap[76] = KEY_UNKNOWN; // AKEYCODE_SLASH
|
||||
KeyMap[77] = KEY_UNKNOWN; // AKEYCODE_AT
|
||||
KeyMap[78] = KEY_UNKNOWN; // AKEYCODE_NUM
|
||||
KeyMap[79] = KEY_UNKNOWN; // AKEYCODE_HEADSETHOOK
|
||||
KeyMap[80] = KEY_UNKNOWN; // AKEYCODE_FOCUS (*Camera* focus)
|
||||
KeyMap[81] = KEY_PLUS; // AKEYCODE_PLUS
|
||||
KeyMap[82] = KEY_MENU; // AKEYCODE_MENU
|
||||
KeyMap[83] = KEY_UNKNOWN; // AKEYCODE_NOTIFICATION
|
||||
KeyMap[84] = KEY_UNKNOWN; // AKEYCODE_SEARCH
|
||||
KeyMap[85] = KEY_MEDIA_PLAY_PAUSE; // AKEYCODE_MEDIA_PLAY_PAUSE
|
||||
KeyMap[86] = KEY_MEDIA_STOP; // AKEYCODE_MEDIA_STOP
|
||||
KeyMap[87] = KEY_MEDIA_NEXT_TRACK; // AKEYCODE_MEDIA_NEXT
|
||||
KeyMap[88] = KEY_MEDIA_PREV_TRACK; // AKEYCODE_MEDIA_PREVIOUS
|
||||
KeyMap[89] = KEY_UNKNOWN; // AKEYCODE_MEDIA_REWIND
|
||||
KeyMap[90] = KEY_UNKNOWN; // AKEYCODE_MEDIA_FAST_FORWARD
|
||||
KeyMap[91] = KEY_VOLUME_MUTE; // AKEYCODE_MUTE
|
||||
KeyMap[92] = KEY_PRIOR; // AKEYCODE_PAGE_UP
|
||||
KeyMap[93] = KEY_NEXT; // AKEYCODE_PAGE_DOWN
|
||||
KeyMap[94] = KEY_UNKNOWN; // AKEYCODE_PICTSYMBOLS
|
||||
KeyMap[95] = KEY_UNKNOWN; // AKEYCODE_SWITCH_CHARSET
|
||||
|
||||
// following look like controller inputs
|
||||
KeyMap[96] = KEY_UNKNOWN; // AKEYCODE_BUTTON_A
|
||||
KeyMap[97] = KEY_UNKNOWN; // AKEYCODE_BUTTON_B
|
||||
KeyMap[98] = KEY_UNKNOWN; // AKEYCODE_BUTTON_C
|
||||
KeyMap[99] = KEY_UNKNOWN; // AKEYCODE_BUTTON_X
|
||||
KeyMap[100] = KEY_UNKNOWN; // AKEYCODE_BUTTON_Y
|
||||
KeyMap[101] = KEY_UNKNOWN; // AKEYCODE_BUTTON_Z
|
||||
KeyMap[102] = KEY_UNKNOWN; // AKEYCODE_BUTTON_L1
|
||||
KeyMap[103] = KEY_UNKNOWN; // AKEYCODE_BUTTON_R1
|
||||
KeyMap[104] = KEY_UNKNOWN; // AKEYCODE_BUTTON_L2
|
||||
KeyMap[105] = KEY_UNKNOWN; // AKEYCODE_BUTTON_R2
|
||||
KeyMap[106] = KEY_UNKNOWN; // AKEYCODE_BUTTON_THUMBL
|
||||
KeyMap[107] = KEY_UNKNOWN; // AKEYCODE_BUTTON_THUMBR
|
||||
KeyMap[108] = KEY_UNKNOWN; // AKEYCODE_BUTTON_START
|
||||
KeyMap[109] = KEY_UNKNOWN; // AKEYCODE_BUTTON_SELECT
|
||||
KeyMap[110] = KEY_UNKNOWN; // AKEYCODE_BUTTON_MODE
|
||||
|
||||
KeyMap[111] = KEY_ESCAPE; // AKEYCODE_ESCAPE
|
||||
KeyMap[112] = KEY_DELETE; // AKEYCODE_FORWARD_DEL
|
||||
KeyMap[113] = KEY_CONTROL; // AKEYCODE_CTRL_LEFT
|
||||
KeyMap[114] = KEY_CONTROL; // AKEYCODE_CTRL_RIGHT
|
||||
KeyMap[115] = KEY_CAPITAL; // AKEYCODE_CAPS_LOCK
|
||||
KeyMap[116] = KEY_SCROLL; // AKEYCODE_SCROLL_LOCK
|
||||
KeyMap[117] = KEY_UNKNOWN; // AKEYCODE_META_LEFT
|
||||
KeyMap[118] = KEY_UNKNOWN; // AKEYCODE_META_RIGHT
|
||||
KeyMap[119] = KEY_UNKNOWN; // AKEYCODE_FUNCTION
|
||||
KeyMap[120] = KEY_SNAPSHOT; // AKEYCODE_SYSRQ
|
||||
KeyMap[121] = KEY_PAUSE; // AKEYCODE_BREAK
|
||||
KeyMap[122] = KEY_HOME; // AKEYCODE_MOVE_HOME
|
||||
KeyMap[123] = KEY_END; // AKEYCODE_MOVE_END
|
||||
KeyMap[124] = KEY_INSERT; // AKEYCODE_INSERT
|
||||
KeyMap[125] = KEY_UNKNOWN; // AKEYCODE_FORWARD
|
||||
KeyMap[126] = KEY_PLAY; // AKEYCODE_MEDIA_PLAY
|
||||
KeyMap[127] = KEY_MEDIA_PLAY_PAUSE; // AKEYCODE_MEDIA_PAUSE
|
||||
KeyMap[128] = KEY_UNKNOWN; // AKEYCODE_MEDIA_CLOSE
|
||||
KeyMap[129] = KEY_UNKNOWN; // AKEYCODE_MEDIA_EJECT
|
||||
KeyMap[130] = KEY_UNKNOWN; // AKEYCODE_MEDIA_RECORD
|
||||
KeyMap[131] = KEY_F1; // AKEYCODE_F1
|
||||
KeyMap[132] = KEY_F2; // AKEYCODE_F2
|
||||
KeyMap[133] = KEY_F3; // AKEYCODE_F3
|
||||
KeyMap[134] = KEY_F4; // AKEYCODE_F4
|
||||
KeyMap[135] = KEY_F5; // AKEYCODE_F5
|
||||
KeyMap[136] = KEY_F6; // AKEYCODE_F6
|
||||
KeyMap[137] = KEY_F7; // AKEYCODE_F7
|
||||
KeyMap[138] = KEY_F8; // AKEYCODE_F8
|
||||
KeyMap[139] = KEY_F9; // AKEYCODE_F9
|
||||
KeyMap[140] = KEY_F10; // AKEYCODE_F10
|
||||
KeyMap[141] = KEY_F11; // AKEYCODE_F11
|
||||
KeyMap[142] = KEY_F12; // AKEYCODE_F12
|
||||
KeyMap[143] = KEY_NUMLOCK; // AKEYCODE_NUM_LOCK
|
||||
KeyMap[144] = KEY_NUMPAD0; // AKEYCODE_NUMPAD_0
|
||||
KeyMap[145] = KEY_NUMPAD1; // AKEYCODE_NUMPAD_1
|
||||
KeyMap[146] = KEY_NUMPAD2; // AKEYCODE_NUMPAD_2
|
||||
KeyMap[147] = KEY_NUMPAD3; // AKEYCODE_NUMPAD_3
|
||||
KeyMap[148] = KEY_NUMPAD4; // AKEYCODE_NUMPAD_4
|
||||
KeyMap[149] = KEY_NUMPAD5; // AKEYCODE_NUMPAD_5
|
||||
KeyMap[150] = KEY_NUMPAD6; // AKEYCODE_NUMPAD_6
|
||||
KeyMap[151] = KEY_NUMPAD7; // AKEYCODE_NUMPAD_7
|
||||
KeyMap[152] = KEY_NUMPAD8; // AKEYCODE_NUMPAD_8
|
||||
KeyMap[153] = KEY_NUMPAD9; // AKEYCODE_NUMPAD_9
|
||||
KeyMap[154] = KEY_DIVIDE; // AKEYCODE_NUMPAD_DIVIDE
|
||||
KeyMap[155] = KEY_MULTIPLY; // AKEYCODE_NUMPAD_MULTIPLY
|
||||
KeyMap[156] = KEY_SUBTRACT; // AKEYCODE_NUMPAD_SUBTRACT
|
||||
KeyMap[157] = KEY_ADD; // AKEYCODE_NUMPAD_ADD
|
||||
KeyMap[158] = KEY_UNKNOWN; // AKEYCODE_NUMPAD_DOT
|
||||
KeyMap[159] = KEY_COMMA; // AKEYCODE_NUMPAD_COMMA
|
||||
KeyMap[160] = KEY_RETURN; // AKEYCODE_NUMPAD_ENTER
|
||||
KeyMap[161] = KEY_UNKNOWN; // AKEYCODE_NUMPAD_EQUALS
|
||||
KeyMap[162] = KEY_UNKNOWN; // AKEYCODE_NUMPAD_LEFT_PAREN
|
||||
KeyMap[163] = KEY_UNKNOWN; // AKEYCODE_NUMPAD_RIGHT_PAREN
|
||||
KeyMap[164] = KEY_VOLUME_MUTE; // AKEYCODE_VOLUME_MUTE
|
||||
KeyMap[165] = KEY_UNKNOWN; // AKEYCODE_INFO
|
||||
KeyMap[166] = KEY_UNKNOWN; // AKEYCODE_CHANNEL_UP
|
||||
KeyMap[167] = KEY_UNKNOWN; // AKEYCODE_CHANNEL_DOWN
|
||||
KeyMap[168] = KEY_ZOOM; // AKEYCODE_ZOOM_IN
|
||||
KeyMap[169] = KEY_UNKNOWN; // AKEYCODE_ZOOM_OUT
|
||||
KeyMap[170] = KEY_UNKNOWN; // AKEYCODE_TV
|
||||
KeyMap[171] = KEY_UNKNOWN; // AKEYCODE_WINDOW
|
||||
KeyMap[172] = KEY_UNKNOWN; // AKEYCODE_GUIDE
|
||||
KeyMap[173] = KEY_UNKNOWN; // AKEYCODE_DVR
|
||||
KeyMap[174] = KEY_UNKNOWN; // AKEYCODE_BOOKMARK
|
||||
KeyMap[175] = KEY_UNKNOWN; // AKEYCODE_CAPTIONS
|
||||
KeyMap[176] = KEY_UNKNOWN; // AKEYCODE_SETTINGS
|
||||
KeyMap[177] = KEY_UNKNOWN; // AKEYCODE_TV_POWER
|
||||
KeyMap[178] = KEY_UNKNOWN; // AKEYCODE_TV_INPUT
|
||||
KeyMap[179] = KEY_UNKNOWN; // AKEYCODE_STB_POWER
|
||||
KeyMap[180] = KEY_UNKNOWN; // AKEYCODE_STB_INPUT
|
||||
KeyMap[181] = KEY_UNKNOWN; // AKEYCODE_AVR_POWER
|
||||
KeyMap[182] = KEY_UNKNOWN; // AKEYCODE_AVR_INPUT
|
||||
KeyMap[183] = KEY_UNKNOWN; // AKEYCODE_PROG_RED
|
||||
KeyMap[184] = KEY_UNKNOWN; // AKEYCODE_PROG_GREEN
|
||||
KeyMap[185] = KEY_UNKNOWN; // AKEYCODE_PROG_YELLOW
|
||||
KeyMap[186] = KEY_UNKNOWN; // AKEYCODE_PROG_BLUE
|
||||
KeyMap[187] = KEY_UNKNOWN; // AKEYCODE_APP_SWITCH
|
||||
KeyMap[188] = KEY_UNKNOWN; // AKEYCODE_BUTTON_1
|
||||
KeyMap[189] = KEY_UNKNOWN; // AKEYCODE_BUTTON_2
|
||||
KeyMap[190] = KEY_UNKNOWN; // AKEYCODE_BUTTON_3
|
||||
KeyMap[191] = KEY_UNKNOWN; // AKEYCODE_BUTTON_4
|
||||
KeyMap[192] = KEY_UNKNOWN; // AKEYCODE_BUTTON_5
|
||||
KeyMap[193] = KEY_UNKNOWN; // AKEYCODE_BUTTON_6
|
||||
KeyMap[194] = KEY_UNKNOWN; // AKEYCODE_BUTTON_7
|
||||
KeyMap[195] = KEY_UNKNOWN; // AKEYCODE_BUTTON_8
|
||||
KeyMap[196] = KEY_UNKNOWN; // AKEYCODE_BUTTON_9
|
||||
KeyMap[197] = KEY_UNKNOWN; // AKEYCODE_BUTTON_10
|
||||
KeyMap[198] = KEY_UNKNOWN; // AKEYCODE_BUTTON_11
|
||||
KeyMap[199] = KEY_UNKNOWN; // AKEYCODE_BUTTON_12
|
||||
KeyMap[200] = KEY_UNKNOWN; // AKEYCODE_BUTTON_13
|
||||
KeyMap[201] = KEY_UNKNOWN; // AKEYCODE_BUTTON_14
|
||||
KeyMap[202] = KEY_UNKNOWN; // AKEYCODE_BUTTON_15
|
||||
KeyMap[203] = KEY_UNKNOWN; // AKEYCODE_BUTTON_16
|
||||
KeyMap[204] = KEY_UNKNOWN; // AKEYCODE_LANGUAGE_SWITCH
|
||||
KeyMap[205] = KEY_UNKNOWN; // AKEYCODE_MANNER_MODE
|
||||
KeyMap[206] = KEY_UNKNOWN; // AKEYCODE_3D_MODE
|
||||
KeyMap[207] = KEY_UNKNOWN; // AKEYCODE_CONTACTS
|
||||
KeyMap[208] = KEY_UNKNOWN; // AKEYCODE_CALENDAR
|
||||
KeyMap[209] = KEY_UNKNOWN; // AKEYCODE_MUSIC
|
||||
KeyMap[210] = KEY_UNKNOWN; // AKEYCODE_CALCULATOR
|
||||
KeyMap[211] = KEY_UNKNOWN; // AKEYCODE_ZENKAKU_HANKAKU
|
||||
KeyMap[212] = KEY_UNKNOWN; // AKEYCODE_EISU
|
||||
KeyMap[213] = KEY_UNKNOWN; // AKEYCODE_MUHENKAN
|
||||
KeyMap[214] = KEY_UNKNOWN; // AKEYCODE_HENKAN
|
||||
KeyMap[215] = KEY_UNKNOWN; // AKEYCODE_KATAKANA_HIRAGANA
|
||||
KeyMap[216] = KEY_UNKNOWN; // AKEYCODE_YEN
|
||||
KeyMap[217] = KEY_UNKNOWN; // AKEYCODE_RO
|
||||
KeyMap[218] = KEY_UNKNOWN; // AKEYCODE_KANA
|
||||
KeyMap[219] = KEY_UNKNOWN; // AKEYCODE_ASSIST
|
||||
KeyMap[220] = KEY_UNKNOWN; // AKEYCODE_BRIGHTNESS_DOWN
|
||||
KeyMap[221] = KEY_UNKNOWN; // AKEYCODE_BRIGHTNESS_UP ,
|
||||
KeyMap[222] = KEY_UNKNOWN; // AKEYCODE_MEDIA_AUDIO_TRACK
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::activateAccelerometer(float updateInterval)
|
||||
{
|
||||
if (!isAccelerometerAvailable())
|
||||
return false;
|
||||
|
||||
ASensorEventQueue_enableSensor(SensorEventQueue, Accelerometer);
|
||||
ASensorEventQueue_setEventRate(SensorEventQueue, Accelerometer, (int32_t)(updateInterval * 1000.f * 1000.f)); // in microseconds
|
||||
|
||||
os::Printer::log("Activated accelerometer", ELL_DEBUG);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::deactivateAccelerometer()
|
||||
{
|
||||
if (Accelerometer) {
|
||||
ASensorEventQueue_disableSensor(SensorEventQueue, Accelerometer);
|
||||
Accelerometer = 0;
|
||||
os::Printer::log("Deactivated accelerometer", ELL_DEBUG);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isAccelerometerActive()
|
||||
{
|
||||
return (Accelerometer != 0);
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isAccelerometerAvailable()
|
||||
{
|
||||
if (!Accelerometer)
|
||||
Accelerometer = ASensorManager_getDefaultSensor(SensorManager, ASENSOR_TYPE_ACCELEROMETER);
|
||||
|
||||
return (Accelerometer != 0);
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::activateGyroscope(float updateInterval)
|
||||
{
|
||||
if (!isGyroscopeAvailable())
|
||||
return false;
|
||||
|
||||
ASensorEventQueue_enableSensor(SensorEventQueue, Gyroscope);
|
||||
ASensorEventQueue_setEventRate(SensorEventQueue, Gyroscope, (int32_t)(updateInterval * 1000.f * 1000.f)); // in microseconds
|
||||
|
||||
os::Printer::log("Activated gyroscope", ELL_DEBUG);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::deactivateGyroscope()
|
||||
{
|
||||
if (Gyroscope) {
|
||||
ASensorEventQueue_disableSensor(SensorEventQueue, Gyroscope);
|
||||
Gyroscope = 0;
|
||||
os::Printer::log("Deactivated gyroscope", ELL_DEBUG);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isGyroscopeActive()
|
||||
{
|
||||
return (Gyroscope != 0);
|
||||
}
|
||||
|
||||
bool CIrrDeviceAndroid::isGyroscopeAvailable()
|
||||
{
|
||||
if (!Gyroscope)
|
||||
Gyroscope = ASensorManager_getDefaultSensor(SensorManager, ASENSOR_TYPE_GYROSCOPE);
|
||||
|
||||
return (Gyroscope != 0);
|
||||
}
|
||||
|
||||
} // end namespace irr
|
|
@ -1,98 +0,0 @@
|
|||
// Copyright (C) 2002-2011 Nikolaus Gebhardt
|
||||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CIrrDeviceStub.h"
|
||||
#include "IrrlichtDevice.h"
|
||||
#include "ICursorControl.h"
|
||||
|
||||
#include <android/sensor.h>
|
||||
#include <android_native_app_glue.h>
|
||||
|
||||
namespace irr
|
||||
{
|
||||
class CIrrDeviceAndroid : public CIrrDeviceStub
|
||||
{
|
||||
public:
|
||||
CIrrDeviceAndroid(const SIrrlichtCreationParameters ¶m);
|
||||
|
||||
virtual ~CIrrDeviceAndroid();
|
||||
|
||||
virtual bool run();
|
||||
|
||||
virtual void yield();
|
||||
|
||||
virtual void sleep(u32 timeMs, bool pauseTimer = false);
|
||||
|
||||
virtual void setWindowCaption(const wchar_t *text);
|
||||
|
||||
virtual bool isWindowActive() const;
|
||||
|
||||
virtual bool isWindowFocused() const;
|
||||
|
||||
virtual bool isWindowMinimized() const;
|
||||
|
||||
virtual bool isWindowVisible() const;
|
||||
|
||||
virtual void closeDevice();
|
||||
|
||||
virtual void setResizable(bool resize = false);
|
||||
|
||||
virtual void minimizeWindow();
|
||||
|
||||
virtual void maximizeWindow();
|
||||
|
||||
virtual void restoreWindow();
|
||||
|
||||
virtual core::position2di getWindowPosition();
|
||||
|
||||
virtual E_DEVICE_TYPE getType() const;
|
||||
|
||||
virtual bool activateAccelerometer(float updateInterval);
|
||||
|
||||
virtual bool deactivateAccelerometer();
|
||||
|
||||
virtual bool isAccelerometerActive();
|
||||
|
||||
virtual bool isAccelerometerAvailable();
|
||||
|
||||
virtual bool activateGyroscope(float updateInterval);
|
||||
|
||||
virtual bool deactivateGyroscope();
|
||||
|
||||
virtual bool isGyroscopeActive();
|
||||
|
||||
virtual bool isGyroscopeAvailable();
|
||||
|
||||
private:
|
||||
static void handleAndroidCommand(android_app *app, int32_t cmd);
|
||||
|
||||
static s32 handleInput(android_app *app, AInputEvent *event);
|
||||
|
||||
void createDriver();
|
||||
|
||||
void createKeyMap();
|
||||
|
||||
video::SExposedVideoData &getExposedVideoData();
|
||||
|
||||
android_app *Android;
|
||||
ASensorManager *SensorManager;
|
||||
ASensorEventQueue *SensorEventQueue;
|
||||
const ASensor *Accelerometer;
|
||||
const ASensor *Gyroscope;
|
||||
|
||||
bool Initialized;
|
||||
bool Stopped;
|
||||
bool Paused;
|
||||
bool Focused;
|
||||
|
||||
JNIEnv *JNIEnvAttachedToVM;
|
||||
|
||||
video::SExposedVideoData ExposedVideoData;
|
||||
|
||||
core::array<EKEY_CODE> KeyMap;
|
||||
};
|
||||
|
||||
} // end namespace irr
|
|
@ -1,52 +0,0 @@
|
|||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#include "CKeyEventWrapper.h"
|
||||
|
||||
#include "os.h"
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace jni
|
||||
{
|
||||
|
||||
jclass CKeyEventWrapper::Class_KeyEvent = 0;
|
||||
jmethodID CKeyEventWrapper::Method_constructor = 0;
|
||||
jmethodID CKeyEventWrapper::Method_getUnicodeChar = 0;
|
||||
|
||||
CKeyEventWrapper::CKeyEventWrapper(JNIEnv *jniEnv, int action, int code) :
|
||||
JniEnv(jniEnv), JniKeyEvent(0)
|
||||
{
|
||||
if (JniEnv) {
|
||||
if (!Class_KeyEvent) {
|
||||
// Find java classes & functions on first call
|
||||
os::Printer::log("CKeyEventWrapper first initialize", ELL_DEBUG);
|
||||
jclass localClass = JniEnv->FindClass("android/view/KeyEvent");
|
||||
if (localClass) {
|
||||
Class_KeyEvent = reinterpret_cast<jclass>(JniEnv->NewGlobalRef(localClass));
|
||||
}
|
||||
|
||||
Method_constructor = JniEnv->GetMethodID(Class_KeyEvent, "<init>", "(II)V");
|
||||
Method_getUnicodeChar = JniEnv->GetMethodID(Class_KeyEvent, "getUnicodeChar", "(I)I");
|
||||
}
|
||||
|
||||
if (Class_KeyEvent && Method_constructor) {
|
||||
JniKeyEvent = JniEnv->NewObject(Class_KeyEvent, Method_constructor, action, code);
|
||||
} else {
|
||||
os::Printer::log("CKeyEventWrapper didn't find JNI classes/methods", ELL_WARNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CKeyEventWrapper::~CKeyEventWrapper()
|
||||
{
|
||||
JniEnv->DeleteLocalRef(JniKeyEvent);
|
||||
}
|
||||
|
||||
int CKeyEventWrapper::getUnicodeChar(int metaState)
|
||||
{
|
||||
return JniEnv->CallIntMethod(JniKeyEvent, Method_getUnicodeChar, metaState);
|
||||
}
|
||||
|
||||
} // namespace jni
|
||||
} // namespace irr
|
|
@ -1,35 +0,0 @@
|
|||
// This file is part of the "Irrlicht Engine".
|
||||
// For conditions of distribution and use, see copyright notice in irrlicht.h
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
struct android_app;
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace jni
|
||||
{
|
||||
|
||||
//! Minimal JNI wrapper class around android.view.KeyEvent
|
||||
//! NOTE: Only functions we actually use in the engine are wrapped
|
||||
//! This is currently not written to support multithreading - meaning threads are not attached/detached to the Java VM (to be discussed)
|
||||
class CKeyEventWrapper
|
||||
{
|
||||
public:
|
||||
CKeyEventWrapper(JNIEnv *jniEnv, int action, int code);
|
||||
~CKeyEventWrapper();
|
||||
|
||||
int getUnicodeChar(int metaState);
|
||||
|
||||
private:
|
||||
static jclass Class_KeyEvent;
|
||||
static jmethodID Method_getUnicodeChar;
|
||||
static jmethodID Method_constructor;
|
||||
JNIEnv *JniEnv;
|
||||
jobject JniKeyEvent; // this object in java
|
||||
};
|
||||
|
||||
} // namespace jni
|
||||
} // namespace irr
|
|
@ -10,10 +10,6 @@
|
|||
#include "irrArray.h"
|
||||
#include "os.h"
|
||||
|
||||
#if defined(_IRR_COMPILE_WITH_ANDROID_DEVICE_)
|
||||
#include <android/native_activity.h>
|
||||
#endif
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace video
|
||||
|
@ -55,9 +51,6 @@ bool CEGLManager::initialize(const SIrrlichtCreationParameters ¶ms, const SE
|
|||
#elif defined(_IRR_COMPILE_WITH_X11_DEVICE_)
|
||||
EglWindow = (NativeWindowType)Data.OpenGLLinux.X11Window;
|
||||
EglDisplay = eglGetDisplay((NativeDisplayType)Data.OpenGLLinux.X11Display);
|
||||
#elif defined(_IRR_COMPILE_WITH_ANDROID_DEVICE_)
|
||||
EglWindow = (ANativeWindow *)Data.OGLESAndroid.Window;
|
||||
EglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
#elif defined(_IRR_COMPILE_WITH_FB_DEVICE_)
|
||||
EglWindow = (NativeWindowType)Data.OpenGLFB.Window;
|
||||
EglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
|
@ -119,10 +112,6 @@ bool CEGLManager::generateSurface()
|
|||
// at this time only Android support this feature.
|
||||
// this needs an update method instead!
|
||||
|
||||
#if defined(_IRR_COMPILE_WITH_ANDROID_DEVICE_)
|
||||
EglWindow = (ANativeWindow *)Data.OGLESAndroid.Window;
|
||||
#endif
|
||||
|
||||
#if defined(_IRR_EMSCRIPTEN_PLATFORM_)
|
||||
// eglChooseConfig is currently only implemented as stub in emscripten (version 1.37.22 at point of writing)
|
||||
// But the other solution would also be fine as it also only generates a single context so there is not much to choose from.
|
||||
|
@ -136,13 +125,6 @@ bool CEGLManager::generateSurface()
|
|||
return false;
|
||||
}
|
||||
|
||||
#if defined(_IRR_COMPILE_WITH_ANDROID_DEVICE_)
|
||||
EGLint Format = 0;
|
||||
eglGetConfigAttrib(EglDisplay, EglConfig, EGL_NATIVE_VISUAL_ID, &Format);
|
||||
|
||||
ANativeWindow_setBuffersGeometry(EglWindow, 0, 0, Format);
|
||||
#endif
|
||||
|
||||
// Now we are able to create EGL surface.
|
||||
EglSurface = eglCreateWindowSurface(EglDisplay, EglConfig, EglWindow, 0);
|
||||
|
||||
|
@ -547,7 +529,8 @@ bool CEGLManager::swapBuffers()
|
|||
|
||||
bool CEGLManager::testEGLError()
|
||||
{
|
||||
#if defined(EGL_VERSION_1_0) && defined(_DEBUG)
|
||||
if (!Params.DriverDebug)
|
||||
return false;
|
||||
EGLint status = eglGetError();
|
||||
|
||||
switch (status) {
|
||||
|
@ -600,9 +583,6 @@ bool CEGLManager::testEGLError()
|
|||
};
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,14 +18,20 @@ namespace video
|
|||
// PNG function for error handling
|
||||
static void png_cpexcept_error(png_structp png_ptr, png_const_charp msg)
|
||||
{
|
||||
os::Printer::log("PNG fatal error", msg, ELL_ERROR);
|
||||
io::IReadFile *file = reinterpret_cast<io::IReadFile *>(png_get_error_ptr(png_ptr));
|
||||
std::string logmsg = std::string("PNG fatal error for ")
|
||||
+ file->getFileName().c_str() + ": " + msg;
|
||||
os::Printer::log(logmsg.c_str(), ELL_ERROR);
|
||||
longjmp(png_jmpbuf(png_ptr), 1);
|
||||
}
|
||||
|
||||
// PNG function for warning handling
|
||||
static void png_cpexcept_warn(png_structp png_ptr, png_const_charp msg)
|
||||
{
|
||||
os::Printer::log("PNG warning", msg, ELL_WARNING);
|
||||
io::IReadFile *file = reinterpret_cast<io::IReadFile *>(png_get_error_ptr(png_ptr));
|
||||
std::string logmsg = std::string("PNG warning for ")
|
||||
+ file->getFileName().c_str() + ": " + msg;
|
||||
os::Printer::log(logmsg.c_str(), ELL_WARNING);
|
||||
}
|
||||
|
||||
// PNG function for file reading
|
||||
|
@ -88,7 +94,7 @@ IImage *CImageLoaderPng::loadImage(io::IReadFile *file) const
|
|||
|
||||
// Allocate the png read struct
|
||||
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
|
||||
NULL, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warn);
|
||||
file, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warn);
|
||||
if (!png_ptr) {
|
||||
os::Printer::log("LOAD PNG: Internal PNG create read struct failure", file->getFileName(), ELL_ERROR);
|
||||
return 0;
|
||||
|
|
|
@ -25,14 +25,20 @@ IImageWriter *createImageWriterPNG()
|
|||
// PNG function for error handling
|
||||
static void png_cpexcept_error(png_structp png_ptr, png_const_charp msg)
|
||||
{
|
||||
os::Printer::log("PNG fatal error", msg, ELL_ERROR);
|
||||
io::IWriteFile *file = reinterpret_cast<io::IWriteFile *>(png_get_error_ptr(png_ptr));
|
||||
std::string logmsg = std::string("PNG fatal error for ")
|
||||
+ file->getFileName().c_str() + ": " + msg;
|
||||
os::Printer::log(logmsg.c_str(), ELL_ERROR);
|
||||
longjmp(png_jmpbuf(png_ptr), 1);
|
||||
}
|
||||
|
||||
// PNG function for warning handling
|
||||
static void png_cpexcept_warning(png_structp png_ptr, png_const_charp msg)
|
||||
{
|
||||
os::Printer::log("PNG warning", msg, ELL_WARNING);
|
||||
io::IWriteFile *file = reinterpret_cast<io::IWriteFile *>(png_get_error_ptr(png_ptr));
|
||||
std::string logmsg = std::string("PNG warning for ")
|
||||
+ file->getFileName().c_str() + ": " + msg;
|
||||
os::Printer::log(logmsg.c_str(), ELL_WARNING);
|
||||
}
|
||||
|
||||
// PNG function for file writing
|
||||
|
@ -66,7 +72,7 @@ bool CImageWriterPNG::writeImage(io::IWriteFile *file, IImage *image, u32 param)
|
|||
|
||||
// Allocate the png write struct
|
||||
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
|
||||
NULL, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warning);
|
||||
file, (png_error_ptr)png_cpexcept_error, (png_error_ptr)png_cpexcept_warning);
|
||||
if (!png_ptr) {
|
||||
os::Printer::log("PNGWriter: Internal PNG create write struct failure", file->getFileName(), ELL_ERROR);
|
||||
return false;
|
||||
|
|
|
@ -249,16 +249,34 @@ void CIrrDeviceSDL::resetReceiveTextInputEvents()
|
|||
//! constructor
|
||||
CIrrDeviceSDL::CIrrDeviceSDL(const SIrrlichtCreationParameters ¶m) :
|
||||
CIrrDeviceStub(param),
|
||||
Window((SDL_Window *)param.WindowId), SDL_Flags(0),
|
||||
Window((SDL_Window *)param.WindowId),
|
||||
MouseX(0), MouseY(0), MouseXRel(0), MouseYRel(0), MouseButtonStates(0),
|
||||
Width(param.WindowSize.Width), Height(param.WindowSize.Height),
|
||||
Resizable(param.WindowResizable == 1 ? true : false), CurrentTouchCount(0)
|
||||
Resizable(param.WindowResizable == 1 ? true : false), CurrentTouchCount(0),
|
||||
IsInBackground(false)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
setDebugName("CIrrDeviceSDL");
|
||||
#endif
|
||||
|
||||
if (++SDLDeviceInstances == 1) {
|
||||
#ifdef __ANDROID__
|
||||
// Blocking on pause causes problems with multiplayer.
|
||||
// See https://github.com/minetest/minetest/issues/10842.
|
||||
SDL_SetHint(SDL_HINT_ANDROID_BLOCK_ON_PAUSE, "0");
|
||||
SDL_SetHint(SDL_HINT_ANDROID_BLOCK_ON_PAUSE_PAUSEAUDIO, "0");
|
||||
|
||||
SDL_SetHint(SDL_HINT_ANDROID_TRAP_BACK_BUTTON, "1");
|
||||
|
||||
// Minetest does its own screen keyboard handling.
|
||||
SDL_SetHint(SDL_HINT_ENABLE_SCREEN_KEYBOARD, "0");
|
||||
#endif
|
||||
|
||||
// Minetest has its own code to synthesize mouse events from touch events,
|
||||
// so we prevent SDL from doing it.
|
||||
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
|
||||
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
|
||||
|
||||
u32 flags = SDL_INIT_TIMER | SDL_INIT_EVENTS;
|
||||
if (CreationParams.DriverType != video::EDT_NULL)
|
||||
flags |= SDL_INIT_VIDEO;
|
||||
|
@ -273,11 +291,6 @@ CIrrDeviceSDL::CIrrDeviceSDL(const SIrrlichtCreationParameters ¶m) :
|
|||
}
|
||||
}
|
||||
|
||||
// Minetest has its own code to synthesize mouse events from touch events,
|
||||
// so we prevent SDL from doing it.
|
||||
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
|
||||
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
|
||||
|
||||
// create keymap
|
||||
createKeyMap();
|
||||
|
||||
|
@ -369,6 +382,87 @@ void CIrrDeviceSDL::logAttributes()
|
|||
|
||||
bool CIrrDeviceSDL::createWindow()
|
||||
{
|
||||
if (Close)
|
||||
return false;
|
||||
|
||||
if (createWindowWithContext())
|
||||
return true;
|
||||
|
||||
if (CreationParams.DriverDebug) {
|
||||
CreationParams.DriverDebug = false;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("DriverDebug reduced due to lack of support!");
|
||||
// Turn it back on because the GL driver can maybe still do something useful.
|
||||
CreationParams.DriverDebug = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
while (CreationParams.AntiAlias > 0) {
|
||||
CreationParams.AntiAlias--;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("AntiAlias reduced/disabled due to lack of support!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CreationParams.WithAlphaChannel) {
|
||||
CreationParams.WithAlphaChannel = false;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("WithAlphaChannel disabled due to lack of support!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CreationParams.Stencilbuffer) {
|
||||
CreationParams.Stencilbuffer = false;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("Stencilbuffer disabled due to lack of support!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
while (CreationParams.ZBufferBits > 16) {
|
||||
CreationParams.ZBufferBits -= 8;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("ZBufferBits reduced due to lack of support!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
while (CreationParams.Bits > 16) {
|
||||
CreationParams.Bits -= 8;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("Bits reduced due to lack of support!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CreationParams.Stereobuffer) {
|
||||
CreationParams.Stereobuffer = false;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("Stereobuffer disabled due to lack of support!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CreationParams.Doublebuffer) {
|
||||
// Try single buffer
|
||||
CreationParams.Doublebuffer = false;
|
||||
if (createWindowWithContext()) {
|
||||
os::Printer::log("Doublebuffer disabled due to lack of support!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
os::Printer::log("Could not create window and context!", ELL_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CIrrDeviceSDL::createWindowWithContext()
|
||||
{
|
||||
u32 SDL_Flags = 0;
|
||||
|
||||
if (CreationParams.Fullscreen) {
|
||||
#ifdef _IRR_EMSCRIPTEN_PLATFORM_
|
||||
SDL_Flags |= SDL_WINDOW_FULLSCREEN;
|
||||
|
@ -382,6 +476,8 @@ bool CIrrDeviceSDL::createWindow()
|
|||
SDL_Flags |= SDL_WINDOW_MAXIMIZED;
|
||||
SDL_Flags |= SDL_WINDOW_OPENGL;
|
||||
|
||||
SDL_GL_ResetAttributes();
|
||||
|
||||
#ifdef _IRR_EMSCRIPTEN_PLATFORM_
|
||||
if (Width != 0 || Height != 0)
|
||||
emscripten_set_canvas_size(Width, Height);
|
||||
|
@ -421,9 +517,6 @@ bool CIrrDeviceSDL::createWindow()
|
|||
|
||||
return true;
|
||||
#else // !_IRR_EMSCRIPTEN_PLATFORM_
|
||||
if (Close)
|
||||
return false;
|
||||
|
||||
switch (CreationParams.DriverType) {
|
||||
case video::EDT_OPENGL:
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||
|
@ -448,9 +541,9 @@ bool CIrrDeviceSDL::createWindow()
|
|||
default:;
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG | SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG);
|
||||
#endif
|
||||
if (CreationParams.DriverDebug) {
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG | SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG);
|
||||
}
|
||||
|
||||
if (CreationParams.Bits == 16) {
|
||||
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
|
||||
|
@ -470,46 +563,33 @@ bool CIrrDeviceSDL::createWindow()
|
|||
if (CreationParams.AntiAlias > 1) {
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, CreationParams.AntiAlias);
|
||||
}
|
||||
if (!Window)
|
||||
Window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, Width, Height, SDL_Flags);
|
||||
if (!Window) {
|
||||
os::Printer::log("Could not create window...", SDL_GetError(), ELL_WARNING);
|
||||
}
|
||||
if (!Window && CreationParams.AntiAlias > 1) {
|
||||
while (--CreationParams.AntiAlias > 1) {
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, CreationParams.AntiAlias);
|
||||
Window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, Width, Height, SDL_Flags);
|
||||
if (Window)
|
||||
break;
|
||||
}
|
||||
if (!Window) {
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0);
|
||||
Window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, Width, Height, SDL_Flags);
|
||||
if (Window)
|
||||
os::Printer::log("AntiAliasing disabled due to lack of support!", ELL_WARNING);
|
||||
}
|
||||
} else {
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0);
|
||||
}
|
||||
|
||||
if (!Window && CreationParams.Doublebuffer) {
|
||||
// Try single buffer
|
||||
if (CreationParams.DriverType == video::EDT_OPENGL)
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
Window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, Width, Height, SDL_Flags);
|
||||
}
|
||||
Window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, Width, Height, SDL_Flags);
|
||||
if (!Window) {
|
||||
os::Printer::log("Could not initialize display", SDL_GetError(), ELL_ERROR);
|
||||
os::Printer::log("Could not create window", SDL_GetError(), ELL_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
Context = SDL_GL_CreateContext(Window);
|
||||
if (!Context) {
|
||||
os::Printer::log("Could not initialize context", SDL_GetError(), ELL_ERROR);
|
||||
os::Printer::log("Could not create context", SDL_GetError(), ELL_WARNING);
|
||||
SDL_DestroyWindow(Window);
|
||||
Window = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update Width and Height to match the actual window size.
|
||||
// In fullscreen mode, the window size specified in SIrrlichtCreationParameters
|
||||
// is ignored, so we cannot rely on it.
|
||||
int w = 0, h = 0;
|
||||
SDL_GetWindowSize(Window, &w, &h);
|
||||
Width = w;
|
||||
Height = h;
|
||||
|
||||
return true;
|
||||
#endif // !_IRR_EMSCRIPTEN_PLATFORM_
|
||||
}
|
||||
|
@ -542,6 +622,19 @@ void CIrrDeviceSDL::createDriver()
|
|||
os::Printer::log("Could not create video driver", ELL_ERROR);
|
||||
}
|
||||
|
||||
static int wrap_PollEvent(SDL_Event *ev)
|
||||
{
|
||||
u32 t0 = os::Timer::getRealTime();
|
||||
int ret = SDL_PollEvent(ev);
|
||||
u32 d = os::Timer::getRealTime() - t0;
|
||||
if (d >= 5) {
|
||||
auto msg = std::string("SDL_PollEvent took too long: ") + std::to_string(d) + "ms";
|
||||
// 50ms delay => more than three missed frames (at 60fps)
|
||||
os::Printer::log(msg.c_str(), d >= 50 ? ELL_WARNING : ELL_INFORMATION);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
//! runs the device. Returns false if device wants to be deleted
|
||||
bool CIrrDeviceSDL::run()
|
||||
{
|
||||
|
@ -550,7 +643,7 @@ bool CIrrDeviceSDL::run()
|
|||
SEvent irrevent;
|
||||
SDL_Event SDL_event;
|
||||
|
||||
while (!Close && SDL_PollEvent(&SDL_event)) {
|
||||
while (!Close && wrap_PollEvent(&SDL_event)) {
|
||||
// os::Printer::log("event: ", core::stringc((int)SDL_event.type).c_str(), ELL_INFORMATION); // just for debugging
|
||||
|
||||
switch (SDL_event.type) {
|
||||
|
@ -621,7 +714,17 @@ bool CIrrDeviceSDL::run()
|
|||
}
|
||||
#endif
|
||||
|
||||
switch (SDL_event.button.button) {
|
||||
auto button = SDL_event.button.button;
|
||||
#ifdef __ANDROID__
|
||||
// Android likes to send the right mouse button as the back button.
|
||||
// According to some web searches I did, this is probably
|
||||
// vendor/device-specific.
|
||||
// Since a working right mouse button is very important for
|
||||
// Minetest, we have this little hack.
|
||||
if (button == SDL_BUTTON_X2)
|
||||
button = SDL_BUTTON_RIGHT;
|
||||
#endif
|
||||
switch (button) {
|
||||
case SDL_BUTTON_LEFT:
|
||||
if (SDL_event.type == SDL_MOUSEBUTTONDOWN) {
|
||||
irrevent.MouseInput.Event = irr::EMIE_LMOUSE_PRESSED_DOWN;
|
||||
|
@ -772,6 +875,20 @@ bool CIrrDeviceSDL::run()
|
|||
postEventFromUser(irrevent);
|
||||
break;
|
||||
|
||||
// Contrary to what the SDL documentation says, SDL_APP_WILLENTERBACKGROUND
|
||||
// and SDL_APP_WILLENTERFOREGROUND are actually sent in onStop/onStart,
|
||||
// not onPause/onResume, on recent Android versions. This can be verified
|
||||
// by testing or by looking at the org.libsdl.app.SDLActivity Java code.
|
||||
// -> This means we can use them to implement isWindowVisible().
|
||||
|
||||
case SDL_APP_WILLENTERBACKGROUND:
|
||||
IsInBackground = true;
|
||||
break;
|
||||
|
||||
case SDL_APP_WILLENTERFOREGROUND:
|
||||
IsInBackground = false;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
} // end switch
|
||||
|
@ -998,11 +1115,6 @@ void CIrrDeviceSDL::setResizable(bool resize)
|
|||
return;
|
||||
#else // !_IRR_EMSCRIPTEN_PLATFORM_
|
||||
if (resize != Resizable) {
|
||||
if (resize)
|
||||
SDL_Flags |= SDL_WINDOW_RESIZABLE;
|
||||
else
|
||||
SDL_Flags &= ~SDL_WINDOW_RESIZABLE;
|
||||
|
||||
if (Window) {
|
||||
SDL_SetWindowResizable(Window, (SDL_bool)resize);
|
||||
}
|
||||
|
@ -1053,6 +1165,11 @@ bool CIrrDeviceSDL::isFullscreen() const
|
|||
#endif
|
||||
}
|
||||
|
||||
bool CIrrDeviceSDL::isWindowVisible() const
|
||||
{
|
||||
return !IsInBackground;
|
||||
}
|
||||
|
||||
//! returns if window is active. if not, nothing need to be drawn
|
||||
bool CIrrDeviceSDL::isWindowActive() const
|
||||
{
|
||||
|
@ -1111,6 +1228,8 @@ void CIrrDeviceSDL::createKeyMap()
|
|||
|
||||
// buttons missing
|
||||
|
||||
KeyMap.push_back(SKeyMap(SDLK_AC_BACK, KEY_CANCEL));
|
||||
|
||||
KeyMap.push_back(SKeyMap(SDLK_BACKSPACE, KEY_BACK));
|
||||
KeyMap.push_back(SKeyMap(SDLK_TAB, KEY_TAB));
|
||||
KeyMap.push_back(SKeyMap(SDLK_CLEAR, KEY_CLEAR));
|
||||
|
|
|
@ -86,6 +86,9 @@ public:
|
|||
/** \return True if window is fullscreen. */
|
||||
bool isFullscreen() const override;
|
||||
|
||||
//! Checks if the window could possibly be visible.
|
||||
bool isWindowVisible() const override;
|
||||
|
||||
//! Get the position of this window on screen
|
||||
core::position2di getWindowPosition() override;
|
||||
|
||||
|
@ -277,13 +280,13 @@ private:
|
|||
void createDriver();
|
||||
|
||||
bool createWindow();
|
||||
bool createWindowWithContext();
|
||||
|
||||
void createKeyMap();
|
||||
|
||||
void logAttributes();
|
||||
SDL_GLContext Context;
|
||||
SDL_Window *Window;
|
||||
int SDL_Flags;
|
||||
#if defined(_IRR_COMPILE_WITH_JOYSTICK_EVENTS_)
|
||||
core::array<SDL_Joystick *> Joysticks;
|
||||
#endif
|
||||
|
@ -319,6 +322,7 @@ private:
|
|||
SDL_SysWMinfo Info;
|
||||
|
||||
s32 CurrentTouchCount;
|
||||
bool IsInBackground;
|
||||
};
|
||||
|
||||
} // end namespace irr
|
||||
|
|
|
@ -1,25 +1,11 @@
|
|||
if(NOT ANDROID AND NOT APPLE)
|
||||
if(NOT APPLE)
|
||||
set(DEFAULT_SDL2 ON)
|
||||
endif()
|
||||
|
||||
option(BUILD_SHARED_LIBS "Build shared library" TRUE)
|
||||
option(USE_SDL2 "Use the SDL2 backend" ${DEFAULT_SDL2})
|
||||
|
||||
# Compiler flags
|
||||
|
||||
add_definitions(-DIRRLICHT_EXPORTS)
|
||||
if(BUILD_SHARED_LIBS)
|
||||
if(WIN32)
|
||||
set(API_IMPORT "__declspec(dllimport)")
|
||||
set(API_EXPORT "__declspec(dllexport)")
|
||||
elseif(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
|
||||
set(API_EXPORT "__attribute__ ((visibility(\"default\")))") # only necessary if default visibility is set to hidden
|
||||
endif()
|
||||
else()
|
||||
add_definitions(-D_IRR_STATIC_LIB_)
|
||||
endif()
|
||||
add_definitions("-DIRRLICHT_API=${API_EXPORT}")
|
||||
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
add_definitions(-D_DEBUG)
|
||||
endif()
|
||||
|
@ -52,8 +38,6 @@ elseif(MSVC)
|
|||
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
add_compile_options(/arch:SSE)
|
||||
endif()
|
||||
|
||||
add_compile_options(/D_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING)
|
||||
endif()
|
||||
|
||||
# Platform-independent configuration (hard-coded currently)
|
||||
|
@ -77,10 +61,9 @@ elseif(APPLE)
|
|||
set(DEVICE "OSX")
|
||||
elseif(ANDROID)
|
||||
add_definitions(-D_IRR_ANDROID_PLATFORM_)
|
||||
if(USE_SDL2)
|
||||
message(FATAL_ERROR "SDL2 device is not (yet) supported on Android")
|
||||
if(NOT USE_SDL2)
|
||||
message(FATAL_ERROR "The Android build requires SDL2")
|
||||
endif()
|
||||
set(DEVICE "ANDROID")
|
||||
elseif(EMSCRIPTEN)
|
||||
add_definitions(-D_IRR_EMSCRIPTEN_PLATFORM_ -D_IRR_COMPILE_WITH_EGL_MANAGER_)
|
||||
set(LINUX_PLATFORM TRUE)
|
||||
|
@ -131,7 +114,10 @@ endif()
|
|||
# OpenGL
|
||||
|
||||
if(USE_SDL2)
|
||||
option(ENABLE_OPENGL3 "Enable OpenGL 3+" TRUE)
|
||||
if(NOT ANDROID)
|
||||
set(DEFAULT_OPENGL3 TRUE)
|
||||
endif()
|
||||
option(ENABLE_OPENGL3 "Enable OpenGL 3+" ${DEFAULT_OPENGL3})
|
||||
else()
|
||||
set(ENABLE_OPENGL3 FALSE)
|
||||
endif()
|
||||
|
@ -142,13 +128,10 @@ else()
|
|||
option(ENABLE_OPENGL "Enable OpenGL" TRUE)
|
||||
endif()
|
||||
|
||||
if(EMSCRIPTEN OR APPLE)
|
||||
if(USE_SDL2 OR EMSCRIPTEN OR APPLE)
|
||||
set(ENABLE_GLES1 FALSE)
|
||||
else()
|
||||
if(ANDROID)
|
||||
set(DEFAULT_GLES1 TRUE)
|
||||
endif()
|
||||
option(ENABLE_GLES1 "Enable OpenGL ES" ${DEFAULT_GLES1})
|
||||
option(ENABLE_GLES1 "Enable OpenGL ES" FALSE)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
|
@ -188,19 +171,16 @@ if(ENABLE_OPENGL3)
|
|||
endif()
|
||||
|
||||
if(ENABLE_GLES1)
|
||||
if (USE_SDL2)
|
||||
message(FATAL_ERROR "OpenGL ES 1 is not supported with SDL2")
|
||||
endif()
|
||||
add_definitions(-D_IRR_COMPILE_WITH_OGLES1_)
|
||||
set(OPENGLES_DIRECT_LINK TRUE)
|
||||
if(DEVICE MATCHES "^(WINDOWS|X11|ANDROID)$")
|
||||
if(DEVICE MATCHES "^(WINDOWS|X11)$")
|
||||
add_definitions(-D_IRR_COMPILE_WITH_EGL_MANAGER_)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ENABLE_GLES2)
|
||||
add_definitions(-D_IRR_COMPILE_WITH_OGLES2_)
|
||||
if(DEVICE MATCHES "^(WINDOWS|X11|ANDROID)$" OR EMSCRIPTEN)
|
||||
if(DEVICE MATCHES "^(WINDOWS|X11)$" OR EMSCRIPTEN)
|
||||
add_definitions(-D_IRR_COMPILE_WITH_EGL_MANAGER_)
|
||||
endif()
|
||||
endif()
|
||||
|
@ -248,11 +228,15 @@ endif()
|
|||
if(ENABLE_GLES2)
|
||||
find_package(OpenGLES2 REQUIRED)
|
||||
endif()
|
||||
if(ENABLE_OPENGL OR ENABLE_OPENGL3)
|
||||
if(ENABLE_OPENGL)
|
||||
find_package(OpenGL REQUIRED)
|
||||
endif()
|
||||
if(USE_SDL2)
|
||||
find_package(SDL2 REQUIRED)
|
||||
if(NOT ANDROID)
|
||||
find_package(SDL2 REQUIRED)
|
||||
else()
|
||||
# provided by MinetestAndroidLibs.cmake
|
||||
endif()
|
||||
message(STATUS "Found SDL2: ${SDL2_LIBRARIES}")
|
||||
|
||||
# unfortunately older SDL does not provide its version to cmake, so check header.
|
||||
|
@ -323,7 +307,6 @@ set(link_includes
|
|||
${OPENGLES2_INCLUDE_DIR}
|
||||
${EGL_INCLUDE_DIR}
|
||||
|
||||
"$<$<PLATFORM_ID:Android>:${ANDROID_NDK}/sources/android/native_app_glue>"
|
||||
"$<$<BOOL:${USE_X11}>:${X11_INCLUDE_DIR}>"
|
||||
)
|
||||
|
||||
|
@ -453,14 +436,7 @@ if(ENABLE_OPENGL3)
|
|||
target_compile_definitions(IRROTHEROBJ PRIVATE ENABLE_OPENGL3)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
target_sources(IRROTHEROBJ PRIVATE
|
||||
Android/CIrrDeviceAndroid.cpp
|
||||
Android/CAndroidAssetReader.cpp
|
||||
Android/CAndroidAssetFileArchive.cpp
|
||||
Android/CKeyEventWrapper.cpp
|
||||
)
|
||||
elseif(APPLE)
|
||||
if(APPLE)
|
||||
# Build all IRROTHEROBJ sources as objc++, including the .cpp's
|
||||
set_target_properties(IRROTHEROBJ PROPERTIES COMPILE_OPTIONS "-xobjective-c++")
|
||||
target_sources(IRROTHEROBJ PRIVATE
|
||||
|
@ -501,7 +477,7 @@ add_library(IRRGUIOBJ OBJECT
|
|||
|
||||
# Library
|
||||
|
||||
add_library(IrrlichtMt)
|
||||
add_library(IrrlichtMt STATIC)
|
||||
foreach(object_lib
|
||||
IRRMESHOBJ IRROBJ IRRVIDEOOBJ
|
||||
IRRIOOBJ IRROTHEROBJ IRRGUIOBJ)
|
||||
|
@ -518,7 +494,6 @@ target_include_directories(IrrlichtMt
|
|||
PUBLIC
|
||||
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include/>"
|
||||
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
|
||||
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/irrlichtmt>"
|
||||
PRIVATE
|
||||
${link_includes}
|
||||
)
|
||||
|
@ -535,7 +510,8 @@ target_link_libraries(IrrlichtMt PRIVATE
|
|||
"$<$<BOOL:${OPENGLES_DIRECT_LINK}>:${OPENGLES_LIBRARY}>"
|
||||
${EGL_LIBRARY}
|
||||
|
||||
"$<$<PLATFORM_ID:Android>:-landroid -llog>"
|
||||
# incl. transitive SDL2 dependencies for static linking
|
||||
"$<$<PLATFORM_ID:Android>:-landroid -llog -lGLESv2 -lGLESv1_CM -lOpenSLES>"
|
||||
${COCOA_LIB}
|
||||
${IOKIT_LIB}
|
||||
"$<$<PLATFORM_ID:Windows>:gdi32>"
|
||||
|
@ -547,21 +523,6 @@ target_link_libraries(IrrlichtMt PRIVATE
|
|||
if(WIN32)
|
||||
target_compile_definitions(IrrlichtMt INTERFACE _IRR_WINDOWS_API_) # used in _IRR_DEBUG_BREAK_IF definition in a public header
|
||||
endif()
|
||||
target_compile_definitions(IrrlichtMt INTERFACE "IRRLICHT_API=${API_IMPORT}")
|
||||
if(APPLE OR ANDROID OR EMSCRIPTEN)
|
||||
target_compile_definitions(IrrlichtMt PUBLIC IRR_MOBILE_PATHS)
|
||||
endif()
|
||||
|
||||
set_target_properties(IrrlichtMt PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(IrrlichtMt PROPERTIES PREFIX "") # for DLL name
|
||||
endif()
|
||||
|
||||
# Installation of library
|
||||
install(TARGETS IrrlichtMt
|
||||
EXPORT IrrlichtMt-export
|
||||
DESTINATION "${CMAKE_INSTALL_LIBDIR}"
|
||||
)
|
||||
|
|
|
@ -983,6 +983,11 @@ IImage *CNullDriver::createImageFromFile(io::IReadFile *file)
|
|||
continue;
|
||||
|
||||
file->seek(0); // reset file position which might have changed due to previous loadImage calls
|
||||
// avoid warnings if extension is wrong
|
||||
if (!SurfaceLoader[i]->isALoadableFileFormat(file))
|
||||
continue;
|
||||
|
||||
file->seek(0);
|
||||
if (IImage *image = SurfaceLoader[i]->loadImage(file))
|
||||
return image;
|
||||
}
|
||||
|
|
|
@ -90,6 +90,9 @@ typedef char GLchar;
|
|||
// to check if this header is in the current compile unit (different GL implementation used different "GLCommon" headers in Irrlicht
|
||||
#define IRR_COMPILE_GLES_COMMON
|
||||
|
||||
// macro used with COGLES1Driver
|
||||
#define TEST_GL_ERROR(cls) (cls)->testGLError(__LINE__)
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace video
|
||||
|
|
|
@ -19,10 +19,6 @@
|
|||
#include "CImage.h"
|
||||
#include "os.h"
|
||||
|
||||
#ifdef _IRR_COMPILE_WITH_ANDROID_DEVICE_
|
||||
#include "android_native_app_glue.h"
|
||||
#endif
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace video
|
||||
|
@ -1181,7 +1177,9 @@ void COGLES1Driver::setMaterial(const SMaterial &material)
|
|||
//! prints error if an error happened.
|
||||
bool COGLES1Driver::testGLError(int code)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
if (!Params.DriverDebug)
|
||||
return false;
|
||||
|
||||
GLenum g = glGetError();
|
||||
switch (g) {
|
||||
case GL_NO_ERROR:
|
||||
|
@ -1205,11 +1203,7 @@ bool COGLES1Driver::testGLError(int code)
|
|||
os::Printer::log("GL_OUT_OF_MEMORY", core::stringc(code).c_str(), ELL_ERROR);
|
||||
break;
|
||||
};
|
||||
// _IRR_DEBUG_BREAK_IF(true);
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
//! sets the needed renderstates
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
#include "COGLESExtensionHandler.h"
|
||||
#include "IContextManager.h"
|
||||
|
||||
#define TEST_GL_ERROR(cls) (cls)->testGLError(__LINE__)
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace video
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
#include "SMaterial.h"
|
||||
#include "fast_atof.h"
|
||||
|
||||
#if defined(_IRR_COMPILE_WITH_ANDROID_DEVICE_) || defined(_IRR_COMPILE_WITH_WINDOWS_DEVICE_)
|
||||
#if defined(_IRR_COMPILE_WITH_WINDOWS_DEVICE_)
|
||||
#include <EGL/egl.h>
|
||||
#else
|
||||
#include <GLES/egl.h>
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
// To check if this header is in the current compile unit (different GL driver implementations use different "GLCommon" headers in Irrlicht)
|
||||
#define IRR_COMPILE_GL_COMMON
|
||||
|
||||
// macro used with COpenGLDriver
|
||||
#define TEST_GL_ERROR(cls) (cls)->testGLError(__LINE__)
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace video
|
||||
|
|
|
@ -174,9 +174,7 @@ public:
|
|||
AssignedTextures[i] = GL_COLOR_ATTACHMENT0 + i;
|
||||
GLenum textarget = currentTexture->getType() == ETT_2D ? GL_TEXTURE_2D : GL_TEXTURE_CUBE_MAP_POSITIVE_X + (int)CubeSurfaces[i];
|
||||
Driver->irrGlFramebufferTexture2D(GL_FRAMEBUFFER, AssignedTextures[i], textarget, textureID, 0);
|
||||
#ifdef _DEBUG
|
||||
Driver->testGLError(__LINE__);
|
||||
#endif
|
||||
TEST_GL_ERROR(Driver);
|
||||
} else if (AssignedTextures[i] != GL_NONE) {
|
||||
AssignedTextures[i] = GL_NONE;
|
||||
Driver->irrGlFramebufferTexture2D(GL_FRAMEBUFFER, AssignedTextures[i], GL_TEXTURE_2D, 0, 0);
|
||||
|
@ -239,9 +237,7 @@ public:
|
|||
AssignedDepth = false;
|
||||
AssignedStencil = false;
|
||||
}
|
||||
#ifdef _DEBUG
|
||||
Driver->testGLError(__LINE__);
|
||||
#endif
|
||||
TEST_GL_ERROR(Driver);
|
||||
|
||||
RequestDepthStencilUpdate = false;
|
||||
}
|
||||
|
@ -261,9 +257,7 @@ public:
|
|||
Driver->irrGlDrawBuffers(bufferCount, AssignedTextures.pointer());
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
Driver->testGLError(__LINE__);
|
||||
#endif
|
||||
TEST_GL_ERROR(Driver);
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
|
|
|
@ -59,6 +59,18 @@ public:
|
|||
if (!InternalFormat)
|
||||
return;
|
||||
|
||||
#ifdef _DEBUG
|
||||
char lbuf[128];
|
||||
snprintf_irr(lbuf, sizeof(lbuf),
|
||||
"COpenGLCoreTexture: Type = %d Size = %dx%d (%dx%d) ColorFormat = %d (%d)%s -> %#06x %#06x %#06x%s",
|
||||
(int)Type, Size.Width, Size.Height, OriginalSize.Width, OriginalSize.Height,
|
||||
(int)ColorFormat, (int)OriginalColorFormat,
|
||||
HasMipMaps ? " +Mip" : "",
|
||||
InternalFormat, PixelFormat, PixelType, Converter ? " (c)" : ""
|
||||
);
|
||||
os::Printer::log(lbuf, ELL_DEBUG);
|
||||
#endif
|
||||
|
||||
const core::array<IImage *> *tmpImages = &images;
|
||||
|
||||
if (KeepImage || OriginalSize != Size || OriginalColorFormat != ColorFormat) {
|
||||
|
@ -104,6 +116,8 @@ public:
|
|||
}
|
||||
#endif
|
||||
|
||||
TEST_GL_ERROR(Driver);
|
||||
|
||||
for (u32 i = 0; i < (*tmpImages).size(); ++i)
|
||||
uploadTexture(true, i, 0, (*tmpImages)[i]->getData());
|
||||
|
||||
|
@ -124,7 +138,7 @@ public:
|
|||
|
||||
Driver->getCacheHandler()->getTextureCache().set(0, prevTexture);
|
||||
|
||||
Driver->testGLError(__LINE__);
|
||||
TEST_GL_ERROR(Driver);
|
||||
}
|
||||
|
||||
COpenGLCoreTexture(const io::path &name, const core::dimension2d<u32> &size, E_TEXTURE_TYPE type, ECOLOR_FORMAT format, TOpenGLDriver *driver) :
|
||||
|
@ -155,6 +169,16 @@ public:
|
|||
return;
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
char lbuf[100];
|
||||
snprintf_irr(lbuf, sizeof(lbuf),
|
||||
"COpenGLCoreTexture: RTT Type = %d Size = %dx%d ColorFormat = %d -> %#06x %#06x %#06x%s",
|
||||
(int)Type, Size.Width, Size.Height, (int)ColorFormat,
|
||||
InternalFormat, PixelFormat, PixelType, Converter ? " (c)" : ""
|
||||
);
|
||||
os::Printer::log(lbuf, ELL_DEBUG);
|
||||
#endif
|
||||
|
||||
GL.GenTextures(1, &TextureName);
|
||||
|
||||
const COpenGLCoreTexture *prevTexture = Driver->getCacheHandler()->getTextureCache().get(0);
|
||||
|
@ -188,7 +212,7 @@ public:
|
|||
}
|
||||
|
||||
Driver->getCacheHandler()->getTextureCache().set(0, prevTexture);
|
||||
if (Driver->testGLError(__LINE__)) {
|
||||
if (TEST_GL_ERROR(Driver)) {
|
||||
char msg[256];
|
||||
snprintf_irr(msg, 256, "COpenGLCoreTexture: InternalFormat:0x%04x PixelFormat:0x%04x", (int)InternalFormat, (int)PixelFormat);
|
||||
os::Printer::log(msg, ELL_ERROR);
|
||||
|
@ -241,7 +265,7 @@ public:
|
|||
IImage *tmpImage = LockImage; // not sure yet if the size required by glGetTexImage is always correct, if not we might have to allocate a different tmpImage and convert colors later on.
|
||||
|
||||
Driver->getCacheHandler()->getTextureCache().set(0, this);
|
||||
Driver->testGLError(__LINE__);
|
||||
TEST_GL_ERROR(Driver);
|
||||
|
||||
GLenum tmpTextureType = TextureType;
|
||||
|
||||
|
@ -252,7 +276,7 @@ public:
|
|||
}
|
||||
|
||||
GL.GetTexImage(tmpTextureType, MipLevelStored, PixelFormat, PixelType, tmpImage->getData());
|
||||
Driver->testGLError(__LINE__);
|
||||
TEST_GL_ERROR(Driver);
|
||||
|
||||
if (IsRenderTarget && lockFlags == ETLF_FLIP_Y_UP_RTT) {
|
||||
const s32 pitch = tmpImage->getPitch();
|
||||
|
@ -334,7 +358,7 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
Driver->testGLError(__LINE__);
|
||||
TEST_GL_ERROR(Driver);
|
||||
}
|
||||
|
||||
return (LockImage) ? getLockImageData(MipLevelStored) : 0;
|
||||
|
@ -392,6 +416,7 @@ public:
|
|||
} while (width != 1 || height != 1);
|
||||
} else {
|
||||
Driver->irrGlGenerateMipmap(TextureType);
|
||||
TEST_GL_ERROR(Driver);
|
||||
}
|
||||
|
||||
Driver->getCacheHandler()->getTextureCache().set(0, prevTexture);
|
||||
|
@ -544,7 +569,7 @@ protected:
|
|||
GL.TexImage2D(tmpTextureType, level, InternalFormat, width, height, 0, PixelFormat, PixelType, tmpData);
|
||||
else
|
||||
GL.TexSubImage2D(tmpTextureType, level, 0, 0, width, height, PixelFormat, PixelType, tmpData);
|
||||
Driver->testGLError(__LINE__);
|
||||
TEST_GL_ERROR(Driver);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -561,7 +586,7 @@ protected:
|
|||
Driver->irrGlCompressedTexImage2D(tmpTextureType, level, InternalFormat, width, height, 0, dataSize, data);
|
||||
else
|
||||
Driver->irrGlCompressedTexSubImage2D(tmpTextureType, level, 0, 0, width, height, PixelFormat, dataSize, data);
|
||||
Driver->testGLError(__LINE__);
|
||||
TEST_GL_ERROR(Driver);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
@ -1642,7 +1642,9 @@ void COpenGLDriver::setMaterial(const SMaterial &material)
|
|||
//! prints error if an error happened.
|
||||
bool COpenGLDriver::testGLError(int code)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
if (!Params.DriverDebug)
|
||||
return false;
|
||||
|
||||
GLenum g = glGetError();
|
||||
switch (g) {
|
||||
case GL_NO_ERROR:
|
||||
|
@ -1674,11 +1676,7 @@ bool COpenGLDriver::testGLError(int code)
|
|||
break;
|
||||
#endif
|
||||
};
|
||||
// _IRR_DEBUG_BREAK_IF(true);
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
//! sets the needed renderstates
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#if defined(_IRR_COMPILE_WITH_SDL_DEVICE_)
|
||||
|
||||
#include "CIrrDeviceSDL.h"
|
||||
#include "COpenGLCommon.h"
|
||||
|
||||
namespace irr
|
||||
{
|
||||
|
|
|
@ -179,9 +179,8 @@ bool CWGLManager::initialize(const SIrrlichtCreationParameters ¶ms, const SE
|
|||
const bool pixel_format_supported = (wglExtensions.find("WGL_ARB_pixel_format") != -1);
|
||||
const bool multi_sample_supported = ((wglExtensions.find("WGL_ARB_multisample") != -1) ||
|
||||
(wglExtensions.find("WGL_EXT_multisample") != -1) || (wglExtensions.find("WGL_3DFX_multisample") != -1));
|
||||
#ifdef _DEBUG
|
||||
os::Printer::log("WGL_extensions", wglExtensions);
|
||||
#endif
|
||||
if (params.DriverDebug)
|
||||
os::Printer::log("WGL_extensions", wglExtensions);
|
||||
|
||||
// Without a GL context we can't call wglGetProcAddress so store this for later
|
||||
FunctionPointers[0] = (void *)wglGetProcAddress("wglCreateContextAttribsARB");
|
||||
|
|
|
@ -4,13 +4,6 @@
|
|||
|
||||
static const char *const copyright = "Irrlicht Engine (c) 2002-2017 Nikolaus Gebhardt"; // put string in binary
|
||||
|
||||
#ifdef _IRR_WINDOWS_
|
||||
#include <windows.h>
|
||||
#if defined(_DEBUG) && !defined(__GNUWIN32__)
|
||||
#include <crtdbg.h>
|
||||
#endif // _DEBUG
|
||||
#endif
|
||||
|
||||
#include "irrlicht.h"
|
||||
#ifdef _IRR_COMPILE_WITH_WINDOWS_DEVICE_
|
||||
#include "CIrrDeviceWin32.h"
|
||||
|
@ -28,10 +21,6 @@ static const char *const copyright = "Irrlicht Engine (c) 2002-2017 Nikolaus Geb
|
|||
#include "CIrrDeviceSDL.h"
|
||||
#endif
|
||||
|
||||
#ifdef _IRR_COMPILE_WITH_ANDROID_DEVICE_
|
||||
#include "Android/CIrrDeviceAndroid.h"
|
||||
#endif
|
||||
|
||||
namespace irr
|
||||
{
|
||||
//! stub for calling createDeviceEx
|
||||
|
@ -74,11 +63,6 @@ extern "C" IRRLICHT_API IrrlichtDevice *IRRCALLCONV createDeviceEx(const SIrrlic
|
|||
dev = new CIrrDeviceLinux(params);
|
||||
#endif
|
||||
|
||||
#ifdef _IRR_COMPILE_WITH_ANDROID_DEVICE_
|
||||
if (params.DeviceType == EIDT_ANDROID || (!dev && params.DeviceType == EIDT_BEST))
|
||||
dev = new CIrrDeviceAndroid(params);
|
||||
#endif
|
||||
|
||||
#ifdef _IRR_COMPILE_WITH_SDL_DEVICE_
|
||||
if (params.DeviceType == EIDT_SDL || (!dev && params.DeviceType == EIDT_BEST))
|
||||
dev = new CIrrDeviceSDL(params);
|
||||
|
@ -135,27 +119,3 @@ extern "C" IRRLICHT_API bool IRRCALLCONV isDriverSupported(E_DRIVER_TYPE driver)
|
|||
}
|
||||
|
||||
} // end namespace irr
|
||||
|
||||
#if defined(_IRR_WINDOWS_API_) && !defined(_IRR_STATIC_LIB_)
|
||||
|
||||
BOOL APIENTRY DllMain(HANDLE hModule,
|
||||
DWORD ul_reason_for_call,
|
||||
LPVOID lpReserved)
|
||||
{
|
||||
// _crtBreakAlloc = 139;
|
||||
|
||||
switch (ul_reason_for_call) {
|
||||
case DLL_PROCESS_ATTACH:
|
||||
#if defined(_DEBUG) && !defined(__GNUWIN32__)
|
||||
_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);
|
||||
#endif
|
||||
break;
|
||||
case DLL_THREAD_ATTACH:
|
||||
case DLL_THREAD_DETACH:
|
||||
case DLL_PROCESS_DETACH:
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
#endif // defined(_IRR_WINDOWS_)
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
#include "vendor/gl.h"
|
||||
#endif
|
||||
|
||||
// macro used with COpenGL3DriverBase
|
||||
#define TEST_GL_ERROR(cls) (cls)->testGLError(__FILE__, __LINE__)
|
||||
|
||||
namespace irr
|
||||
{
|
||||
namespace video
|
||||
|
|
|
@ -21,10 +21,6 @@
|
|||
#include "CImage.h"
|
||||
#include "os.h"
|
||||
|
||||
#ifdef _IRR_COMPILE_WITH_ANDROID_DEVICE_
|
||||
#include "android_native_app_glue.h"
|
||||
#endif
|
||||
|
||||
#include "mt_opengl.h"
|
||||
|
||||
namespace irr
|
||||
|
@ -138,7 +134,18 @@ void APIENTRY COpenGL3DriverBase::debugCb(GLenum source, GLenum type, GLuint id,
|
|||
|
||||
void COpenGL3DriverBase::debugCb(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message)
|
||||
{
|
||||
printf("%04x %04x %x %x %.*s\n", source, type, id, severity, length, message);
|
||||
// shader compiler can be very noisy
|
||||
if (source == GL_DEBUG_SOURCE_SHADER_COMPILER && severity == GL_DEBUG_SEVERITY_NOTIFICATION)
|
||||
return;
|
||||
|
||||
ELOG_LEVEL ll = ELL_INFORMATION;
|
||||
if (severity == GL_DEBUG_SEVERITY_HIGH)
|
||||
ll = ELL_ERROR;
|
||||
else if (severity == GL_DEBUG_SEVERITY_MEDIUM)
|
||||
ll = ELL_WARNING;
|
||||
char buf[256];
|
||||
snprintf_irr(buf, sizeof(buf), "%04x %04x %.*s", source, type, length, message);
|
||||
os::Printer::log("GL", buf, ll);
|
||||
}
|
||||
|
||||
COpenGL3DriverBase::COpenGL3DriverBase(const SIrrlichtCreationParameters ¶ms, io::IFileSystem *io, IContextManager *contextManager) :
|
||||
|
@ -147,7 +154,7 @@ COpenGL3DriverBase::COpenGL3DriverBase(const SIrrlichtCreationParameters ¶ms
|
|||
MaterialRenderer2DActive(0), MaterialRenderer2DTexture(0), MaterialRenderer2DNoTexture(0),
|
||||
CurrentRenderMode(ERM_NONE), Transformation3DChanged(true),
|
||||
OGLES2ShaderPath(params.OGLES2ShaderPath),
|
||||
ColorFormat(ECF_R8G8B8), ContextManager(contextManager)
|
||||
ColorFormat(ECF_R8G8B8), ContextManager(contextManager), EnableErrorTest(params.DriverDebug)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
setDebugName("Driver");
|
||||
|
@ -162,7 +169,10 @@ COpenGL3DriverBase::COpenGL3DriverBase(const SIrrlichtCreationParameters ¶ms
|
|||
ExposedData = ContextManager->getContext();
|
||||
ContextManager->activateContext(ExposedData, false);
|
||||
GL.LoadAllProcedures(ContextManager);
|
||||
GL.DebugMessageCallback(debugCb, this);
|
||||
if (EnableErrorTest) {
|
||||
GL.Enable(GL_DEBUG_OUTPUT);
|
||||
GL.DebugMessageCallback(debugCb, this);
|
||||
}
|
||||
initQuadsIndices();
|
||||
}
|
||||
|
||||
|
@ -284,7 +294,7 @@ bool COpenGL3DriverBase::genericDriverInit(const core::dimension2d<u32> &screenS
|
|||
// This fixes problems with intermediate changes to the material during texture load.
|
||||
ResetRenderStates = true;
|
||||
|
||||
testGLError(__LINE__);
|
||||
TEST_GL_ERROR(this);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -515,7 +525,7 @@ bool COpenGL3DriverBase::updateVertexHardwareBuffer(SHWBufferLink_opengl *HWBuff
|
|||
|
||||
GL.BindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
||||
return (!testGLError(__LINE__));
|
||||
return (!TEST_GL_ERROR(this));
|
||||
}
|
||||
|
||||
bool COpenGL3DriverBase::updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffer)
|
||||
|
@ -572,7 +582,7 @@ bool COpenGL3DriverBase::updateIndexHardwareBuffer(SHWBufferLink_opengl *HWBuffe
|
|||
|
||||
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
|
||||
return (!testGLError(__LINE__));
|
||||
return (!TEST_GL_ERROR(this));
|
||||
}
|
||||
|
||||
//! updates hardware buffer if needed
|
||||
|
@ -846,7 +856,7 @@ void COpenGL3DriverBase::draw2DImage(const video::ITexture *texture, const core:
|
|||
if (clipRect)
|
||||
GL.Disable(GL_SCISSOR_TEST);
|
||||
|
||||
testGLError(__LINE__);
|
||||
TEST_GL_ERROR(this);
|
||||
}
|
||||
|
||||
void COpenGL3DriverBase::draw2DImage(const video::ITexture *texture, u32 layer, bool flip)
|
||||
|
@ -1127,87 +1137,61 @@ void COpenGL3DriverBase::setMaterial(const SMaterial &material)
|
|||
}
|
||||
|
||||
//! prints error if an error happened.
|
||||
bool COpenGL3DriverBase::testGLError(int code)
|
||||
bool COpenGL3DriverBase::testGLError(const char *file, int line)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
if (!EnableErrorTest)
|
||||
return false;
|
||||
|
||||
GLenum g = GL.GetError();
|
||||
const char *err = nullptr;
|
||||
switch (g) {
|
||||
case GL_NO_ERROR:
|
||||
return false;
|
||||
case GL_INVALID_ENUM:
|
||||
os::Printer::log("GL_INVALID_ENUM", core::stringc(code).c_str(), ELL_ERROR);
|
||||
err = "GL_INVALID_ENUM";
|
||||
break;
|
||||
case GL_INVALID_VALUE:
|
||||
os::Printer::log("GL_INVALID_VALUE", core::stringc(code).c_str(), ELL_ERROR);
|
||||
err = "GL_INVALID_VALUE";
|
||||
break;
|
||||
case GL_INVALID_OPERATION:
|
||||
os::Printer::log("GL_INVALID_OPERATION", core::stringc(code).c_str(), ELL_ERROR);
|
||||
err = "GL_INVALID_OPERATION";
|
||||
break;
|
||||
case GL_STACK_OVERFLOW:
|
||||
err = "GL_STACK_OVERFLOW";
|
||||
break;
|
||||
case GL_STACK_UNDERFLOW:
|
||||
err = "GL_STACK_UNDERFLOW";
|
||||
break;
|
||||
case GL_OUT_OF_MEMORY:
|
||||
os::Printer::log("GL_OUT_OF_MEMORY", core::stringc(code).c_str(), ELL_ERROR);
|
||||
err = "GL_OUT_OF_MEMORY";
|
||||
break;
|
||||
case GL_INVALID_FRAMEBUFFER_OPERATION:
|
||||
err = "GL_INVALID_FRAMEBUFFER_OPERATION";
|
||||
break;
|
||||
#ifdef GL_VERSION_4_5
|
||||
case GL_CONTEXT_LOST:
|
||||
err = "GL_CONTEXT_LOST";
|
||||
break;
|
||||
};
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
//! prints error if an error happened.
|
||||
bool COpenGL3DriverBase::testEGLError()
|
||||
{
|
||||
#if defined(EGL_VERSION_1_0) && defined(_DEBUG)
|
||||
EGLint g = eglGetError();
|
||||
switch (g) {
|
||||
case EGL_SUCCESS:
|
||||
return false;
|
||||
case EGL_NOT_INITIALIZED:
|
||||
os::Printer::log("Not Initialized", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_ACCESS:
|
||||
os::Printer::log("Bad Access", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_ALLOC:
|
||||
os::Printer::log("Bad Alloc", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_ATTRIBUTE:
|
||||
os::Printer::log("Bad Attribute", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_CONTEXT:
|
||||
os::Printer::log("Bad Context", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_CONFIG:
|
||||
os::Printer::log("Bad Config", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_CURRENT_SURFACE:
|
||||
os::Printer::log("Bad Current Surface", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_DISPLAY:
|
||||
os::Printer::log("Bad Display", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_SURFACE:
|
||||
os::Printer::log("Bad Surface", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_MATCH:
|
||||
os::Printer::log("Bad Match", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_PARAMETER:
|
||||
os::Printer::log("Bad Parameter", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_NATIVE_PIXMAP:
|
||||
os::Printer::log("Bad Native Pixmap", ELL_ERROR);
|
||||
break;
|
||||
case EGL_BAD_NATIVE_WINDOW:
|
||||
os::Printer::log("Bad Native Window", ELL_ERROR);
|
||||
break;
|
||||
case EGL_CONTEXT_LOST:
|
||||
os::Printer::log("Context Lost", ELL_ERROR);
|
||||
break;
|
||||
};
|
||||
// Empty the error queue, see <https://www.khronos.org/opengl/wiki/OpenGL_Error>
|
||||
bool multiple = false;
|
||||
while (GL.GetError() != GL_NO_ERROR)
|
||||
multiple = true;
|
||||
|
||||
// basename
|
||||
for (char sep : {'/', '\\'}) {
|
||||
const char *tmp = strrchr(file, sep);
|
||||
if (tmp)
|
||||
file = tmp+1;
|
||||
}
|
||||
|
||||
char buf[80];
|
||||
snprintf_irr(buf, sizeof(buf), "%s %s:%d%s",
|
||||
err, file, line, multiple ? " (older errors exist)" : "");
|
||||
os::Printer::log(buf, ELL_ERROR);
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void COpenGL3DriverBase::setRenderStates3DMode()
|
||||
|
@ -1856,7 +1840,7 @@ IImage *COpenGL3DriverBase::createScreenShot(video::ECOLOR_FORMAT format, video:
|
|||
}
|
||||
|
||||
GL.ReadPixels(0, 0, ScreenSize.Width, ScreenSize.Height, internalformat, type, pixels);
|
||||
testGLError(__LINE__);
|
||||
TEST_GL_ERROR(this);
|
||||
|
||||
// opengl images are horizontally flipped, so we have to fix that here.
|
||||
const s32 pitch = newImage->getPitch();
|
||||
|
@ -1884,11 +1868,10 @@ IImage *COpenGL3DriverBase::createScreenShot(video::ECOLOR_FORMAT format, video:
|
|||
}
|
||||
}
|
||||
|
||||
if (testGLError(__LINE__)) {
|
||||
if (TEST_GL_ERROR(this)) {
|
||||
newImage->drop();
|
||||
return 0;
|
||||
}
|
||||
testGLError(__LINE__);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
|
|
|
@ -221,11 +221,9 @@ public:
|
|||
//! Returns an image created from the last rendered frame.
|
||||
IImage *createScreenShot(video::ECOLOR_FORMAT format = video::ECF_UNKNOWN, video::E_RENDER_TARGET target = video::ERT_FRAME_BUFFER) override;
|
||||
|
||||
//! checks if an OpenGL error has happened and prints it (+ some internal code which is usually the line number)
|
||||
bool testGLError(int code = 0);
|
||||
|
||||
//! checks if an OGLES1 error has happened and prints it
|
||||
bool testEGLError();
|
||||
//! checks if an OpenGL error has happened and prints it, use via TEST_GL_ERROR().
|
||||
// Does *nothing* unless in debug mode.
|
||||
bool testGLError(const char *file, int line);
|
||||
|
||||
//! Set/unset a clipping plane.
|
||||
bool setClipPlane(u32 index, const core::plane3df &plane, bool enable = false) override;
|
||||
|
@ -385,7 +383,7 @@ private:
|
|||
|
||||
void printTextureFormats();
|
||||
|
||||
void addDummyMaterial(E_MATERIAL_TYPE type);
|
||||
bool EnableErrorTest;
|
||||
|
||||
unsigned QuadIndexCount;
|
||||
GLuint QuadIndexBuffer = 0;
|
||||
|
|
|
@ -109,12 +109,6 @@ if(BUILD_CLIENT AND ENABLE_SOUND)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
option(ENABLE_TOUCH "Enable touchscreen by default" FALSE)
|
||||
if(ENABLE_TOUCH)
|
||||
message(STATUS "Touchscreen support enabled by default.")
|
||||
add_definitions(-DENABLE_TOUCH)
|
||||
endif()
|
||||
|
||||
if(BUILD_CLIENT)
|
||||
find_package(Freetype REQUIRED)
|
||||
endif()
|
||||
|
@ -272,9 +266,14 @@ if(WIN32)
|
|||
if(NOT VCPKG_APPLOCAL_DEPS)
|
||||
set(ZLIB_DLL "" CACHE FILEPATH "Path to Zlib DLL for installation (optional)")
|
||||
set(ZSTD_DLL "" CACHE FILEPATH "Path to Zstd DLL for installation (optional)")
|
||||
if(ENABLE_SOUND)
|
||||
set(OPENAL_DLL "" CACHE FILEPATH "Path to OpenAL32.dll for installation (optional)")
|
||||
set(OGG_DLL "" CACHE FILEPATH "Path to libogg.dll for installation (optional)")
|
||||
if(BUILD_CLIENT)
|
||||
set(PNG_DLL "" CACHE FILEPATH "Path to libpng DLL for installation (optional)")
|
||||
set(JPEG_DLL "" CACHE FILEPATH "Path to libjpeg DLL for installation (optional)")
|
||||
set(SDL2_DLL "" CACHE FILEPATH "Path to SDL2 DLL for installation (optional)")
|
||||
endif()
|
||||
if(BUILD_CLIENT AND ENABLE_SOUND)
|
||||
set(OPENAL_DLL "" CACHE FILEPATH "Path to OpenAL DLL for installation (optional)")
|
||||
set(OGG_DLL "" CACHE FILEPATH "Path to Ogg DLL for installation (optional)")
|
||||
set(VORBIS_DLL "" CACHE FILEPATH "Path to Vorbis DLLs for installation (optional)")
|
||||
endif()
|
||||
if(USE_GETTEXT)
|
||||
|
@ -307,10 +306,6 @@ else()
|
|||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
include_directories(${ANDROID_NDK}/sources/android/native_app_glue)
|
||||
add_library(native_app_glue OBJECT ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c)
|
||||
set(PLATFORM_LIBS ${PLATFORM_LIBS} native_app_glue)
|
||||
|
||||
set(PLATFORM_LIBS ${PLATFORM_LIBS} android log)
|
||||
endif()
|
||||
endif()
|
||||
|
@ -320,11 +315,13 @@ endif()
|
|||
# Note that find_library does not reliably find it so we have to resort to this.
|
||||
# Also, passing -latomic is not always the same as adding atomic to the library list.
|
||||
include(CheckCSourceCompiles)
|
||||
set(CMAKE_REQUIRED_LIBRARIES "-latomic")
|
||||
check_c_source_compiles("int main(){}" HAVE_LINK_ATOMIC)
|
||||
set(CMAKE_REQUIRED_LIBRARIES "")
|
||||
if(HAVE_LINK_ATOMIC)
|
||||
set(PLATFORM_LIBS ${PLATFORM_LIBS} "-latomic")
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||
set(CMAKE_REQUIRED_LIBRARIES "-latomic")
|
||||
check_c_source_compiles("int main(){}" HAVE_LINK_ATOMIC)
|
||||
set(CMAKE_REQUIRED_LIBRARIES "")
|
||||
if(HAVE_LINK_ATOMIC)
|
||||
set(PLATFORM_LIBS ${PLATFORM_LIBS} "-latomic")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(CheckSymbolExists)
|
||||
|
@ -518,6 +515,9 @@ include_directories(SYSTEM
|
|||
${GMP_INCLUDE_DIR}
|
||||
${JSON_INCLUDE_DIR}
|
||||
${LUA_BIT_INCLUDE_DIR}
|
||||
# on Android, Minetest depends on SDL2 directly
|
||||
# on other platforms, only IrrlichtMt depends on SDL2
|
||||
"$<$<PLATFORM_ID:Android>:${SDL2_INCLUDE_DIRS}>"
|
||||
)
|
||||
|
||||
if(USE_GETTEXT)
|
||||
|
@ -562,6 +562,9 @@ if(BUILD_CLIENT)
|
|||
${LUA_BIT_LIBRARY}
|
||||
${FREETYPE_LIBRARY}
|
||||
${PLATFORM_LIBS}
|
||||
# on Android, Minetest depends on SDL2 directly
|
||||
# on other platforms, only IrrlichtMt depends on SDL2
|
||||
"$<$<PLATFORM_ID:Android>:${SDL2_LIBRARIES}>"
|
||||
)
|
||||
if(NOT USE_LUAJIT)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
|
@ -860,6 +863,20 @@ if(WIN32)
|
|||
install(FILES ${VORBIS_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
endif()
|
||||
if(BUILD_CLIENT)
|
||||
if(PNG_DLL)
|
||||
install(FILES ${PNG_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
if(JPEG_DLL)
|
||||
install(FILES ${JPEG_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
if(SDL2_DLL)
|
||||
install(FILES ${SDL2_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
if(FREETYPE_DLL)
|
||||
install(FILES ${FREETYPE_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
endif()
|
||||
if(CURL_DLL)
|
||||
install(FILES ${CURL_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
|
@ -869,9 +886,6 @@ if(WIN32)
|
|||
if(ZSTD_DLL)
|
||||
install(FILES ${ZSTD_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
if(BUILD_CLIENT AND FREETYPE_DLL)
|
||||
install(FILES ${FREETYPE_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
if(SQLITE3_DLL)
|
||||
install(FILES ${SQLITE3_DLL} DESTINATION ${BINDIR})
|
||||
endif()
|
||||
|
|
|
@ -799,7 +799,7 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
|
|||
video::IVideoDriver *vdrv = m_rendering_engine->get_video_driver();
|
||||
|
||||
io::IReadFile *rfile = irrfs->createMemoryReadFile(
|
||||
data.c_str(), data.size(), "_tempreadfile");
|
||||
data.c_str(), data.size(), filename.c_str());
|
||||
|
||||
FATAL_ERROR_IF(!rfile, "Could not create irrlicht memory file.");
|
||||
|
||||
|
|
|
@ -47,11 +47,6 @@ gui::IGUIEnvironment *guienv = nullptr;
|
|||
gui::IGUIStaticText *guiroot = nullptr;
|
||||
MainMenuManager g_menumgr;
|
||||
|
||||
bool isMenuActive()
|
||||
{
|
||||
return g_menumgr.menuCount() != 0;
|
||||
}
|
||||
|
||||
// Passed to menus to allow disconnecting and exiting
|
||||
MainGameCallback *g_gamecallback = nullptr;
|
||||
|
||||
|
@ -74,13 +69,20 @@ ClientLauncher::~ClientLauncher()
|
|||
{
|
||||
delete input;
|
||||
|
||||
delete receiver;
|
||||
|
||||
delete g_fontengine;
|
||||
g_fontengine = nullptr;
|
||||
delete g_gamecallback;
|
||||
g_gamecallback = nullptr;
|
||||
|
||||
guiroot = nullptr;
|
||||
guienv = nullptr;
|
||||
assert(g_menumgr.menuCount() == 0);
|
||||
|
||||
delete m_rendering_engine;
|
||||
|
||||
// delete event receiver only after all Irrlicht stuff is gone
|
||||
delete receiver;
|
||||
|
||||
#if USE_SOUND
|
||||
g_sound_manager_singleton.reset();
|
||||
#endif
|
||||
|
@ -103,10 +105,8 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
|
|||
g_sound_manager_singleton = createSoundManagerSingleton();
|
||||
#endif
|
||||
|
||||
if (!init_engine()) {
|
||||
errorstream << "Could not initialize game engine." << std::endl;
|
||||
if (!init_engine())
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_rendering_engine->get_video_driver()) {
|
||||
errorstream << "Could not initialize video driver." << std::endl;
|
||||
|
@ -129,7 +129,6 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
|
|||
init_guienv(guienv);
|
||||
|
||||
g_fontengine = new FontEngine(guienv);
|
||||
FATAL_ERROR_IF(!g_fontengine, "Font engine creation failed.");
|
||||
|
||||
// Create the menu clouds
|
||||
// This is only global so it can be used by RenderingEngine::draw_load_screen().
|
||||
|
@ -173,8 +172,9 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
|
|||
m_rendering_engine->get_raw_device()->
|
||||
setWindowCaption(utf8_to_wide(caption).c_str());
|
||||
|
||||
try { // This is used for catching disconnects
|
||||
|
||||
#ifdef NDEBUG
|
||||
try {
|
||||
#endif
|
||||
m_rendering_engine->get_gui_env()->clear();
|
||||
|
||||
/*
|
||||
|
@ -206,10 +206,6 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
|
|||
if (!m_rendering_engine->run() || *kill)
|
||||
break;
|
||||
|
||||
if (g_settings->getBool("enable_touch")) {
|
||||
g_touchscreengui = new TouchScreenGUI(m_rendering_engine->get_raw_device(), receiver);
|
||||
}
|
||||
|
||||
the_game(
|
||||
kill,
|
||||
input,
|
||||
|
@ -219,18 +215,8 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
|
|||
chat_backend,
|
||||
&reconnect_requested
|
||||
);
|
||||
} //try
|
||||
catch (con::PeerNotFoundException &e) {
|
||||
error_message = gettext("Connection error (timed out?)");
|
||||
errorstream << error_message << std::endl;
|
||||
}
|
||||
catch (ShaderException &e) {
|
||||
error_message = e.what();
|
||||
errorstream << error_message << std::endl;
|
||||
}
|
||||
|
||||
#ifdef NDEBUG
|
||||
catch (std::exception &e) {
|
||||
} catch (std::exception &e) {
|
||||
error_message = "Some exception: ";
|
||||
error_message.append(debug_describe_exc(e));
|
||||
errorstream << error_message << std::endl;
|
||||
|
@ -262,6 +248,13 @@ bool ClientLauncher::run(GameStartData &start_data, const Settings &cmd_args)
|
|||
}
|
||||
} // Menu-game loop
|
||||
|
||||
// If profiler was enabled print it one last time
|
||||
if (g_settings->getFloat("profiler_print_interval") > 0) {
|
||||
infostream << "Profiler:" << std::endl;
|
||||
g_profiler->print(infostream);
|
||||
g_profiler->clear();
|
||||
}
|
||||
|
||||
assert(g_menucloudsmgr->getReferenceCount() == 1);
|
||||
g_menucloudsmgr->drop();
|
||||
g_menucloudsmgr = nullptr;
|
||||
|
@ -298,8 +291,12 @@ void ClientLauncher::init_args(GameStartData &start_data, const Settings &cmd_ar
|
|||
bool ClientLauncher::init_engine()
|
||||
{
|
||||
receiver = new MyEventReceiver();
|
||||
m_rendering_engine = new RenderingEngine(receiver);
|
||||
return m_rendering_engine->get_raw_device() != nullptr;
|
||||
try {
|
||||
m_rendering_engine = new RenderingEngine(receiver);
|
||||
} catch (std::exception &e) {
|
||||
errorstream << e.what() << std::endl;
|
||||
}
|
||||
return !!m_rendering_engine;
|
||||
}
|
||||
|
||||
void ClientLauncher::init_input()
|
||||
|
|
|
@ -33,6 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "camera.h" // CameraModes
|
||||
#include "collision.h"
|
||||
#include "content_cso.h"
|
||||
#include "clientobject.h"
|
||||
#include "environment.h"
|
||||
#include "itemdef.h"
|
||||
#include "localplayer.h"
|
||||
|
@ -218,14 +219,15 @@ private:
|
|||
};
|
||||
|
||||
// Prototype
|
||||
TestCAO proto_TestCAO(NULL, NULL);
|
||||
static TestCAO proto_TestCAO(nullptr, nullptr);
|
||||
|
||||
TestCAO::TestCAO(Client *client, ClientEnvironment *env):
|
||||
ClientActiveObject(0, client, env),
|
||||
m_node(NULL),
|
||||
m_position(v3f(0,10*BS,0))
|
||||
{
|
||||
ClientActiveObject::registerType(getType(), create);
|
||||
if (!client)
|
||||
ClientActiveObject::registerType(getType(), create);
|
||||
}
|
||||
|
||||
std::unique_ptr<ClientActiveObject> TestCAO::create(Client *client, ClientEnvironment *env)
|
||||
|
@ -322,8 +324,6 @@ void TestCAO::processMessage(const std::string &data)
|
|||
GenericCAO
|
||||
*/
|
||||
|
||||
#include "clientobject.h"
|
||||
|
||||
GenericCAO::GenericCAO(Client *client, ClientEnvironment *env):
|
||||
ClientActiveObject(0, client, env)
|
||||
{
|
||||
|
@ -2082,4 +2082,4 @@ void GenericCAO::updateMeshCulling()
|
|||
}
|
||||
|
||||
// Prototype
|
||||
GenericCAO proto_GenericCAO(NULL, NULL);
|
||||
static GenericCAO proto_GenericCAO(nullptr, nullptr);
|
||||
|
|
|
@ -1534,8 +1534,10 @@ void MapblockMeshGenerator::drawNodeboxNode()
|
|||
bool param2_is_rotation =
|
||||
cur_node.f->param_type_2 == CPT2_COLORED_FACEDIR ||
|
||||
cur_node.f->param_type_2 == CPT2_COLORED_WALLMOUNTED ||
|
||||
cur_node.f->param_type_2 == CPT2_COLORED_4DIR ||
|
||||
cur_node.f->param_type_2 == CPT2_FACEDIR ||
|
||||
cur_node.f->param_type_2 == CPT2_WALLMOUNTED;
|
||||
cur_node.f->param_type_2 == CPT2_WALLMOUNTED ||
|
||||
cur_node.f->param_type_2 == CPT2_4DIR;
|
||||
|
||||
bool param2_is_level =
|
||||
cur_node.f->param_type_2 == CPT2_LEVELED;
|
||||
|
|
|
@ -27,16 +27,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "irrlicht_changes/CGUITTFont.h"
|
||||
#include "util/numeric.h" // rangelim
|
||||
|
||||
/** maximum size distance for getting a "similar" font size */
|
||||
#define MAX_FONT_SIZE_OFFSET 10
|
||||
|
||||
/** reference to access font engine, has to be initialized by main */
|
||||
FontEngine* g_fontengine = NULL;
|
||||
FontEngine *g_fontengine = nullptr;
|
||||
|
||||
/** callback to be used on change of font size setting */
|
||||
static void font_setting_changed(const std::string &name, void *userdata)
|
||||
{
|
||||
g_fontengine->readSettings();
|
||||
if (g_fontengine)
|
||||
g_fontengine->readSettings();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -226,7 +224,7 @@ gui::IGUIFont *FontEngine::initFont(const FontSpec &spec)
|
|||
u16 divisible_by = g_settings->getU16(setting_prefix + "font_size_divisible_by");
|
||||
if (divisible_by > 1) {
|
||||
size = std::max<u32>(
|
||||
std::round((double)size / divisible_by) * divisible_by, divisible_by);
|
||||
std::round((float)size / divisible_by) * divisible_by, divisible_by);
|
||||
}
|
||||
|
||||
sanity_check(size != 0);
|
||||
|
|
|
@ -1015,6 +1015,12 @@ Game::Game() :
|
|||
|
||||
Game::~Game()
|
||||
{
|
||||
delete client;
|
||||
delete soundmaker;
|
||||
sound_manager.reset();
|
||||
|
||||
delete server;
|
||||
|
||||
delete hud;
|
||||
delete camera;
|
||||
delete quicktune;
|
||||
|
@ -1132,9 +1138,11 @@ void Game::run()
|
|||
FpsControl draw_times;
|
||||
f32 dtime; // in seconds
|
||||
|
||||
/* Clear the profiler */
|
||||
Profiler::GraphValues dummyvalues;
|
||||
g_profiler->graphGet(dummyvalues);
|
||||
// Clear the profiler
|
||||
{
|
||||
Profiler::GraphValues dummyvalues;
|
||||
g_profiler->graphPop(dummyvalues);
|
||||
}
|
||||
|
||||
draw_times.reset();
|
||||
|
||||
|
@ -1265,11 +1273,14 @@ void Game::shutdown()
|
|||
}
|
||||
|
||||
delete client;
|
||||
client = nullptr;
|
||||
delete soundmaker;
|
||||
soundmaker = nullptr;
|
||||
sound_manager.reset();
|
||||
|
||||
auto stop_thread = runInThread([=] {
|
||||
delete server;
|
||||
server = nullptr;
|
||||
}, "ServerStop");
|
||||
|
||||
FpsControl fps_control;
|
||||
|
@ -1555,8 +1566,8 @@ bool Game::initGui()
|
|||
gui_chat_console = new GUIChatConsole(guienv, guienv->getRootGUIElement(),
|
||||
-1, chat_backend, client, &g_menumgr);
|
||||
|
||||
if (g_touchscreengui)
|
||||
g_touchscreengui->init(texture_src);
|
||||
if (g_settings->getBool("enable_touch"))
|
||||
g_touchscreengui = new TouchScreenGUI(device, texture_src);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1902,7 +1913,8 @@ void Game::updateProfilers(const RunStats &stats, const FpsControl &draw_times,
|
|||
g_settings->getFloat("profiler_print_interval");
|
||||
bool print_to_log = true;
|
||||
|
||||
if (profiler_print_interval == 0) {
|
||||
// Update game UI anyway but don't log
|
||||
if (profiler_print_interval <= 0) {
|
||||
print_to_log = false;
|
||||
profiler_print_interval = 3;
|
||||
}
|
||||
|
@ -1917,12 +1929,12 @@ void Game::updateProfilers(const RunStats &stats, const FpsControl &draw_times,
|
|||
g_profiler->clear();
|
||||
}
|
||||
|
||||
// Update update graphs
|
||||
// Update graphs
|
||||
g_profiler->graphAdd("Time non-rendering [us]",
|
||||
draw_times.busy_time - stats.drawtime);
|
||||
|
||||
g_profiler->graphAdd("Sleep [us]", draw_times.sleep_time);
|
||||
g_profiler->graphAdd("FPS", 1.0f / dtime);
|
||||
|
||||
g_profiler->graphSet("FPS", 1.0f / dtime);
|
||||
}
|
||||
|
||||
void Game::updateStats(RunStats *stats, const FpsControl &draw_times,
|
||||
|
@ -2258,9 +2270,11 @@ void Game::openConsole(float scale, const wchar_t *line)
|
|||
assert(scale > 0.0f && scale <= 1.0f);
|
||||
|
||||
#ifdef __ANDROID__
|
||||
porting::showTextInputDialog("", "", 2);
|
||||
m_android_chat_open = true;
|
||||
#else
|
||||
if (!porting::hasPhysicalKeyboardAndroid()) {
|
||||
porting::showTextInputDialog("", "", 2);
|
||||
m_android_chat_open = true;
|
||||
} else {
|
||||
#endif
|
||||
if (gui_chat_console->isOpenInhibited())
|
||||
return;
|
||||
gui_chat_console->openConsole(scale);
|
||||
|
@ -2268,6 +2282,8 @@ void Game::openConsole(float scale, const wchar_t *line)
|
|||
gui_chat_console->setCloseOnEnter(true);
|
||||
gui_chat_console->replaceAndAddToHistory(line);
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
} // else
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -4225,7 +4241,7 @@ void Game::updateClouds(float dtime)
|
|||
inline void Game::updateProfilerGraphs(ProfilerGraph *graph)
|
||||
{
|
||||
Profiler::GraphValues values;
|
||||
g_profiler->graphGet(values);
|
||||
g_profiler->graphPop(values);
|
||||
graph->put(values);
|
||||
}
|
||||
|
||||
|
@ -4554,6 +4570,13 @@ void the_game(bool *kill,
|
|||
error_message = std::string("ModError: ") + e.what() +
|
||||
strgettext("\nCheck debug.txt for details.");
|
||||
errorstream << error_message << std::endl;
|
||||
} catch (con::PeerNotFoundException &e) {
|
||||
error_message = gettext("Connection error (timed out?)");
|
||||
errorstream << error_message << std::endl;
|
||||
} catch (ShaderException &e) {
|
||||
error_message = e.what();
|
||||
errorstream << error_message << std::endl;
|
||||
}
|
||||
|
||||
game.shutdown();
|
||||
}
|
||||
|
|
|
@ -29,12 +29,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
* converting textures back into images repeatedly, and some don't even
|
||||
* allow it at all.
|
||||
*/
|
||||
std::map<io::path, video::IImage *> g_imgCache;
|
||||
static std::map<io::path, video::IImage *> g_imgCache;
|
||||
|
||||
/* Maintain a static cache of all pre-scaled textures. These need to be
|
||||
* cleared as well when the cached images.
|
||||
*/
|
||||
std::map<io::path, video::ITexture *> g_txrCache;
|
||||
static std::map<io::path, video::ITexture *> g_txrCache;
|
||||
|
||||
/* Manually insert an image into the cache, useful to avoid texture-to-image
|
||||
* conversion whenever we can intercept it.
|
||||
|
|
|
@ -40,6 +40,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "client/renderingengine.h"
|
||||
#include "client/minimap.h"
|
||||
#include "gui/touchscreengui.h"
|
||||
#include "util/enriched_string.h"
|
||||
#include "irrlicht_changes/CGUITTFont.h"
|
||||
|
||||
#define OBJECT_CROSSHAIR_LINE_SIZE 8
|
||||
#define CROSSHAIR_LINE_SIZE 10
|
||||
|
@ -390,10 +392,14 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
|
|||
(e->style & HUD_STYLE_MONO) ? FM_Mono : FM_Unspecified,
|
||||
e->style & HUD_STYLE_BOLD, e->style & HUD_STYLE_ITALIC));
|
||||
|
||||
irr::gui::CGUITTFont *ttfont = nullptr;
|
||||
if (textfont->getType() == irr::gui::EGFT_CUSTOM)
|
||||
ttfont = static_cast<irr::gui::CGUITTFont *>(textfont);
|
||||
|
||||
video::SColor color(255, (e->number >> 16) & 0xFF,
|
||||
(e->number >> 8) & 0xFF,
|
||||
(e->number >> 0) & 0xFF);
|
||||
std::wstring text = unescape_translate(utf8_to_wide(e->text));
|
||||
EnrichedString text(unescape_string(utf8_to_wide(e->text)), color);
|
||||
core::dimension2d<u32> textsize = textfont->getDimension(text.c_str());
|
||||
|
||||
v2s32 offset(0, (e->align.Y - 1.0) * (textsize.Height / 2));
|
||||
|
@ -401,13 +407,19 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
|
|||
text_height * e->scale.Y * m_scale_factor);
|
||||
v2s32 offs(e->offset.X * m_scale_factor,
|
||||
e->offset.Y * m_scale_factor);
|
||||
std::wstringstream wss(text);
|
||||
std::wstring line;
|
||||
while (std::getline(wss, line, L'\n'))
|
||||
{
|
||||
|
||||
// Draw each line
|
||||
// See also: GUIFormSpecMenu::parseLabel
|
||||
size_t str_pos = 0;
|
||||
while (str_pos < text.size()) {
|
||||
EnrichedString line = text.getNextLine(&str_pos);
|
||||
|
||||
core::dimension2d<u32> linesize = textfont->getDimension(line.c_str());
|
||||
v2s32 line_offset((e->align.X - 1.0) * (linesize.Width / 2), 0);
|
||||
textfont->draw(line.c_str(), size + pos + offset + offs + line_offset, color);
|
||||
if (ttfont)
|
||||
ttfont->draw(line, size + pos + offset + offs + line_offset);
|
||||
else
|
||||
textfont->draw(line.c_str(), size + pos + offset + offs + line_offset, color);
|
||||
offset.Y += linesize.Height;
|
||||
}
|
||||
break; }
|
||||
|
@ -1034,8 +1046,7 @@ void drawItemStack(
|
|||
return;
|
||||
}
|
||||
|
||||
const static thread_local bool enable_animations =
|
||||
g_settings->getBool("inventory_items_animations");
|
||||
const bool enable_animations = g_settings->getBool("inventory_items_animations");
|
||||
|
||||
auto *idef = client->idef();
|
||||
const ItemDefinition &def = item.getDefinition(idef);
|
||||
|
|
|
@ -1704,7 +1704,7 @@ bool ImageSource::generateImagePart(std::string_view part_of_name,
|
|||
auto *device = RenderingEngine::get_raw_device();
|
||||
auto *fs = device->getFileSystem();
|
||||
auto *vd = device->getVideoDriver();
|
||||
auto *memfile = fs->createMemoryReadFile(png.data(), png.size(), "__temp_png");
|
||||
auto *memfile = fs->createMemoryReadFile(png.data(), png.size(), "[png_tmpfile");
|
||||
video::IImage* pngimg = vd->createImageFromFile(memfile);
|
||||
memfile->drop();
|
||||
|
||||
|
|
|
@ -99,19 +99,30 @@ void KeyCache::populate()
|
|||
|
||||
bool MyEventReceiver::OnEvent(const SEvent &event)
|
||||
{
|
||||
/*
|
||||
React to nothing here if a menu is active
|
||||
*/
|
||||
if (event.EventType == irr::EET_LOG_TEXT_EVENT) {
|
||||
static const LogLevel irr_loglev_conv[] = {
|
||||
LL_VERBOSE, // ELL_DEBUG
|
||||
LL_INFO, // ELL_INFORMATION
|
||||
LL_WARNING, // ELL_WARNING
|
||||
LL_ERROR, // ELL_ERROR
|
||||
LL_NONE, // ELL_NONE
|
||||
};
|
||||
assert(event.LogEvent.Level < ARRLEN(irr_loglev_conv));
|
||||
g_logger.log(irr_loglev_conv[event.LogEvent.Level],
|
||||
std::string("Irrlicht: ") + event.LogEvent.Text);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Let the menu handle events, if one is active.
|
||||
if (isMenuActive()) {
|
||||
if (g_touchscreengui) {
|
||||
if (g_touchscreengui)
|
||||
g_touchscreengui->setVisible(false);
|
||||
}
|
||||
return g_menumgr.preprocessEvent(event);
|
||||
}
|
||||
|
||||
// Remember whether each key is down or up
|
||||
if (event.EventType == irr::EET_KEY_INPUT_EVENT) {
|
||||
const KeyPress &keyCode = event.KeyInput;
|
||||
const KeyPress keyCode(event.KeyInput);
|
||||
if (keysListenedFor[keyCode]) {
|
||||
if (event.KeyInput.PressedDown) {
|
||||
if (!IsKeyDown(keyCode))
|
||||
|
@ -133,66 +144,48 @@ bool MyEventReceiver::OnEvent(const SEvent &event)
|
|||
// In case of touchscreengui, we have to handle different events
|
||||
g_touchscreengui->translateEvent(event);
|
||||
return true;
|
||||
|
||||
} else if (event.EventType == irr::EET_JOYSTICK_INPUT_EVENT) {
|
||||
// joystick may be nullptr if game is launched with '--random-input' parameter
|
||||
return joystick && joystick->handleEvent(event.JoystickEvent);
|
||||
} else if (event.EventType == irr::EET_MOUSE_INPUT_EVENT) {
|
||||
// Handle mouse events
|
||||
KeyPress key;
|
||||
switch (event.MouseInput.Event) {
|
||||
case EMIE_LMOUSE_PRESSED_DOWN:
|
||||
key = "KEY_LBUTTON";
|
||||
keyIsDown.set(key);
|
||||
keyWasDown.set(key);
|
||||
keyWasPressed.set(key);
|
||||
keyIsDown.set(LMBKey);
|
||||
keyWasDown.set(LMBKey);
|
||||
keyWasPressed.set(LMBKey);
|
||||
break;
|
||||
case EMIE_MMOUSE_PRESSED_DOWN:
|
||||
key = "KEY_MBUTTON";
|
||||
keyIsDown.set(key);
|
||||
keyWasDown.set(key);
|
||||
keyWasPressed.set(key);
|
||||
keyIsDown.set(MMBKey);
|
||||
keyWasDown.set(MMBKey);
|
||||
keyWasPressed.set(MMBKey);
|
||||
break;
|
||||
case EMIE_RMOUSE_PRESSED_DOWN:
|
||||
key = "KEY_RBUTTON";
|
||||
keyIsDown.set(key);
|
||||
keyWasDown.set(key);
|
||||
keyWasPressed.set(key);
|
||||
keyIsDown.set(RMBKey);
|
||||
keyWasDown.set(RMBKey);
|
||||
keyWasPressed.set(RMBKey);
|
||||
break;
|
||||
case EMIE_LMOUSE_LEFT_UP:
|
||||
key = "KEY_LBUTTON";
|
||||
keyIsDown.unset(key);
|
||||
keyWasReleased.set(key);
|
||||
keyIsDown.unset(LMBKey);
|
||||
keyWasReleased.set(LMBKey);
|
||||
break;
|
||||
case EMIE_MMOUSE_LEFT_UP:
|
||||
key = "KEY_MBUTTON";
|
||||
keyIsDown.unset(key);
|
||||
keyWasReleased.set(key);
|
||||
keyIsDown.unset(MMBKey);
|
||||
keyWasReleased.set(MMBKey);
|
||||
break;
|
||||
case EMIE_RMOUSE_LEFT_UP:
|
||||
key = "KEY_RBUTTON";
|
||||
keyIsDown.unset(key);
|
||||
keyWasReleased.set(key);
|
||||
keyIsDown.unset(RMBKey);
|
||||
keyWasReleased.set(RMBKey);
|
||||
break;
|
||||
case EMIE_MOUSE_WHEEL:
|
||||
mouse_wheel += event.MouseInput.Wheel;
|
||||
break;
|
||||
default: break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (event.EventType == irr::EET_LOG_TEXT_EVENT) {
|
||||
static const LogLevel irr_loglev_conv[] = {
|
||||
LL_VERBOSE, // ELL_DEBUG
|
||||
LL_INFO, // ELL_INFORMATION
|
||||
LL_WARNING, // ELL_WARNING
|
||||
LL_ERROR, // ELL_ERROR
|
||||
LL_NONE, // ELL_NONE
|
||||
};
|
||||
assert(event.LogEvent.Level < ARRLEN(irr_loglev_conv));
|
||||
g_logger.log(irr_loglev_conv[event.LogEvent.Level],
|
||||
std::string("Irrlicht: ") + event.LogEvent.Text);
|
||||
return true;
|
||||
}
|
||||
/* always return false in order to continue processing events */
|
||||
|
||||
// tell Irrlicht to continue processing this event
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -239,7 +239,7 @@ static const struct table_key table[] = {
|
|||
#undef N_
|
||||
|
||||
|
||||
struct table_key lookup_keyname(const char *name)
|
||||
static const table_key &lookup_keyname(const char *name)
|
||||
{
|
||||
for (const auto &table_key : table) {
|
||||
if (strcmp(table_key.Name, name) == 0)
|
||||
|
@ -249,7 +249,7 @@ struct table_key lookup_keyname(const char *name)
|
|||
throw UnknownKeycode(name);
|
||||
}
|
||||
|
||||
struct table_key lookup_keykey(irr::EKEY_CODE key)
|
||||
static const table_key &lookup_keykey(irr::EKEY_CODE key)
|
||||
{
|
||||
for (const auto &table_key : table) {
|
||||
if (table_key.Key == key)
|
||||
|
@ -261,7 +261,7 @@ struct table_key lookup_keykey(irr::EKEY_CODE key)
|
|||
throw UnknownKeycode(os.str().c_str());
|
||||
}
|
||||
|
||||
struct table_key lookup_keychar(wchar_t Char)
|
||||
static const table_key &lookup_keychar(wchar_t Char)
|
||||
{
|
||||
for (const auto &table_key : table) {
|
||||
if (table_key.Char == Char)
|
||||
|
@ -287,7 +287,7 @@ KeyPress::KeyPress(const char *name)
|
|||
int chars_read = mbtowc(&Char, name, 1);
|
||||
FATAL_ERROR_IF(chars_read != 1, "Unexpected multibyte character");
|
||||
try {
|
||||
struct table_key k = lookup_keychar(Char);
|
||||
auto &k = lookup_keychar(Char);
|
||||
m_name = k.Name;
|
||||
Key = k.Key;
|
||||
return;
|
||||
|
@ -296,7 +296,7 @@ KeyPress::KeyPress(const char *name)
|
|||
// Lookup by name
|
||||
m_name = name;
|
||||
try {
|
||||
struct table_key k = lookup_keyname(name);
|
||||
auto &k = lookup_keyname(name);
|
||||
Key = k.Key;
|
||||
Char = k.Char;
|
||||
return;
|
||||
|
@ -350,23 +350,26 @@ const char *KeyPress::name() const
|
|||
const KeyPress EscapeKey("KEY_ESCAPE");
|
||||
const KeyPress CancelKey("KEY_CANCEL");
|
||||
|
||||
const KeyPress LMBKey("KEY_LBUTTON");
|
||||
const KeyPress MMBKey("KEY_MBUTTON");
|
||||
const KeyPress RMBKey("KEY_RBUTTON");
|
||||
|
||||
/*
|
||||
Key config
|
||||
*/
|
||||
|
||||
// A simple cache for quicker lookup
|
||||
std::unordered_map<std::string, KeyPress> g_key_setting_cache;
|
||||
static std::unordered_map<std::string, KeyPress> g_key_setting_cache;
|
||||
|
||||
KeyPress getKeySetting(const char *settingname)
|
||||
const KeyPress &getKeySetting(const char *settingname)
|
||||
{
|
||||
std::unordered_map<std::string, KeyPress>::iterator n;
|
||||
n = g_key_setting_cache.find(settingname);
|
||||
auto n = g_key_setting_cache.find(settingname);
|
||||
if (n != g_key_setting_cache.end())
|
||||
return n->second;
|
||||
|
||||
KeyPress k(g_settings->get(settingname).c_str());
|
||||
g_key_setting_cache[settingname] = k;
|
||||
return k;
|
||||
auto &ref = g_key_setting_cache[settingname];
|
||||
ref = g_settings->get(settingname).c_str();
|
||||
return ref;
|
||||
}
|
||||
|
||||
void clearKeyCache()
|
||||
|
|
|
@ -21,7 +21,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
#include "exceptions.h"
|
||||
#include "irrlichttypes.h"
|
||||
#include "Keycodes.h"
|
||||
#include <Keycodes.h>
|
||||
#include <IEventReceiver.h>
|
||||
#include <string>
|
||||
|
||||
|
@ -63,11 +63,17 @@ protected:
|
|||
std::string m_name = "";
|
||||
};
|
||||
|
||||
// Global defines for convenience
|
||||
|
||||
extern const KeyPress EscapeKey;
|
||||
extern const KeyPress CancelKey;
|
||||
|
||||
extern const KeyPress LMBKey;
|
||||
extern const KeyPress MMBKey; // Middle Mouse Button
|
||||
extern const KeyPress RMBKey;
|
||||
|
||||
// Key configuration getter
|
||||
KeyPress getKeySetting(const char *settingname);
|
||||
const KeyPress &getKeySetting(const char *settingname);
|
||||
|
||||
// Clear fast lookup cache
|
||||
void clearKeyCache();
|
||||
|
|
|
@ -229,9 +229,8 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver)
|
|||
params.Stencilbuffer = false;
|
||||
params.Vsync = vsync;
|
||||
params.EventReceiver = receiver;
|
||||
#ifdef __ANDROID__
|
||||
params.PrivateData = porting::app_global;
|
||||
#endif
|
||||
params.DriverDebug = g_settings->getBool("opengl_debug");
|
||||
|
||||
// there is no standardized path for these on desktop
|
||||
std::string rel_path = std::string("client") + DIR_DELIM
|
||||
+ "shaders" + DIR_DELIM + "Irrlicht";
|
||||
|
@ -254,8 +253,11 @@ RenderingEngine::RenderingEngine(IEventReceiver *receiver)
|
|||
|
||||
RenderingEngine::~RenderingEngine()
|
||||
{
|
||||
sanity_check(s_singleton == this);
|
||||
|
||||
core.reset();
|
||||
m_device->closeDevice();
|
||||
m_device->drop();
|
||||
s_singleton = nullptr;
|
||||
}
|
||||
|
||||
|
@ -279,10 +281,7 @@ void RenderingEngine::removeMesh(const scene::IMesh* mesh)
|
|||
void RenderingEngine::cleanupMeshCache()
|
||||
{
|
||||
auto mesh_cache = m_device->getSceneManager()->getMeshCache();
|
||||
while (mesh_cache->getMeshCount() != 0) {
|
||||
if (scene::IAnimatedMesh *mesh = mesh_cache->getMeshByIndex(0))
|
||||
mesh_cache->removeMesh(mesh);
|
||||
}
|
||||
mesh_cache->clear();
|
||||
}
|
||||
|
||||
bool RenderingEngine::setupTopLevelWindow()
|
||||
|
|
|
@ -46,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
/*
|
||||
A cache from shader name to shader path
|
||||
*/
|
||||
MutexedMap<std::string, std::string> g_shadername_to_path_cache;
|
||||
static MutexedMap<std::string, std::string> g_shadername_to_path_cache;
|
||||
|
||||
/*
|
||||
Gets the path to a shader by first checking if the file
|
||||
|
|
|
@ -122,7 +122,7 @@ void Sky::render()
|
|||
if (!camera || !driver)
|
||||
return;
|
||||
|
||||
ScopeProfiler sp(g_profiler, "Sky::render()", SPT_AVG);
|
||||
ScopeProfiler sp(g_profiler, "Sky::render()", SPT_AVG, PRECISION_MICRO);
|
||||
|
||||
// Draw perspective skybox
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
|
||||
#include "al_extensions.h"
|
||||
#include "debug.h"
|
||||
#include "sound_constants.h"
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
|
||||
|
@ -77,33 +78,27 @@ PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> dat
|
|||
warn_if_al_error("when creating non-streaming sound");
|
||||
|
||||
} else {
|
||||
// Start with 2 buffers
|
||||
ALuint buf_ids[2];
|
||||
// Start with first buffer
|
||||
|
||||
// If m_next_sample_pos >= len_samples (happens only if not looped), one
|
||||
// or both of buf_ids will be 0. Queuing 0 is a NOP.
|
||||
// If m_next_sample_pos >= len_samples (happens only if not looped), buf0
|
||||
// will be 0. Queuing 0 is a NOP.
|
||||
|
||||
auto [buf0, buf0_end, offset_in_buf0] = m_data->getOrLoadBufferAt(m_next_sample_pos);
|
||||
buf_ids[0] = buf0;
|
||||
m_next_sample_pos = buf0_end;
|
||||
|
||||
if (m_looping && m_next_sample_pos == len_samples)
|
||||
m_next_sample_pos = 0;
|
||||
|
||||
auto [buf1, buf1_end, offset_in_buf1] = m_data->getOrLoadBufferAt(m_next_sample_pos);
|
||||
buf_ids[1] = buf1;
|
||||
m_next_sample_pos = buf1_end;
|
||||
assert(offset_in_buf1 == 0);
|
||||
|
||||
alSourceQueueBuffers(m_source_id, 2, buf_ids);
|
||||
alSourceQueueBuffers(m_source_id, 1, &buf0);
|
||||
alSourcei(m_source_id, AL_SAMPLE_OFFSET, offset_in_buf0);
|
||||
|
||||
// We can't use AL_LOOPING because more buffers are queued later
|
||||
// looping is therefore done manually
|
||||
// We can't use AL_LOOPING because more buffers are queued later.
|
||||
// Looping is therefore done manually.
|
||||
|
||||
// Sound is not dead if queue runs empty prematurely
|
||||
m_stopped_means_dead = false;
|
||||
|
||||
warn_if_al_error("when creating streaming sound");
|
||||
|
||||
// Enqueue more buffers
|
||||
stepStream(true);
|
||||
}
|
||||
|
||||
// Set initial pos, volume, pitch
|
||||
|
@ -129,23 +124,44 @@ PlayingSound::PlayingSound(ALuint source_id, std::shared_ptr<ISoundDataOpen> dat
|
|||
setPitch(pitch);
|
||||
}
|
||||
|
||||
bool PlayingSound::stepStream()
|
||||
bool PlayingSound::stepStream(bool playback_speed_changed)
|
||||
{
|
||||
if (isDead())
|
||||
return false;
|
||||
|
||||
// unqueue finished buffers
|
||||
ALint num_unqueued_bufs = 0;
|
||||
alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_unqueued_bufs);
|
||||
if (num_unqueued_bufs == 0)
|
||||
return true;
|
||||
// We always have 2 buffers enqueued at most
|
||||
SANITY_CHECK(num_unqueued_bufs <= 2);
|
||||
ALuint unqueued_buffer_ids[2];
|
||||
alSourceUnqueueBuffers(m_source_id, num_unqueued_bufs, unqueued_buffer_ids);
|
||||
// Unqueue finished buffers
|
||||
ALint num_processed_bufs = 0;
|
||||
alGetSourcei(m_source_id, AL_BUFFERS_PROCESSED, &num_processed_bufs);
|
||||
if (num_processed_bufs == 0 && !playback_speed_changed)
|
||||
return true; // Nothing to do
|
||||
if (num_processed_bufs > 0) {
|
||||
ALint num_to_unqueue = num_processed_bufs;
|
||||
ALuint unqueued_buffer_ids[8];
|
||||
while (num_to_unqueue > 8) {
|
||||
alSourceUnqueueBuffers(m_source_id, 8, unqueued_buffer_ids);
|
||||
num_to_unqueue -= 8;
|
||||
}
|
||||
alSourceUnqueueBuffers(m_source_id, num_to_unqueue, unqueued_buffer_ids);
|
||||
}
|
||||
|
||||
// Fill up again
|
||||
for (ALint i = 0; i < num_unqueued_bufs; ++i) {
|
||||
// Find out how many buffers we want to enqueue
|
||||
f32 pitch = 1.0f;
|
||||
alGetSourcef(m_source_id, AL_PITCH, &pitch);
|
||||
ALint num_queued_bufs = 0;
|
||||
alGetSourcei(m_source_id, AL_BUFFERS_QUEUED, &num_queued_bufs);
|
||||
// Min. length of untouched buffers
|
||||
const f32 playback_left = MIN_STREAM_BUFFER_LENGTH * std::max(0, num_queued_bufs - 1);
|
||||
// Max. time until next stepStream() call, see also [Streaming of sounds] in
|
||||
// sound_constants.h.
|
||||
// Multiplied by pitch because pitch makes playback faster than real time.
|
||||
// (Does not account for doppler effect, if we had that.)
|
||||
// +0.1 seconds to accommodate hickups.
|
||||
const f32 playback_until_next_check = (2.0f * STREAM_BIGSTEP_TIME + 0.1f) * pitch;
|
||||
const f32 playback_to_fill_up = std::max(0.0f, playback_until_next_check - playback_left);
|
||||
const int num_bufs_to_enqueue = std::ceil(playback_to_fill_up / MIN_STREAM_BUFFER_LENGTH);
|
||||
|
||||
// Fill up
|
||||
for (int i = 0; i < num_bufs_to_enqueue; ++i) {
|
||||
if (m_next_sample_pos == m_data->m_decode_info.length_samples) {
|
||||
// Reached end
|
||||
if (m_looping) {
|
||||
|
@ -256,4 +272,11 @@ f32 PlayingSound::getGain() noexcept
|
|||
return gain;
|
||||
}
|
||||
|
||||
void PlayingSound::setPitch(f32 pitch)
|
||||
{
|
||||
alSourcef(m_source_id, AL_PITCH, pitch);
|
||||
if (isStreaming())
|
||||
stepStream(true);
|
||||
}
|
||||
|
||||
} // namespace sound
|
||||
|
|
|
@ -63,7 +63,7 @@ public:
|
|||
DISABLE_CLASS_COPY(PlayingSound)
|
||||
|
||||
// return false means streaming finished
|
||||
bool stepStream();
|
||||
bool stepStream(bool playback_speed_changed = false);
|
||||
|
||||
// retruns true if it wasn't fading already
|
||||
bool fade(f32 step, f32 target_gain) noexcept;
|
||||
|
@ -77,7 +77,7 @@ public:
|
|||
|
||||
f32 getGain() noexcept;
|
||||
|
||||
void setPitch(f32 pitch) noexcept { alSourcef(m_source_id, AL_PITCH, pitch); }
|
||||
void setPitch(f32 pitch);
|
||||
|
||||
bool isStreaming() const noexcept { return m_data->isStreaming(); }
|
||||
|
||||
|
|
|
@ -89,14 +89,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
* In the worst case, a sound is stepped at the start of one bigstep and in the
|
||||
* end of the next bigstep. So between two stepStream()-calls lie at most
|
||||
* 2 * STREAM_BIGSTEP_TIME seconds.
|
||||
* As there are always 2 sound buffers enqueued, at least one untouched full buffer
|
||||
* is still available after the first stepStream().
|
||||
* If we take a MIN_STREAM_BUFFER_LENGTH > 2 * STREAM_BIGSTEP_TIME, we can hence
|
||||
* not run into an empty queue.
|
||||
*
|
||||
* The MIN_STREAM_BUFFER_LENGTH needs to be a little bigger because of dtime jitter,
|
||||
* other sounds that may have taken long to stepStream(), and sounds being played
|
||||
* faster due to Doppler effect.
|
||||
* We ensure that there are always enough untouched full buffers left such that
|
||||
* we do not run into an empty queue in this time period, see stepStream().
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -115,8 +109,6 @@ constexpr f32 STREAM_BIGSTEP_TIME = 0.3f;
|
|||
// step duration for the OpenALSoundManager thread, in seconds
|
||||
constexpr f32 SOUNDTHREAD_DTIME = 0.016f;
|
||||
|
||||
static_assert(MIN_STREAM_BUFFER_LENGTH > STREAM_BIGSTEP_TIME * 2.0f,
|
||||
"See [Streaming of sounds].");
|
||||
static_assert(SOUND_DURATION_MAX_SINGLE >= MIN_STREAM_BUFFER_LENGTH * 2.0f,
|
||||
"There's no benefit in streaming if we can't queue more than 2 buffers.");
|
||||
|
||||
|
|
|
@ -191,7 +191,7 @@ private:
|
|||
scene::IMesh *m_cube;
|
||||
};
|
||||
|
||||
ExtrusionMeshCache *g_extrusion_mesh_cache = NULL;
|
||||
static ExtrusionMeshCache *g_extrusion_mesh_cache = nullptr;
|
||||
|
||||
|
||||
WieldMeshSceneNode::WieldMeshSceneNode(scene::ISceneManager *mgr, s32 id, bool lighting):
|
||||
|
|
|
@ -234,7 +234,7 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef,
|
|||
Map *map = &env->getMap();
|
||||
ServerEnvironment *s_env = dynamic_cast<ServerEnvironment*>(env);
|
||||
|
||||
ScopeProfiler sp(g_profiler, PROFILER_NAME("collisionMoveSimple()"), SPT_AVG);
|
||||
ScopeProfiler sp(g_profiler, PROFILER_NAME("collisionMoveSimple()"), SPT_AVG, PRECISION_MICRO);
|
||||
|
||||
collisionMoveResult result;
|
||||
|
||||
|
@ -273,7 +273,7 @@ collisionMoveResult collisionMoveSimple(Environment *env, IGameDef *gamedef,
|
|||
std::vector<NearbyCollisionInfo> cinfo;
|
||||
{
|
||||
//TimeTaker tt2("collisionMoveSimple collect boxes");
|
||||
ScopeProfiler sp2(g_profiler, PROFILER_NAME("collision collect boxes"), SPT_AVG);
|
||||
ScopeProfiler sp2(g_profiler, PROFILER_NAME("collision collect boxes"), SPT_AVG, PRECISION_MICRO);
|
||||
|
||||
v3f minpos_f(
|
||||
MYMIN(pos_f->X, newpos_f.X),
|
||||
|
|
|
@ -26,9 +26,74 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
#include "mapgen/mapgen.h" // Mapgen::setDefaultSettings
|
||||
#include "util/string.h"
|
||||
|
||||
|
||||
/*
|
||||
* inspired by https://github.com/systemd/systemd/blob/7aed43437175623e0f3ae8b071bbc500c13ce893/src/hostname/hostnamed.c#L406
|
||||
* this could be done in future with D-Bus using query:
|
||||
* busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 Chassis
|
||||
*/
|
||||
static bool detect_touch()
|
||||
{
|
||||
#if defined(__ANDROID__)
|
||||
return true;
|
||||
#elif defined(__linux__)
|
||||
std::string chassis_type;
|
||||
|
||||
// device-tree platforms (non-X86)
|
||||
std::ifstream dtb_file("/proc/device-tree/chassis-type");
|
||||
if (dtb_file.is_open()) {
|
||||
std::getline(dtb_file, chassis_type);
|
||||
chassis_type.pop_back();
|
||||
|
||||
if (chassis_type == "tablet" ||
|
||||
chassis_type == "handset" ||
|
||||
chassis_type == "watch")
|
||||
return true;
|
||||
|
||||
if (!chassis_type.empty())
|
||||
return false;
|
||||
}
|
||||
// SMBIOS
|
||||
std::ifstream dmi_file("/sys/class/dmi/id/chassis_type");
|
||||
if (dmi_file.is_open()) {
|
||||
std::getline(dmi_file, chassis_type);
|
||||
|
||||
if (chassis_type == "11" /* Handheld */ ||
|
||||
chassis_type == "30" /* Tablet */)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ACPI-based platforms
|
||||
std::ifstream acpi_file("/sys/firmware/acpi/pm_profile");
|
||||
if (acpi_file.is_open()) {
|
||||
std::getline(acpi_file, chassis_type);
|
||||
|
||||
if (chassis_type == "8" /* Tablet */)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
#elif defined(_WIN32)
|
||||
// 0x01 The device has an integrated touch digitizer
|
||||
// 0x80 The device is ready to receive digitizer input.
|
||||
if ((GetSystemMetrics(SM_DIGITIZER) & 0x81) == 0x81)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
#else
|
||||
// we don't know, return default
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void set_default_settings()
|
||||
{
|
||||
Settings *settings = Settings::createLayer(SL_DEFAULTS);
|
||||
bool has_touch = detect_touch();
|
||||
|
||||
// Client and server
|
||||
settings->setDefault("language", "");
|
||||
|
@ -39,11 +104,7 @@ void set_default_settings()
|
|||
// Client
|
||||
settings->setDefault("address", "");
|
||||
settings->setDefault("enable_sound", "true");
|
||||
#if ENABLE_TOUCH
|
||||
settings->setDefault("enable_touch", "true");
|
||||
#else
|
||||
settings->setDefault("enable_touch", "false");
|
||||
#endif
|
||||
settings->setDefault("enable_touch", bool_to_cstr(has_touch));
|
||||
settings->setDefault("sound_volume", "0.8");
|
||||
settings->setDefault("sound_volume_unfocused", "0.3");
|
||||
settings->setDefault("mute_sound", "false");
|
||||
|
@ -94,12 +155,10 @@ void set_default_settings()
|
|||
settings->setDefault("keymap_cmd_local", ".");
|
||||
settings->setDefault("keymap_minimap", "KEY_KEY_V");
|
||||
settings->setDefault("keymap_console", "KEY_F10");
|
||||
#if ENABLE_TOUCH
|
||||
|
||||
// See https://github.com/minetest/minetest/issues/12792
|
||||
settings->setDefault("keymap_rangeselect", "KEY_KEY_R");
|
||||
#else
|
||||
settings->setDefault("keymap_rangeselect", "");
|
||||
#endif
|
||||
settings->setDefault("keymap_rangeselect", has_touch ? "KEY_KEY_R" : "");
|
||||
|
||||
settings->setDefault("keymap_freemove", "KEY_KEY_K");
|
||||
settings->setDefault("keymap_pitchmove", "");
|
||||
settings->setDefault("keymap_fastmove", "KEY_KEY_J");
|
||||
|
@ -114,7 +173,7 @@ void set_default_settings()
|
|||
settings->setDefault("keymap_toggle_hud", "KEY_F1");
|
||||
settings->setDefault("keymap_toggle_chat", "KEY_F2");
|
||||
settings->setDefault("keymap_toggle_fog", "KEY_F3");
|
||||
#if DEBUG
|
||||
#ifndef NDEBUG
|
||||
settings->setDefault("keymap_toggle_update_camera", "KEY_F4");
|
||||
#else
|
||||
settings->setDefault("keymap_toggle_update_camera", "");
|
||||
|
@ -174,8 +233,10 @@ void set_default_settings()
|
|||
// Visuals
|
||||
#ifdef NDEBUG
|
||||
settings->setDefault("show_debug", "false");
|
||||
settings->setDefault("opengl_debug", "false");
|
||||
#else
|
||||
settings->setDefault("show_debug", "true");
|
||||
settings->setDefault("opengl_debug", "true");
|
||||
#endif
|
||||
settings->setDefault("fsaa", "2");
|
||||
settings->setDefault("undersampling", "1");
|
||||
|
@ -196,11 +257,7 @@ void set_default_settings()
|
|||
settings->setDefault("screen_h", "600");
|
||||
settings->setDefault("window_maximized", "false");
|
||||
settings->setDefault("autosave_screensize", "true");
|
||||
#ifdef ENABLE_TOUCH
|
||||
settings->setDefault("fullscreen", "true");
|
||||
#else
|
||||
settings->setDefault("fullscreen", "false");
|
||||
#endif
|
||||
settings->setDefault("fullscreen", bool_to_cstr(has_touch));
|
||||
settings->setDefault("vsync", "false");
|
||||
settings->setDefault("fov", "72");
|
||||
settings->setDefault("leaves_style", "fancy");
|
||||
|
@ -307,11 +364,7 @@ void set_default_settings()
|
|||
settings->setDefault("aux1_descends", "false");
|
||||
settings->setDefault("doubletap_jump", "false");
|
||||
settings->setDefault("always_fly_fast", "true");
|
||||
#ifdef ENABLE_TOUCH
|
||||
settings->setDefault("autojump", "true");
|
||||
#else
|
||||
settings->setDefault("autojump", "false");
|
||||
#endif
|
||||
settings->setDefault("autojump", bool_to_cstr(has_touch));
|
||||
settings->setDefault("continuous_forward", "false");
|
||||
settings->setDefault("enable_joysticks", "false");
|
||||
settings->setDefault("joystick_id", "0");
|
||||
|
@ -492,11 +545,7 @@ void set_default_settings()
|
|||
settings->setDefault("fixed_virtual_joystick", "false");
|
||||
settings->setDefault("virtual_joystick_triggers_aux1", "false");
|
||||
settings->setDefault("touch_punch_gesture", "short_tap");
|
||||
#ifdef ENABLE_TOUCH
|
||||
settings->setDefault("clickable_chat_weblinks", "false");
|
||||
#else
|
||||
settings->setDefault("clickable_chat_weblinks", "true");
|
||||
#endif
|
||||
// Altered settings for Android
|
||||
#ifdef __ANDROID__
|
||||
settings->setDefault("screen_w", "0");
|
||||
|
|
|
@ -70,7 +70,7 @@ enum EmergeAction {
|
|||
EMERGE_GENERATED,
|
||||
};
|
||||
|
||||
const static std::string emergeActionStrs[] = {
|
||||
constexpr const char *emergeActionStrs[] = {
|
||||
"cancelled",
|
||||
"errored",
|
||||
"from_memory",
|
||||
|
|
|
@ -26,10 +26,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
class BaseException : public std::exception
|
||||
{
|
||||
public:
|
||||
BaseException(const std::string &s) throw(): m_s(s) {}
|
||||
BaseException(const std::string &s) noexcept: m_s(s) {}
|
||||
~BaseException() throw() = default;
|
||||
|
||||
virtual const char * what() const throw()
|
||||
virtual const char * what() const noexcept
|
||||
{
|
||||
return m_s.c_str();
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue