Merge branch 'master' into doc-refactor-2

This commit is contained in:
Bituvo 2024-04-27 09:44:25 -04:00
commit 408bb3e60f
138 changed files with 9045 additions and 4541 deletions

View File

@ -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:

View File

@ -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?!")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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"
}
}

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

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
--Minetest
--Copyright (C) 2018-24 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
local function get_formspec(data)
local selected_game, selected_game_idx = pkgmgr.find_by_gameid(core.settings:get("menu_last_game"))
if not selected_game_idx then
selected_game_idx = 1
selected_game = pkgmgr.games[1]
end
local game_list = {}
for i, game in ipairs(pkgmgr.games) do
game_list[i] = core.formspec_escape(game.title)
end
local package = data.package
local will_install_deps = data.will_install_deps
local deps_to_install = 0
local deps_not_found = 0
data.dependencies = contentdb.resolve_dependencies(package, selected_game)
local formatted_deps = {}
for _, dep in pairs(data.dependencies) do
formatted_deps[#formatted_deps + 1] = "#fff"
formatted_deps[#formatted_deps + 1] = core.formspec_escape(dep.name)
if dep.installed then
formatted_deps[#formatted_deps + 1] = "#ccf"
formatted_deps[#formatted_deps + 1] = fgettext("Already installed")
elseif dep.package then
formatted_deps[#formatted_deps + 1] = "#cfc"
formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author)
deps_to_install = deps_to_install + 1
else
formatted_deps[#formatted_deps + 1] = "#f00"
formatted_deps[#formatted_deps + 1] = fgettext("Not found")
deps_not_found = deps_not_found + 1
end
end
local message_bg = "#3333"
local message
if will_install_deps then
message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install)
else
message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install)
end
if deps_not_found > 0 then
message = fgettext("$1 required dependencies could not be found.", deps_not_found) ..
" " .. fgettext("Please check that the base game is correct.", deps_not_found) ..
"\n" .. message
message_bg = mt_color_orange
end
local formspec = {
"formspec_version[3]",
"size[7,7.85]",
"style[title;border=false]",
"box[0,0;7,0.5;#3333]",
"button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]",
"container[0.375,0.70]",
"label[0,0.25;", fgettext("Base Game:"), "]",
"dropdown[2,0;4.25,0.5;selected_game;", table.concat(game_list, ","), ";", selected_game_idx, "]",
"label[0,0.8;", fgettext("Dependencies:"), "]",
"tablecolumns[color;text;color;text]",
"table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]",
"container_end[]",
"checkbox[0.375,5.1;will_install_deps;",
fgettext("Install missing dependencies"), ";",
will_install_deps and "true" or "false", "]",
"box[0,5.4;7,1.2;", message_bg, "]",
"textarea[0.375,5.5;6.25,1;;;", message, "]",
"container[1.375,6.85]",
"button[0,0;2,0.8;install_all;", fgettext("Install"), "]",
"button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]",
"container_end[]",
}
return table.concat(formspec)
end
local function handle_submit(this, fields)
local data = this.data
if fields.cancel then
this:delete()
return true
end
if fields.will_install_deps ~= nil then
data.will_install_deps = core.is_yes(fields.will_install_deps)
return true
end
if fields.install_all then
contentdb.queue_download(data.package, contentdb.REASON_NEW)
if data.will_install_deps then
for _, dep in pairs(data.dependencies) do
if not dep.is_optional and not dep.installed and dep.package then
contentdb.queue_download(dep.package, contentdb.REASON_DEPENDENCY)
end
end
end
this:delete()
return true
end
if fields.selected_game then
for _, game in pairs(pkgmgr.games) do
if game.title == fields.selected_game then
core.settings:set("menu_last_game", game.id)
break
end
end
return true
end
return false
end
function create_install_dialog(package)
local dlg = dialog_create("install_dialog", get_formspec, handle_submit, nil)
dlg.data.dependencies = nil
dlg.data.package = package
dlg.data.will_install_deps = true
return dlg
end

View File

@ -0,0 +1,53 @@
--Minetest
--Copyright (C) 2018-24 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
function get_formspec(data)
local package = data.package
return confirmation_formspec(
fgettext("\"$1\" already exists. Would you like to overwrite it?", package.name),
'install', fgettext("Overwrite"),
'cancel', fgettext("Cancel"))
end
local function handle_submit(this, fields)
local data = this.data
if fields.cancel then
this:delete()
return true
end
if fields.install then
this:delete()
data.callback()
return true
end
return false
end
function create_confirm_overwrite(package, callback)
assert(type(package) == "table")
assert(type(callback) == "function")
local dlg = dialog_create("data", get_formspec, handle_submit, nil)
dlg.data.package = package
dlg.data.callback = callback
return dlg
end

View File

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

View File

@ -0,0 +1,78 @@
--Minetest
--Copyright (C) 2023-24 rubenwardy
--
--This program is free software; you can redistribute it and/or modify
--it under the terms of the GNU Lesser General Public License as published by
--the Free Software Foundation; either version 2.1 of the License, or
--(at your option) any later version.
--
--This program is distributed in the hope that it will be useful,
--but WITHOUT ANY WARRANTY; without even the implied warranty of
--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--GNU Lesser General Public License for more details.
--
--You should have received a copy of the GNU Lesser General Public License along
--with this program; if not, write to the Free Software Foundation, Inc.,
--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-- Screenshot
local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb"
assert(core.create_dir(screenshot_dir))
local screenshot_downloading = {}
local screenshot_downloaded = {}
local function get_file_extension(path)
local parts = path:split(".")
return parts[#parts]
end
function get_screenshot(package)
if not package.thumbnail then
return defaulttexturedir .. "no_screenshot.png"
elseif screenshot_downloading[package.thumbnail] then
return defaulttexturedir .. "loading_screenshot.png"
end
-- Get tmp screenshot path
local ext = get_file_extension(package.thumbnail)
local filepath = screenshot_dir .. DIR_DELIM ..
("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext)
-- Return if already downloaded
local file = io.open(filepath, "r")
if file then
file:close()
return filepath
end
-- Show error if we've failed to download before
if screenshot_downloaded[package.thumbnail] then
return defaulttexturedir .. "error_screenshot.png"
end
-- Download
local function download_screenshot(params)
return core.download_file(params.url, params.dest)
end
local function callback(success)
screenshot_downloading[package.thumbnail] = nil
screenshot_downloaded[package.thumbnail] = true
if not success then
core.log("warning", "Screenshot download failed for some reason")
end
ui.update()
end
if core.handle_async(download_screenshot,
{ dest = filepath, url = package.thumbnail }, callback) then
screenshot_downloading[package.thumbnail] = true
else
core.log("error", "ERROR: async event failed")
return defaulttexturedir .. "error_screenshot.png"
end
return defaulttexturedir .. "loading_screenshot.png"
end

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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.

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -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.

View File

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

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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
---------

View File

@ -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;
};
};

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 &params,
io::IFileSystem *io, video::IContextManager *contextManager);
IVideoDriver *createOGLES2Driver(const SIrrlichtCreationParameters &params,
io::IFileSystem *io, video::IContextManager *contextManager);
}
}
namespace irr
{
CIrrDeviceAndroid::CIrrDeviceAndroid(const SIrrlichtCreationParameters &param) :
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

View File

@ -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 &param);
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

View File

@ -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

View File

@ -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

View File

@ -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 &params, 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
}
}

View File

@ -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;

View File

@ -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;

View File

@ -249,16 +249,34 @@ void CIrrDeviceSDL::resetReceiveTextInputEvents()
//! constructor
CIrrDeviceSDL::CIrrDeviceSDL(const SIrrlichtCreationParameters &param) :
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 &param) :
}
}
// 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));

View File

@ -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

View File

@ -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}"
)

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -15,6 +15,8 @@
#include "COGLESExtensionHandler.h"
#include "IContextManager.h"
#define TEST_GL_ERROR(cls) (cls)->testGLError(__LINE__)
namespace irr
{
namespace video

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -7,7 +7,6 @@
#if defined(_IRR_COMPILE_WITH_SDL_DEVICE_)
#include "CIrrDeviceSDL.h"
#include "COpenGLCommon.h"
namespace irr
{

View File

@ -179,9 +179,8 @@ bool CWGLManager::initialize(const SIrrlichtCreationParameters &params, 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");

View File

@ -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_)

View File

@ -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

View File

@ -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 &params, io::IFileSystem *io, IContextManager *contextManager) :
@ -147,7 +154,7 @@ COpenGL3DriverBase::COpenGL3DriverBase(const SIrrlichtCreationParameters &params
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 &params
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;
}

View File

@ -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;

View File

@ -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()

View File

@ -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.");

View 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()

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -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();
}

View File

@ -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.

View File

@ -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);

View File

@ -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();

View File

@ -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;
}

View File

@ -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()

View File

@ -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();

View File

@ -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()

View File

@ -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

View 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

View File

@ -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

View File

@ -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(); }

View File

@ -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.");

View File

@ -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):

View File

@ -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),

View File

@ -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");

View File

@ -70,7 +70,7 @@ enum EmergeAction {
EMERGE_GENERATED,
};
const static std::string emergeActionStrs[] = {
constexpr const char *emergeActionStrs[] = {
"cancelled",
"errored",
"from_memory",

View File

@ -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