Initial commit
This commit is contained in:
104
backends/platform/3ds/3ds.mk
Normal file
104
backends/platform/3ds/3ds.mk
Normal file
@@ -0,0 +1,104 @@
|
||||
TARGET := scummvm
|
||||
|
||||
APP_TITLE := ScummVM
|
||||
APP_DESCRIPTION := Point-and-click adventure game engines
|
||||
APP_AUTHOR := ScummVM Team
|
||||
APP_ICON := $(srcdir)/backends/platform/3ds/app/icon.png
|
||||
|
||||
APP_RSF := $(srcdir)/backends/platform/3ds/app/scummvm.rsf
|
||||
APP_BANNER_IMAGE:= $(srcdir)/backends/platform/3ds/app/banner.png
|
||||
APP_BANNER_AUDIO:= $(srcdir)/backends/platform/3ds/app/banner.wav
|
||||
|
||||
BANNERTOOL ?= bannertool
|
||||
MAKEROM ?= makerom
|
||||
|
||||
.PHONY: clean_3ds dist_3ds
|
||||
|
||||
clean: clean_3ds
|
||||
|
||||
clean_3ds:
|
||||
$(RM) backends/platform/3ds/shader.shbin
|
||||
$(RM) backends/platform/3ds/shader_shbin.h
|
||||
$(RM) $(TARGET).smdh
|
||||
$(RM) $(TARGET).3dsx
|
||||
$(RM) $(TARGET).bnr
|
||||
$(RM) $(TARGET).cia
|
||||
$(RM) -rf romfs
|
||||
$(RM) -rf dist_3ds
|
||||
|
||||
romfs: $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_3DS_EXTRA_FILES) $(PLUGINS)
|
||||
@rm -rf romfs
|
||||
@mkdir -p romfs
|
||||
@cp $(DIST_FILES_THEMES) romfs/
|
||||
ifdef DIST_FILES_ENGINEDATA
|
||||
@cp $(DIST_FILES_ENGINEDATA) romfs/
|
||||
endif
|
||||
ifdef DIST_FILES_NETWORKING
|
||||
@cp $(DIST_FILES_NETWORKING) romfs/
|
||||
endif
|
||||
ifdef DIST_FILES_VKEYBD
|
||||
@cp $(DIST_FILES_VKEYBD) romfs/
|
||||
endif
|
||||
ifdef DIST_3DS_EXTRA_FILES
|
||||
@cp -a $(DIST_3DS_EXTRA_FILES) romfs/
|
||||
endif
|
||||
ifeq ($(DYNAMIC_MODULES),1)
|
||||
@mkdir -p romfs/plugins
|
||||
@for i in $(PLUGINS); do $(STRIP) --strip-debug $$i -o romfs/plugins/`basename $$i`; done
|
||||
endif
|
||||
|
||||
$(TARGET).smdh: $(APP_ICON)
|
||||
@$(DEVKITPRO)/tools/bin/smdhtool --create "$(APP_TITLE)" "$(APP_DESCRIPTION)" "$(APP_AUTHOR)" $(APP_ICON) $@
|
||||
@echo built ... $(notdir $@)
|
||||
|
||||
$(TARGET).3dsx: $(EXECUTABLE) $(TARGET).smdh romfs
|
||||
@$(DEVKITPRO)/tools/bin/3dsxtool $< $@ --smdh=$(TARGET).smdh --romfs=romfs
|
||||
@echo built ... $(notdir $@)
|
||||
|
||||
$(TARGET).bnr: $(APP_BANNER_IMAGE) $(APP_BANNER_AUDIO)
|
||||
@$(BANNERTOOL) makebanner -o $@ -i $(APP_BANNER_IMAGE) -a $(APP_BANNER_AUDIO)
|
||||
@echo built ... $(notdir $@)
|
||||
|
||||
$(TARGET).cia: $(EXECUTABLE) $(APP_RSF) $(TARGET).smdh $(TARGET).bnr romfs
|
||||
@$(MAKEROM) -ver $(shell echo $$((($(VER_MAJOR)-2000)*256+$(VER_MINOR)*16+$(VER_PATCH)))) -f cia -target t -exefslogo -o $@ -elf $(EXECUTABLE) -rsf $(APP_RSF) -banner $(TARGET).bnr -icon $(TARGET).smdh -DAPP_ROMFS=romfs/
|
||||
@echo built ... $(notdir $@)
|
||||
|
||||
dist_3ds: $(TARGET).cia $(TARGET).3dsx $(DIST_FILES_DOCS)
|
||||
@rm -rf dist_3ds
|
||||
@mkdir -p dist_3ds
|
||||
@cp $(TARGET).3dsx $(TARGET).cia dist_3ds/
|
||||
@cp $(DIST_FILES_DOCS) dist_3ds/
|
||||
@cp $(srcdir)/backends/platform/3ds/README.md dist_3ds/README-3DS.md
|
||||
@echo built ... $(notdir $@)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# rules for assembling GPU shaders
|
||||
#---------------------------------------------------------------------------------
|
||||
define shader-as
|
||||
$(eval FILEPATH := $(patsubst %.shbin.o,%.shbin,$@))
|
||||
$(eval FILE := $(patsubst %.shbin.o,%.shbin,$(notdir $@)))
|
||||
$(DEVKITPRO)/tools/bin/picasso -o $(FILEPATH) $1
|
||||
$(DEVKITPRO)/tools/bin/bin2s $(FILEPATH) | $(AS) -o $@
|
||||
echo "extern const u8" `(echo $(FILE) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(FILEPATH) | tr . _)`.h
|
||||
echo "extern const u8" `(echo $(FILE) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(FILEPATH) | tr . _)`.h
|
||||
echo "extern const u32" `(echo $(FILE) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(FILEPATH) | tr . _)`.h
|
||||
endef
|
||||
|
||||
vpath %.v.pica $(srcdir)
|
||||
vpath %.g.pica $(srcdir)
|
||||
vpath %.shlist $(srcdir)
|
||||
|
||||
%.shbin.o : %.v.pica %.g.pica
|
||||
@echo $(notdir $^)
|
||||
@$(call shader-as,$^)
|
||||
|
||||
%.shbin.o : %.v.pica
|
||||
@echo $(notdir $<)
|
||||
@$(call shader-as,$<)
|
||||
|
||||
%.shbin.o : %.shlist
|
||||
@echo $(notdir $<)
|
||||
@$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)/$(file)))
|
||||
|
||||
# osystem-graphics.cpp includes shader_shbin.h that is generated by the shader assembler
|
||||
backends/platform/3ds/osystem-graphics.o: backends/platform/3ds/shader.shbin.o
|
||||
259
backends/platform/3ds/README.md
Normal file
259
backends/platform/3ds/README.md
Normal file
@@ -0,0 +1,259 @@
|
||||
ScummVM 3DS README
|
||||
------------------------------------------------------------------------
|
||||
|
||||
Table of Contents:
|
||||
------------------
|
||||
[1.0) Installation](#10-installation)
|
||||
* [1.1 3DSX installation](#11-3dsx-installation)
|
||||
* [1.2 CIA installation](#12-cia-installation)
|
||||
|
||||
[2.0) Controls](#20-controls)
|
||||
* [2.1 Default key mappings](#21-default-key-mappings)
|
||||
* [2.2 Hover mode](#22-hover-mode)
|
||||
* [2.3 Drag mode](#23-drag-mode)
|
||||
* [2.4 Magnify mode](#24-magnify-mode)
|
||||
|
||||
[3.0) Supported Games](#30-supported-games)
|
||||
|
||||
[4.0) Compiling](#40-compiling)
|
||||
* [4.1 Prerequisites](#41-prerequisites)
|
||||
* * [4.1.1 Compiling third-party libraries](#411-compiling-third-party-libraries)
|
||||
* * [4.1.2 Manually setting up the environment](#412-manually-setting-up-the-environment)
|
||||
* [4.2 Compiling ScummVM](#42-compiling-scummvm)
|
||||
|
||||
1.0) Installation
|
||||
-----------------
|
||||
There are two possible formats to be used: 3DSX and CIA.
|
||||
The 3DSX format is considered more ethical because it does not make use of
|
||||
invalid title IDs, which get logged. It is also compatible with homebrew loading
|
||||
methods that do not involve CFW.
|
||||
The 3DSX format is exclusively used by the Homebrew Launcher and its derivatives.
|
||||
The CIA format can be installed directly to the 3DS home menu and can be launched
|
||||
using any CFW (Custom Firmware) of your choice.
|
||||
|
||||
Installing the Homebrew Launcher or any CFW is beyond the scope of this README.
|
||||
|
||||
1.1) 3DSX installation
|
||||
----------------
|
||||
You need to merely extract the ScummVM 3DSX files to your SD card so that all
|
||||
files reside in the `/3ds/scummvm/` directory. It can then be launched by Homebrew Launcher
|
||||
or a similar implementation.
|
||||
|
||||
1.2) CIA installation
|
||||
---------------------
|
||||
The CIA format requires a DSP binary dump saved on your SD card as `/3ds/dspfirm.cdc`
|
||||
for proper audio support. You can search online to find software to dump this.
|
||||
Not having this file will cause many problems with games that need audio, sometimes
|
||||
even crashing, so this is NOT considered optional.
|
||||
|
||||
Using any CIA installation software (search elsewhere for that), you need to install
|
||||
the `scummvm.cia` file.
|
||||
|
||||
2.0) Controls
|
||||
-------------
|
||||
|
||||
2.1) Default key mappings
|
||||
-------------------------
|
||||
|
||||
The key mappings can be customized in the options dialog for the global mappings,
|
||||
and in the edit game dialog for per-game mappings. Per-game mappings overlay the
|
||||
global mappings, so if a button is bound to an action twice, the per-game mapping
|
||||
wins.
|
||||
|
||||
The default keymap is:
|
||||
|
||||
| Buttons | Function |
|
||||
|------------|--------------------------------|
|
||||
| A | Left-click |
|
||||
| B | Right-click |
|
||||
| X | . (skips the current line) |
|
||||
| Y | ESC (skips cutscenes and such) |
|
||||
| L | Toggle magnify mode on/off |
|
||||
| R | Toggle hover/drag modes |
|
||||
| Start | Open global main menu |
|
||||
| Select | Use virtual keyboard |
|
||||
| Circle Pad | Move the cursor |
|
||||
|
||||
2.2) Hover mode
|
||||
---------------
|
||||
When you use the touchscreen, you are simulating the mere moving of the mouse. You
|
||||
can click only with taps, meaning it is impossible to drag stuff or hold down a
|
||||
mouse button without using buttons mapped to right/left-click.
|
||||
|
||||
2.3) Drag mode
|
||||
--------------
|
||||
Every time you touch and release the touchscreen, you are simulating the click and
|
||||
release of the mouse buttons. At the moment, this is only a left-click.
|
||||
|
||||
2.4) Magnify mode
|
||||
-----------------
|
||||
Due to the low resolutions of the 3DS's two screens (400x240 for the top, and 320x240
|
||||
for the bottom), games that run at a higher resolution will inevitably lose some visual
|
||||
detail from being scaled down. This can result in situations where essential information
|
||||
is undiscernable, such as text. Magnify mode increases the scale factor of the top screen
|
||||
back to 1; the bottom screen remains unchanged. The touchscreen can then be used to change
|
||||
which part of the game display is being magnified. This can all be done even in situations
|
||||
where the cursor is disabled, such as during full-motion video (FMV) segments.
|
||||
|
||||
When activating magnify mode, touchscreen controls are automatically switched to hover
|
||||
mode; this is to reduce the risk of the user accidentally inputting a click when changing
|
||||
the magnified area via dragging the stylus. Clicking can still be done at will as in normal
|
||||
hover mode. Turning off magnify mode will revert controls back to what was being used
|
||||
previously (ex: if drag mode was in use prior to activating magnify mode, drag mode will
|
||||
be reactivated upon exiting magnify mode), as well as restore the top screen's previous
|
||||
scale factor.
|
||||
|
||||
Currently magnify mode can only be used when the following conditions are met:
|
||||
- In the Backend tab in the options dialog, "Use Screen" is set to "Both"
|
||||
- A game is currently being played
|
||||
- The horizontal and/or vertical resolution in-game is greater than that of the top screen
|
||||
|
||||
Magnify mode cannot be used in the Launcher menu.
|
||||
|
||||
3.0) Supported Games
|
||||
--------------------
|
||||
The full game engine compatibility list can be found here:
|
||||
https://scummvm.org/compatibility/
|
||||
|
||||
While all the above games should run on the 3DS (report if they do not), there are
|
||||
many games which are unplayable due to the lack of CPU speed on the 3DS. So if
|
||||
you play any games that run really slow, this is not considered a bug, but rather
|
||||
a hardware limitation. Though possible GPU optimizations are always in the works.
|
||||
The New 3DS console has much better performance, but there are still many newer and
|
||||
high-resolution games that cannot be played. A list of these unplayable games and
|
||||
game engines will eventually be listed here.
|
||||
|
||||
4.0) Compiling
|
||||
--------------
|
||||
4.1) Prerequisites
|
||||
------------------
|
||||
- Latest version of devkitPro, which comes with devkitARM and `libctru`
|
||||
- `citro3d` thorugh devkitPro's pacman
|
||||
- Optional: You should compile third-party libraries for the 3ds (commonly referred
|
||||
to as portlibs in the devkitPRO community). Some games requires these to operate
|
||||
properly.
|
||||
|
||||
|
||||
4.1.1) Compiling third-party libraries
|
||||
--------------------------------------
|
||||
It is strongly recommended that you use devkitPro's pacman in order to get the most recent
|
||||
portlibs for your build. Instructions for accessing these binaries can be found here:
|
||||
https://devkitpro.org/wiki/devkitPro_pacman
|
||||
|
||||
The following libraries can be downloaded with pacman:
|
||||
|
||||
| Library | Package |
|
||||
----------------|------------------------
|
||||
| zlib | 3ds-zlib |
|
||||
| libpng | 3ds-libpng |
|
||||
| libjpeg | 3ds-libjpeg-turbo |
|
||||
| freetype2 | 3ds-freetype |
|
||||
| libmad | 3ds-libmad |
|
||||
| libogg | 3ds-libogg |
|
||||
| tremor | 3ds-libvorbisidec |
|
||||
| flac | 3ds-flac |
|
||||
| curl | 3ds-curl |
|
||||
| libtheora | 3ds-libtheora |
|
||||
|
||||
At the moment of writing, the version of `freetype2` packaged by devkitPro has an issue
|
||||
where it allocates too much data on the stack when ScummVM loads GUI themes.
|
||||
As a workaround, an older version can be used. Version 2.6.5 is known to work well. The
|
||||
instructions below can be used to compile it.
|
||||
|
||||
At the moment of writing, `faad` is not in the devkitPro 3DS pacman repository. It
|
||||
can be compiled by following the instructions in the section below, in case it cannot
|
||||
be found through pacman.
|
||||
|
||||
The following pacman packages are also recommended:
|
||||
- `3ds-dev`
|
||||
- `dkp-toolchain-vars`
|
||||
|
||||
Once you have the `dkp-toolchain-vars` package, you should be able to find the following
|
||||
scripts in your `/opt/devkitpro` folder:
|
||||
- `devkitarm.sh`
|
||||
- `3dsvars.sh`
|
||||
|
||||
Run them one after the other with `source` in order to setup your environment variables
|
||||
for cross-compiling:
|
||||
```
|
||||
$ source /opt/devkitpro/devkitarm.sh
|
||||
$ source /opt/devkitpro/3dsvars.sh
|
||||
```
|
||||
|
||||
After that, you can download the libraries you want to cross compile, run any autoconf
|
||||
scripts that they may have, and then they can usually be built with the following steps
|
||||
from their source directory:
|
||||
```
|
||||
$ mkdir -p $PORTLIBS
|
||||
$ ./configure --prefix=$PORTLIBS --host=arm-none-eabi --disable-shared \
|
||||
--enable-static
|
||||
$ make
|
||||
$ make install
|
||||
```
|
||||
Most libraries used can be compiled with same commands and configuration flags.
|
||||
|
||||
4.1.2) Manually setting up the environment
|
||||
------------------------------------------
|
||||
In case you don't have the `dkp-toolchain-vars` package downloaded, you can use the
|
||||
following to set-up your environment variables.
|
||||
|
||||
It is assumed that you have these variables already set up. If not, then do so:
|
||||
- DEVKITPRO Your root devkitPro directory (usually /opt/devkitpro)
|
||||
- DEVKITARM Your root devkitARM directory (probably same as $DEVKITPRO/devkitARM)
|
||||
- CTRULIB Your root libctru directory (probably same as $DEVKITPRO/libctru)
|
||||
|
||||
In the source directory of the library:
|
||||
```
|
||||
$ export PORTLIBS=$DEVKITPRO/portlibs/3ds
|
||||
$ export PATH=$DEVKITPRO/tools/bin:$PORTLIBS/bin:$DEVKITARM/bin:$PATH
|
||||
$ export PKG_CONFIG_PATH=$PORTLIBS/lib/pkgconfig
|
||||
$ export PKG_CONFIG_LIBDIR=$PORTLIBS/lib/pkgconfig
|
||||
$ export CFLAGS="-g -march=armv6k -mtune=mpcore -mfloat-abi=hard -O2
|
||||
-mword-relocations -ffunction-sections -fdata-sections"
|
||||
$ export CXXFLAGS="$CFLAGS"
|
||||
$ export CPPFLAGS="-D_3DS -D__3DS__ -I$PORTLIBS/include -I$CTRULIB/include"
|
||||
$ export LDFLAGS="-L$PORTLIBS/lib -L$CTRULIB/lib"
|
||||
$ export TOOL_PREFIX=arm-none-eabi-
|
||||
$ export CC=${TOOL_PREFIX}gcc
|
||||
$ export CXX=${TOOL_PREFIX}g++
|
||||
$ export AR=${TOOL_PREFIX}gcc-ar
|
||||
$ export RANLIB=${TOOL_PREFIX}gcc-ranlib
|
||||
$ export LIBS="-lctru"
|
||||
```
|
||||
|
||||
4.2) Compiling ScummVM
|
||||
----------------------
|
||||
Do the following in a fresh terminal.
|
||||
|
||||
In case you get a "compiler not found" message, add the toolchain's executables to your PATH:
|
||||
```$ export PATH=$DEVKITARM/bin:$PATH```
|
||||
|
||||
Note: In more recent codebases of ScummVM, you may or may not need to set the following beforehand:
|
||||
```$ export PKG_CONFIG_LIBDIR=$PORTLIBS/lib/pkgconfig```
|
||||
See above for $PORTLIBS.
|
||||
|
||||
ScummVM doesn't provide the CA certificates bundle required by the cloud synchronization features.
|
||||
You need to download it from the curl website: https://curl.haxx.se/ca/cacert.pem, and instruct
|
||||
the build system to package it in the binary:
|
||||
```$ export DIST_3DS_EXTRA_FILES=/path/to/cacert.pem```
|
||||
The name of the file must be `cacert.pem`.
|
||||
|
||||
From the root of the scummvm repository:
|
||||
```
|
||||
$ ./configure --host=3ds --enable-plugins --default-dynamic
|
||||
$ make
|
||||
```
|
||||
Additionally compile to specific formats to be used on the 3DS:
|
||||
```
|
||||
$ make scummvm.3dsx
|
||||
$ make scummvm.cia
|
||||
```
|
||||
|
||||
Assuming everything was successful, you'll be able to find the binary
|
||||
files in the root of your scummvm folder.
|
||||
|
||||
Note: for the CIA format, you will need the 'makerom' and 'bannertool' tools which are
|
||||
not supplied with devkitPro.
|
||||
|
||||
Note: using dynamic plugins as suggested is required when building with most or all of the
|
||||
game engines enabled in order to keep the memory usage low and avoid stability issues.
|
||||
83
backends/platform/3ds/allocHeapsOverride.cpp
Normal file
83
backends/platform/3ds/allocHeapsOverride.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// This file largely reuses code from libctru
|
||||
// https://github.com/devkitPro/libctru/blob/33a570b1c335cc38e50b7fef6bf4ed840bbd4ab2/libctru/source/system/allocateHeaps.c
|
||||
#include <3ds.h>
|
||||
|
||||
#define HEAP_SPLIT_SIZE_CAP (24 << 20) // 24MB
|
||||
#define LINEAR_HEAP_SIZE_CAP (32 << 20) // 32MB
|
||||
|
||||
// Hack to get the hardware's FCRAM layout ID. It just werks.
|
||||
// Layouts 0 through 5 are used exclusively in Old 3DS systems.
|
||||
// Layouts 6 through 8 are used exclusively in New 3DS systems.
|
||||
#define APPMEMTYPE (*(u32*)0x1FF80030)
|
||||
|
||||
extern "C" void __system_allocateHeaps(void) {
|
||||
extern char* fake_heap_start;
|
||||
extern char* fake_heap_end;
|
||||
extern u32 __ctru_heap;
|
||||
extern u32 __ctru_linear_heap;
|
||||
extern u32 __ctru_heap_size;
|
||||
extern u32 __ctru_linear_heap_size;
|
||||
Result rc;
|
||||
|
||||
// Retrieve handle to the resource limit object for our process
|
||||
Handle reslimit = 0;
|
||||
rc = svcGetResourceLimit(&reslimit, CUR_PROCESS_HANDLE);
|
||||
if (R_FAILED(rc))
|
||||
svcBreak(USERBREAK_PANIC);
|
||||
|
||||
// Retrieve information about total/used memory
|
||||
s64 maxCommit = 0, currentCommit = 0;
|
||||
ResourceLimitType reslimitType = RESLIMIT_COMMIT;
|
||||
svcGetResourceLimitLimitValues(&maxCommit, reslimit, &reslimitType, 1); // for APPLICATION this is equal to APPMEMALLOC at all times
|
||||
svcGetResourceLimitCurrentValues(¤tCommit, reslimit, &reslimitType, 1);
|
||||
svcCloseHandle(reslimit);
|
||||
|
||||
// Calculate how much remaining free memory is available
|
||||
u32 remaining = (u32)(maxCommit - currentCommit) &~ 0xFFF;
|
||||
|
||||
__ctru_heap_size = 0;
|
||||
// New 3DS needs more linear memory than Old 3DS to boot up ScummVM; app instantly crashes otherwise.
|
||||
// 0x00A00000 bytes = 10 MiB, for Old 3DS
|
||||
// 0x01400000 bytes = 20 MiB, for New 3DS
|
||||
__ctru_linear_heap_size = APPMEMTYPE < 6 ? 0x00A00000 : 0x01400000;
|
||||
__ctru_heap_size = remaining - __ctru_linear_heap_size;
|
||||
|
||||
// Allocate the application heap
|
||||
rc = svcControlMemory(&__ctru_heap, OS_HEAP_AREA_BEGIN, 0x0, __ctru_heap_size, MEMOP_ALLOC, static_cast<MemPerm>(static_cast<int>(MEMPERM_READ) | static_cast<int>(MEMPERM_WRITE)));
|
||||
if (R_FAILED(rc))
|
||||
svcBreak(USERBREAK_PANIC);
|
||||
|
||||
// Allocate the linear heap
|
||||
rc = svcControlMemory(&__ctru_linear_heap, 0x0, 0x0, __ctru_linear_heap_size, MEMOP_ALLOC_LINEAR, static_cast<MemPerm>(static_cast<int>(MEMPERM_READ) | static_cast<int>(MEMPERM_WRITE)));
|
||||
if (R_FAILED(rc))
|
||||
svcBreak(USERBREAK_PANIC);
|
||||
|
||||
// Mappable allocator init
|
||||
mappableInit(OS_MAP_AREA_BEGIN, OS_MAP_AREA_END);
|
||||
|
||||
// Set up newlib heap
|
||||
fake_heap_start = (char*)__ctru_heap;
|
||||
fake_heap_end = fake_heap_start + __ctru_heap_size;
|
||||
|
||||
}
|
||||
BIN
backends/platform/3ds/app/banner.png
Normal file
BIN
backends/platform/3ds/app/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
backends/platform/3ds/app/banner.wav
Normal file
BIN
backends/platform/3ds/app/banner.wav
Normal file
Binary file not shown.
BIN
backends/platform/3ds/app/icon.png
Normal file
BIN
backends/platform/3ds/app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
223
backends/platform/3ds/app/scummvm.rsf
Normal file
223
backends/platform/3ds/app/scummvm.rsf
Normal file
@@ -0,0 +1,223 @@
|
||||
BasicInfo:
|
||||
Title : ScummVM
|
||||
ProductCode : ScummVM
|
||||
Logo : Nintendo # Nintendo / Licensed / Distributed / iQue / iQueForSystem
|
||||
|
||||
TitleInfo:
|
||||
Category : Application
|
||||
UniqueId : 0xFF321
|
||||
|
||||
RomFs:
|
||||
# Specifies the root path of the read only file system to include in the ROM.
|
||||
RootPath : $(APP_ROMFS)
|
||||
|
||||
Option:
|
||||
UseOnSD : true # true if App is to be installed to SD
|
||||
FreeProductCode : true # Removes limitations on ProductCode
|
||||
MediaFootPadding : false # If true CCI files are created with padding
|
||||
EnableCrypt : false # Enables encryption for NCCH and CIA
|
||||
EnableCompress : false # Compresses where applicable (currently only exefs:/.code)
|
||||
|
||||
AccessControlInfo:
|
||||
CoreVersion : 2
|
||||
|
||||
# Exheader Format Version
|
||||
DescVersion : 2
|
||||
|
||||
# Minimum Required Kernel Version (below is for 4.5.0)
|
||||
ReleaseKernelMajor : "02"
|
||||
ReleaseKernelMinor : "33"
|
||||
|
||||
# ExtData
|
||||
UseExtSaveData : false # enables ExtData
|
||||
#ExtSaveDataId : 0x300 # only set this when the ID is different to the UniqueId
|
||||
|
||||
# FS:USER Archive Access Permissions
|
||||
# Uncomment as required
|
||||
FileSystemAccess:
|
||||
#- CategorySystemApplication
|
||||
#- CategoryHardwareCheck
|
||||
#- CategoryFileSystemTool
|
||||
#- Debug
|
||||
#- TwlCardBackup
|
||||
#- TwlNandData
|
||||
#- Boss
|
||||
- DirectSdmc
|
||||
#- Core
|
||||
#- CtrNandRo
|
||||
#- CtrNandRw
|
||||
#- CtrNandRoWrite
|
||||
#- CategorySystemSettings
|
||||
#- CardBoard
|
||||
#- ExportImportIvs
|
||||
#- DirectSdmcWrite
|
||||
#- SwitchCleanup
|
||||
#- SaveDataMove
|
||||
#- Shop
|
||||
#- Shell
|
||||
#- CategoryHomeMenu
|
||||
|
||||
# Process Settings
|
||||
MemoryType : Application # Application/System/Base
|
||||
SystemMode : 64MB # 64MB(Default)/96MB/80MB/72MB/32MB
|
||||
IdealProcessor : 0
|
||||
AffinityMask : 1
|
||||
Priority : 16
|
||||
MaxCpu : 0 # Let system decide
|
||||
HandleTableSize : 0x200
|
||||
DisableDebug : false
|
||||
EnableForceDebug : false
|
||||
CanWriteSharedPage : true
|
||||
CanUsePrivilegedPriority : false
|
||||
CanUseNonAlphabetAndNumber : true
|
||||
PermitMainFunctionArgument : true
|
||||
CanShareDeviceMemory : true
|
||||
RunnableOnSleep : false
|
||||
SpecialMemoryArrange : true
|
||||
|
||||
# New3DS Exclusive Process Settings
|
||||
SystemModeExt : 124MB # Legacy(Default)/124MB/178MB Legacy:Use Old3DS SystemMode
|
||||
CpuSpeed : 804MHz # 268MHz(Default)/804MHz
|
||||
EnableL2Cache : true # false(default)/true
|
||||
CanAccessCore2 : true
|
||||
|
||||
# Virtual Address Mappings
|
||||
IORegisterMapping:
|
||||
- 1ff00000-1ff7ffff # DSP memory
|
||||
MemoryMapping:
|
||||
- 1f000000-1f5fffff:r # VRAM
|
||||
|
||||
# Accessible SVCs, <Name>:<ID>
|
||||
SystemCallAccess:
|
||||
ArbitrateAddress: 34
|
||||
Break: 60
|
||||
CancelTimer: 28
|
||||
ClearEvent: 25
|
||||
ClearTimer: 29
|
||||
CloseHandle: 35
|
||||
ConnectToPort: 45
|
||||
ControlMemory: 1
|
||||
CreateAddressArbiter: 33
|
||||
CreateEvent: 23
|
||||
CreateMemoryBlock: 30
|
||||
CreateMutex: 19
|
||||
CreateSemaphore: 21
|
||||
CreateThread: 8
|
||||
CreateTimer: 26
|
||||
DuplicateHandle: 39
|
||||
ExitProcess: 3
|
||||
ExitThread: 9
|
||||
GetCurrentProcessorNumber: 17
|
||||
GetHandleInfo: 41
|
||||
GetProcessId: 53
|
||||
GetProcessIdOfThread: 54
|
||||
GetProcessIdealProcessor: 6
|
||||
GetProcessInfo: 43
|
||||
GetResourceLimit: 56
|
||||
GetResourceLimitCurrentValues: 58
|
||||
GetResourceLimitLimitValues: 57
|
||||
GetSystemInfo: 42
|
||||
GetSystemTick: 40
|
||||
GetThreadContext: 59
|
||||
GetThreadId: 55
|
||||
GetThreadIdealProcessor: 15
|
||||
GetThreadInfo: 44
|
||||
GetThreadPriority: 11
|
||||
MapMemoryBlock: 31
|
||||
OutputDebugString: 61
|
||||
QueryMemory: 2
|
||||
ReleaseMutex: 20
|
||||
ReleaseSemaphore: 22
|
||||
SendSyncRequest1: 46
|
||||
SendSyncRequest2: 47
|
||||
SendSyncRequest3: 48
|
||||
SendSyncRequest4: 49
|
||||
SendSyncRequest: 50
|
||||
SetThreadPriority: 12
|
||||
SetTimer: 27
|
||||
SignalEvent: 24
|
||||
SleepThread: 10
|
||||
UnmapMemoryBlock: 32
|
||||
WaitSynchronization1: 36
|
||||
WaitSynchronizationN: 37
|
||||
Backdoor: 123
|
||||
|
||||
# Service List
|
||||
# Maximum 34 services (32 if firmware is prior to 9.3.0)
|
||||
ServiceAccessControl:
|
||||
- cfg:u
|
||||
- fs:USER
|
||||
- gsp::Gpu
|
||||
- hid:USER
|
||||
- ndm:u
|
||||
- pxi:dev
|
||||
- APT:U
|
||||
- ac:u
|
||||
- act:u
|
||||
- am:net
|
||||
- boss:U
|
||||
- cam:u
|
||||
- cecd:u
|
||||
- dsp::DSP
|
||||
- frd:u
|
||||
- http:C
|
||||
- ir:USER
|
||||
- ir:u
|
||||
- ir:rst
|
||||
- ldr:ro
|
||||
- mic:u
|
||||
- news:u
|
||||
- nim:aoc
|
||||
- nwm::UDS
|
||||
- ptm:u
|
||||
- qtm:u
|
||||
- soc:U
|
||||
- ssl:C
|
||||
- y2r:u
|
||||
- gsp::Lcd
|
||||
|
||||
|
||||
SystemControlInfo:
|
||||
SaveDataSize: 0K
|
||||
RemasterVersion: 0
|
||||
StackSize: 0x40000
|
||||
|
||||
# Modules that run services listed above should be included below
|
||||
# Maximum 48 dependencies
|
||||
# If a module is listed that isn't present on the 3DS, the title will get stuck at the logo (3ds waves)
|
||||
# So act, nfc and qtm are commented for 4.x support. Uncomment if you need these.
|
||||
# <module name>:<module titleid>
|
||||
Dependency:
|
||||
ac: 0x0004013000002402
|
||||
#act: 0x0004013000003802
|
||||
am: 0x0004013000001502
|
||||
boss: 0x0004013000003402
|
||||
camera: 0x0004013000001602
|
||||
cecd: 0x0004013000002602
|
||||
cfg: 0x0004013000001702
|
||||
codec: 0x0004013000001802
|
||||
csnd: 0x0004013000002702
|
||||
dlp: 0x0004013000002802
|
||||
dsp: 0x0004013000001a02
|
||||
friends: 0x0004013000003202
|
||||
gpio: 0x0004013000001b02
|
||||
gsp: 0x0004013000001c02
|
||||
hid: 0x0004013000001d02
|
||||
http: 0x0004013000002902
|
||||
i2c: 0x0004013000001e02
|
||||
ir: 0x0004013000003302
|
||||
mcu: 0x0004013000001f02
|
||||
mic: 0x0004013000002002
|
||||
ndm: 0x0004013000002b02
|
||||
news: 0x0004013000003502
|
||||
#nfc: 0x0004013000004002
|
||||
nim: 0x0004013000002c02
|
||||
nwm: 0x0004013000002d02
|
||||
pdn: 0x0004013000002102
|
||||
ps: 0x0004013000003102
|
||||
ptm: 0x0004013000002202
|
||||
#qtm: 0x0004013020004202
|
||||
ro: 0x0004013000003702
|
||||
socket: 0x0004013000002e02
|
||||
spi: 0x0004013000002302
|
||||
ssl: 0x0004013000002f02
|
||||
85
backends/platform/3ds/main.cpp
Normal file
85
backends/platform/3ds/main.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "backends/platform/3ds/osystem.h"
|
||||
#include "backends/plugins/3ds/3ds-provider.h"
|
||||
|
||||
#include <3ds.h>
|
||||
#include <malloc.h>
|
||||
|
||||
enum {
|
||||
SYSTEM_MODEL_2DS = 3
|
||||
};
|
||||
|
||||
// Set the size of the stack.
|
||||
u32 __stacksize__ = 64 * 1024;
|
||||
|
||||
// Set the size of the linear heap to allow a larger application heap.
|
||||
// We do this in backends/platform/3ds/allocHeapsOverride.cpp
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
// Initialize basic libctru stuff
|
||||
cfguInit();
|
||||
gfxInitDefault();
|
||||
|
||||
// 800px wide top screen is not available on old 2DS systems
|
||||
u8 systemModel = 0;
|
||||
CFGU_GetSystemModel(&systemModel);
|
||||
gfxSetWide(systemModel != SYSTEM_MODEL_2DS);
|
||||
|
||||
romfsInit();
|
||||
osSetSpeedupEnable(true);
|
||||
// consoleInit(GFX_TOP, NULL);
|
||||
gdbHioDevInit();
|
||||
gdbHioDevRedirectStdStreams(true, true, true);
|
||||
|
||||
#ifdef USE_LIBCURL
|
||||
const uint32 soc_sharedmem_size = 0x10000;
|
||||
void *soc_sharedmem = memalign(0x1000, soc_sharedmem_size);
|
||||
socInit((u32 *)soc_sharedmem, soc_sharedmem_size);
|
||||
#endif
|
||||
|
||||
g_system = new N3DS::OSystem_3DS();
|
||||
assert(g_system);
|
||||
|
||||
#ifdef DYNAMIC_MODULES
|
||||
PluginManager::instance().addPluginProvider(new CTRPluginProvider());
|
||||
#endif
|
||||
|
||||
int res = scummvm_main(argc, argv);
|
||||
|
||||
g_system->destroy();
|
||||
|
||||
// Turn on both screen backlights before exiting.
|
||||
if (R_SUCCEEDED(gspLcdInit())) {
|
||||
GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH);
|
||||
gspLcdExit();
|
||||
}
|
||||
|
||||
#ifdef USE_LIBCURL
|
||||
socExit();
|
||||
#endif
|
||||
gdbHioDevExit();
|
||||
romfsExit();
|
||||
gfxExit();
|
||||
cfguExit();
|
||||
return res;
|
||||
}
|
||||
17
backends/platform/3ds/module.mk
Normal file
17
backends/platform/3ds/module.mk
Normal file
@@ -0,0 +1,17 @@
|
||||
MODULE := backends/platform/3ds
|
||||
|
||||
MODULE_OBJS := \
|
||||
main.o \
|
||||
allocHeapsOverride.o \
|
||||
shader.shbin.o \
|
||||
sprite.o \
|
||||
options.o \
|
||||
osystem.o \
|
||||
osystem-graphics.o \
|
||||
osystem-audio.o \
|
||||
osystem-events.o
|
||||
|
||||
# We don't use rules.mk but rather manually update OBJS and MODULE_DIRS.
|
||||
MODULE_OBJS := $(addprefix $(MODULE)/, $(MODULE_OBJS))
|
||||
OBJS := $(MODULE_OBJS) $(OBJS)
|
||||
MODULE_DIRS += $(sort $(dir $(MODULE_OBJS)))
|
||||
163
backends/platform/3ds/options.cpp
Normal file
163
backends/platform/3ds/options.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
|
||||
#include "backends/platform/3ds/osystem.h"
|
||||
|
||||
#include "gui/gui-manager.h"
|
||||
#include "gui/ThemeEval.h"
|
||||
#include "gui/widget.h"
|
||||
#include "gui/widgets/list.h"
|
||||
#include "gui/widgets/popup.h"
|
||||
|
||||
#include "common/translation.h"
|
||||
|
||||
namespace N3DS {
|
||||
|
||||
class N3DSOptionsWidget : public GUI::OptionsContainerWidget {
|
||||
public:
|
||||
explicit N3DSOptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain);
|
||||
~N3DSOptionsWidget() override;
|
||||
|
||||
// OptionsContainerWidget API
|
||||
void load() override;
|
||||
bool save() override;
|
||||
bool hasKeys() override;
|
||||
void setEnabled(bool e) override;
|
||||
|
||||
private:
|
||||
// OptionsContainerWidget API
|
||||
void defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const override;
|
||||
|
||||
GUI::CheckboxWidget *_showCursorCheckbox;
|
||||
GUI::CheckboxWidget *_snapToBorderCheckbox;
|
||||
GUI::CheckboxWidget *_stretchToFitCheckbox;
|
||||
|
||||
GUI::StaticTextWidget *_screenDesc;
|
||||
GUI::PopUpWidget *_screenPopUp;
|
||||
|
||||
bool _enabled;
|
||||
};
|
||||
|
||||
N3DSOptionsWidget::N3DSOptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain) :
|
||||
OptionsContainerWidget(boss, name, "N3DSOptionsDialog", domain), _enabled(true) {
|
||||
|
||||
_showCursorCheckbox = new GUI::CheckboxWidget(widgetsBoss(), "N3DSOptionsDialog.ShowCursor", _("Show mouse cursor"), Common::U32String(), 0, 'T');
|
||||
_snapToBorderCheckbox = new GUI::CheckboxWidget(widgetsBoss(), "N3DSOptionsDialog.SnapToBorder", _("Snap to edges"), Common::U32String(), 0, 'T');
|
||||
_stretchToFitCheckbox = new GUI::CheckboxWidget(widgetsBoss(), "N3DSOptionsDialog.StretchToFit", _("Stretch to fit"), Common::U32String(), 0, 'T');
|
||||
|
||||
_screenDesc = new GUI::StaticTextWidget(widgetsBoss(), "N3DSOptionsDialog.ScreenText", _("Use Screen:"));
|
||||
_screenPopUp = new GUI::PopUpWidget(widgetsBoss(), "N3DSOptionsDialog.Screen");
|
||||
_screenPopUp->appendEntry(_c("Top", "3ds-screen"), kScreenTop);
|
||||
_screenPopUp->appendEntry(_c("Bottom", "3ds-screen"), kScreenBottom);
|
||||
_screenPopUp->appendEntry(_c("Both", "3ds-screen"), kScreenBoth);
|
||||
}
|
||||
|
||||
N3DSOptionsWidget::~N3DSOptionsWidget() {
|
||||
}
|
||||
|
||||
void N3DSOptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const {
|
||||
layouts.addDialog(layoutName, overlayedLayout)
|
||||
.addLayout(GUI::ThemeLayout::kLayoutVertical)
|
||||
.addPadding(0, 0, 0, 0)
|
||||
.addWidget("ShowCursor", "Checkbox")
|
||||
.addWidget("SnapToBorder", "Checkbox")
|
||||
.addWidget("StretchToFit", "Checkbox")
|
||||
.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
|
||||
.addPadding(16, 16, 0, 0)
|
||||
.addWidget("ScreenText", "OptionsLabel")
|
||||
.addWidget("Screen", "PopUp")
|
||||
.closeLayout()
|
||||
.closeLayout()
|
||||
.closeDialog();
|
||||
}
|
||||
|
||||
void N3DSOptionsWidget::load() {
|
||||
_showCursorCheckbox->setState(ConfMan.getBool("3ds_showcursor", _domain));
|
||||
_snapToBorderCheckbox->setState(ConfMan.getBool("3ds_snaptoborder", _domain));
|
||||
_stretchToFitCheckbox->setState(ConfMan.getBool("3ds_stretchtofit", _domain));
|
||||
_screenPopUp->setSelectedTag(ConfMan.getInt("3ds_screen", _domain));
|
||||
}
|
||||
|
||||
bool N3DSOptionsWidget::save() {
|
||||
if (_enabled) {
|
||||
ConfMan.setBool("3ds_showcursor", _showCursorCheckbox->getState(), _domain);
|
||||
ConfMan.setBool("3ds_snaptoborder", _snapToBorderCheckbox->getState(), _domain);
|
||||
ConfMan.setBool("3ds_stretchtofit", _stretchToFitCheckbox->getState(), _domain);
|
||||
ConfMan.setInt("3ds_screen", _screenPopUp->getSelectedTag(), _domain);
|
||||
} else {
|
||||
ConfMan.removeKey("3ds_showcursor", _domain);
|
||||
ConfMan.removeKey("3ds_snaptoborder", _domain);
|
||||
ConfMan.removeKey("3ds_stretchtofit", _domain);
|
||||
ConfMan.removeKey("3ds_screen", _domain);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool N3DSOptionsWidget::hasKeys() {
|
||||
return ConfMan.hasKey("3ds_showcursor", _domain) ||
|
||||
ConfMan.hasKey("3ds_snaptoborder", _domain) ||
|
||||
ConfMan.hasKey("3ds_stretchtofit", _domain) ||
|
||||
ConfMan.hasKey("3ds_screen", _domain);
|
||||
}
|
||||
|
||||
void N3DSOptionsWidget::setEnabled(bool e) {
|
||||
_enabled = e;
|
||||
|
||||
_showCursorCheckbox->setEnabled(e);
|
||||
_snapToBorderCheckbox->setEnabled(e);
|
||||
_stretchToFitCheckbox->setEnabled(e);
|
||||
_screenDesc->setEnabled(e);
|
||||
_screenPopUp->setEnabled(e);
|
||||
}
|
||||
|
||||
|
||||
GUI::OptionsContainerWidget *OSystem_3DS::buildBackendOptionsWidget(GUI::GuiObject *boss, const Common::String &name, const Common::String &target) const {
|
||||
return new N3DSOptionsWidget(boss, name, target);
|
||||
}
|
||||
|
||||
void OSystem_3DS::registerDefaultSettings(const Common::String &target) const {
|
||||
ConfMan.registerDefault("3ds_showcursor", true);
|
||||
ConfMan.registerDefault("3ds_snaptoborder", true);
|
||||
ConfMan.registerDefault("3ds_stretchtofit", false);
|
||||
ConfMan.registerDefault("3ds_screen", kScreenBoth);
|
||||
}
|
||||
|
||||
void OSystem_3DS::applyBackendSettings() {
|
||||
int oldScreen = _screen;
|
||||
|
||||
_showCursor = ConfMan.getBool("3ds_showcursor");
|
||||
_snapToBorder = ConfMan.getBool("3ds_snaptoborder");
|
||||
_stretchToFit = ConfMan.getBool("3ds_stretchtofit");
|
||||
_screen = (Screen)ConfMan.getInt("3ds_screen");
|
||||
|
||||
updateBacklight();
|
||||
updateConfig();
|
||||
|
||||
if (_screen != oldScreen) {
|
||||
_screenChangeId++;
|
||||
g_gui.checkScreenChange();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace N3DS
|
||||
107
backends/platform/3ds/osystem-audio.cpp
Normal file
107
backends/platform/3ds/osystem-audio.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "backends/platform/3ds/osystem.h"
|
||||
#include "audio/mixer.h"
|
||||
|
||||
namespace N3DS {
|
||||
|
||||
static bool hasAudio = false;
|
||||
|
||||
static void audioThreadFunc(void *arg) {
|
||||
Audio::MixerImpl *mixer = (Audio::MixerImpl *)arg;
|
||||
OSystem_3DS *osys = dynamic_cast<OSystem_3DS *>(g_system);
|
||||
|
||||
const int channel = 0;
|
||||
int bufferIndex = 0;
|
||||
const int bufferCount = 2;
|
||||
const int sampleRate = mixer->getOutputRate();
|
||||
const int bufferSamples = 1024;
|
||||
const int bufferSize = bufferSamples * 4;
|
||||
|
||||
ndspWaveBuf buffers[bufferCount];
|
||||
|
||||
for (int i = 0; i < bufferCount; ++i) {
|
||||
memset(&buffers[i], 0, sizeof(ndspWaveBuf));
|
||||
buffers[i].data_vaddr = linearAlloc(bufferSize);
|
||||
buffers[i].looping = false;
|
||||
buffers[i].status = NDSP_WBUF_FREE;
|
||||
}
|
||||
|
||||
ndspChnReset(channel);
|
||||
ndspChnSetInterp(channel, NDSP_INTERP_LINEAR);
|
||||
ndspChnSetRate(channel, sampleRate);
|
||||
ndspChnSetFormat(channel, NDSP_FORMAT_STEREO_PCM16);
|
||||
|
||||
while (!osys->exiting) {
|
||||
svcSleepThread(5000 * 1000); // Wake up the thread every 5 ms
|
||||
|
||||
if (osys->sleeping) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ndspWaveBuf *buf = &buffers[bufferIndex];
|
||||
if (buf->status == NDSP_WBUF_FREE || buf->status == NDSP_WBUF_DONE) {
|
||||
buf->nsamples = mixer->mixCallback(buf->data_adpcm, bufferSize);
|
||||
if (buf->nsamples > 0) {
|
||||
DSP_FlushDataCache(buf->data_vaddr, bufferSize);
|
||||
ndspChnWaveBufAdd(channel, buf);
|
||||
}
|
||||
|
||||
bufferIndex++;
|
||||
bufferIndex %= bufferCount;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < bufferCount; ++i)
|
||||
linearFree(buffers[i].data_pcm16);
|
||||
}
|
||||
|
||||
void OSystem_3DS::initAudio() {
|
||||
_mixer = new Audio::MixerImpl(22050);
|
||||
|
||||
hasAudio = R_SUCCEEDED(ndspInit());
|
||||
_mixer->setReady(false);
|
||||
|
||||
if (hasAudio) {
|
||||
s32 prio = 0;
|
||||
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
||||
audioThread = threadCreate(&audioThreadFunc, _mixer, 32 * 1048, prio - 1, -2, false);
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::destroyAudio() {
|
||||
if (hasAudio) {
|
||||
threadJoin(audioThread, U64_MAX);
|
||||
threadFree(audioThread);
|
||||
ndspExit();
|
||||
}
|
||||
|
||||
delete _mixer;
|
||||
_mixer = 0;
|
||||
}
|
||||
|
||||
Audio::Mixer *OSystem_3DS::getMixer() {
|
||||
assert(_mixer);
|
||||
return _mixer;
|
||||
}
|
||||
|
||||
} // namespace N3DS
|
||||
430
backends/platform/3ds/osystem-events.cpp
Normal file
430
backends/platform/3ds/osystem-events.cpp
Normal file
@@ -0,0 +1,430 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
|
||||
#include "backends/platform/3ds/osystem.h"
|
||||
|
||||
#include "backends/keymapper/action.h"
|
||||
#include "backends/keymapper/keymapper-defaults.h"
|
||||
#include "backends/keymapper/hardware-input.h"
|
||||
#include "backends/keymapper/keymap.h"
|
||||
#include "backends/keymapper/keymapper.h"
|
||||
#include "backends/keymapper/standard-actions.h"
|
||||
#include "backends/timer/default/default-timer.h"
|
||||
#include "common/translation.h"
|
||||
#include "engines/engine.h"
|
||||
#include "gui/gui-manager.h"
|
||||
|
||||
namespace N3DS {
|
||||
|
||||
static Common::Mutex *eventMutex;
|
||||
static InputMode inputMode = MODE_DRAG;
|
||||
static InputMode savedInputMode = MODE_DRAG;
|
||||
static aptHookCookie cookie;
|
||||
|
||||
static const Common::HardwareInputTableEntry ctrJoystickButtons[] = {
|
||||
{ "JOY_A", Common::JOYSTICK_BUTTON_A, _s("A") },
|
||||
{ "JOY_B", Common::JOYSTICK_BUTTON_B, _s("B") },
|
||||
{ "JOY_X", Common::JOYSTICK_BUTTON_X, _s("X") },
|
||||
{ "JOY_Y", Common::JOYSTICK_BUTTON_Y, _s("Y") },
|
||||
{ "JOY_BACK", Common::JOYSTICK_BUTTON_BACK, _s("Select") },
|
||||
{ "JOY_START", Common::JOYSTICK_BUTTON_START, _s("Start") },
|
||||
{ "JOY_LEFT_STICK", Common::JOYSTICK_BUTTON_LEFT_STICK, _s("ZL") },
|
||||
{ "JOY_RIGHT_STICK", Common::JOYSTICK_BUTTON_RIGHT_STICK, _s("ZR") },
|
||||
{ "JOY_LEFT_SHOULDER", Common::JOYSTICK_BUTTON_LEFT_SHOULDER, _s("L") },
|
||||
{ "JOY_RIGHT_SHOULDER", Common::JOYSTICK_BUTTON_RIGHT_SHOULDER, _s("R") },
|
||||
{ "JOY_UP", Common::JOYSTICK_BUTTON_DPAD_UP, _s("D-pad Up") },
|
||||
{ "JOY_DOWN", Common::JOYSTICK_BUTTON_DPAD_DOWN, _s("D-pad Down") },
|
||||
{ "JOY_LEFT", Common::JOYSTICK_BUTTON_DPAD_LEFT, _s("D-pad Left") },
|
||||
{ "JOY_RIGHT", Common::JOYSTICK_BUTTON_DPAD_RIGHT, _s("D-pad Right") },
|
||||
{ nullptr, 0, nullptr }
|
||||
};
|
||||
|
||||
static const Common::AxisTableEntry ctrJoystickAxes[] = {
|
||||
{ "JOY_LEFT_STICK_X", Common::JOYSTICK_AXIS_LEFT_STICK_X, Common::kAxisTypeFull, _s("C-Pad X") },
|
||||
{ "JOY_LEFT_STICK_Y", Common::JOYSTICK_AXIS_LEFT_STICK_Y, Common::kAxisTypeFull, _s("C-Pad Y") },
|
||||
{ nullptr, 0, Common::kAxisTypeFull, nullptr }
|
||||
};
|
||||
|
||||
const Common::HardwareInputTableEntry ctrMouseButtons[] = {
|
||||
{ "MOUSE_LEFT", Common::MOUSE_BUTTON_LEFT, _s("Touch") },
|
||||
{ nullptr, 0, nullptr }
|
||||
};
|
||||
|
||||
static const int16 CIRCLE_MAX = 160;
|
||||
|
||||
static void pushEventQueue(Common::Queue<Common::Event> *queue, Common::Event &event) {
|
||||
Common::StackLock lock(*eventMutex);
|
||||
queue->push(event);
|
||||
}
|
||||
|
||||
static void doJoyEvent(Common::Queue<Common::Event> *queue, u32 keysPressed, u32 keysReleased, u32 ctrKey, uint8 svmButton) {
|
||||
if (keysPressed & ctrKey || keysReleased & ctrKey) {
|
||||
Common::Event event;
|
||||
event.type = (keysPressed & ctrKey) ? Common::EVENT_JOYBUTTON_DOWN : Common::EVENT_JOYBUTTON_UP;
|
||||
event.joystick.button = svmButton;
|
||||
|
||||
pushEventQueue(queue, event);
|
||||
}
|
||||
}
|
||||
|
||||
static void eventThreadFunc(void *arg) {
|
||||
OSystem_3DS *osys = dynamic_cast<OSystem_3DS *>(g_system);
|
||||
Common::Queue<Common::Event> *eventQueue = (Common::Queue<Common::Event> *)arg;
|
||||
|
||||
uint32 touchStartTime = osys->getMillis();
|
||||
touchPosition lastTouch = {0, 0};
|
||||
circlePosition lastCircle = {0, 0};
|
||||
int borderSnapZone = 6;
|
||||
Common::Event event;
|
||||
|
||||
while (!osys->exiting) {
|
||||
do {
|
||||
osys->delayMillis(10);
|
||||
} while (osys->sleeping && !osys->exiting);
|
||||
|
||||
hidScanInput();
|
||||
u32 held = hidKeysHeld();
|
||||
u32 keysPressed = hidKeysDown();
|
||||
u32 keysReleased = hidKeysUp();
|
||||
|
||||
// Touch screen events
|
||||
if (held & KEY_TOUCH) {
|
||||
touchPosition touch;
|
||||
hidTouchRead(&touch);
|
||||
if (osys->_snapToBorder) {
|
||||
if (touch.px < borderSnapZone) {
|
||||
touch.px = 0;
|
||||
}
|
||||
if (touch.px > 319 - borderSnapZone) {
|
||||
touch.px = 319;
|
||||
}
|
||||
if (touch.py < borderSnapZone) {
|
||||
touch.py = 0;
|
||||
}
|
||||
if (touch.py > 239 - borderSnapZone) {
|
||||
touch.py = 239;
|
||||
}
|
||||
}
|
||||
|
||||
osys->transformPoint(touch);
|
||||
|
||||
event.mouse.x = touch.px;
|
||||
event.mouse.y = touch.py;
|
||||
|
||||
if (keysPressed & KEY_TOUCH) {
|
||||
touchStartTime = osys->getMillis();
|
||||
if (inputMode == MODE_DRAG) {
|
||||
event.type = Common::EVENT_LBUTTONDOWN;
|
||||
pushEventQueue(eventQueue, event);
|
||||
}
|
||||
} else if (touch.px != lastTouch.px || touch.py != lastTouch.py) {
|
||||
event.type = Common::EVENT_MOUSEMOVE;
|
||||
pushEventQueue(eventQueue, event);
|
||||
}
|
||||
|
||||
lastTouch = touch;
|
||||
} else if (keysReleased & KEY_TOUCH) {
|
||||
event.mouse.x = lastTouch.px;
|
||||
event.mouse.y = lastTouch.py;
|
||||
if (inputMode == MODE_DRAG) {
|
||||
event.type = Common::EVENT_LBUTTONUP;
|
||||
pushEventQueue(eventQueue, event);
|
||||
} else if (osys->getMillis() - touchStartTime < 200) {
|
||||
// Process click in MODE_HOVER
|
||||
event.type = Common::EVENT_MOUSEMOVE;
|
||||
pushEventQueue(eventQueue, event);
|
||||
event.type = Common::EVENT_LBUTTONDOWN;
|
||||
pushEventQueue(eventQueue, event);
|
||||
event.type = Common::EVENT_LBUTTONUP;
|
||||
pushEventQueue(eventQueue, event);
|
||||
}
|
||||
}
|
||||
|
||||
// C-Pad events
|
||||
circlePosition circle;
|
||||
hidCircleRead(&circle);
|
||||
|
||||
if (circle.dx != lastCircle.dx) {
|
||||
int32 position = (int32)circle.dx * Common::JOYAXIS_MAX / CIRCLE_MAX;
|
||||
|
||||
event.type = Common::EVENT_JOYAXIS_MOTION;
|
||||
event.joystick.axis = Common::JOYSTICK_AXIS_LEFT_STICK_X;
|
||||
event.joystick.position = CLIP<int32>(position, Common::JOYAXIS_MIN, Common::JOYAXIS_MAX);
|
||||
pushEventQueue(eventQueue, event);
|
||||
}
|
||||
|
||||
if (circle.dy != lastCircle.dy) {
|
||||
int32 position = -(int32)circle.dy * Common::JOYAXIS_MAX / CIRCLE_MAX;
|
||||
|
||||
event.type = Common::EVENT_JOYAXIS_MOTION;
|
||||
event.joystick.axis = Common::JOYSTICK_AXIS_LEFT_STICK_Y;
|
||||
event.joystick.position = CLIP<int32>(position, Common::JOYAXIS_MIN, Common::JOYAXIS_MAX);
|
||||
pushEventQueue(eventQueue, event);
|
||||
}
|
||||
|
||||
lastCircle = circle;
|
||||
|
||||
// Button events
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_L, Common::JOYSTICK_BUTTON_LEFT_SHOULDER);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_R, Common::JOYSTICK_BUTTON_RIGHT_SHOULDER);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_A, Common::JOYSTICK_BUTTON_A);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_B, Common::JOYSTICK_BUTTON_B);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_X, Common::JOYSTICK_BUTTON_X);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_Y, Common::JOYSTICK_BUTTON_Y);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_DUP, Common::JOYSTICK_BUTTON_DPAD_UP);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_DDOWN, Common::JOYSTICK_BUTTON_DPAD_DOWN);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_DLEFT, Common::JOYSTICK_BUTTON_DPAD_LEFT);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_DRIGHT, Common::JOYSTICK_BUTTON_DPAD_RIGHT);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_START, Common::JOYSTICK_BUTTON_START);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_SELECT, Common::JOYSTICK_BUTTON_BACK);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_ZL, Common::JOYSTICK_BUTTON_LEFT_STICK);
|
||||
doJoyEvent(eventQueue, keysPressed, keysReleased, KEY_ZR, Common::JOYSTICK_BUTTON_RIGHT_STICK);
|
||||
}
|
||||
}
|
||||
|
||||
static void aptHookFunc(APT_HookType hookType, void *param) {
|
||||
OSystem_3DS *osys = dynamic_cast<OSystem_3DS *>(g_system);
|
||||
|
||||
switch (hookType) {
|
||||
case APTHOOK_ONSUSPEND:
|
||||
case APTHOOK_ONSLEEP:
|
||||
if (g_engine) {
|
||||
osys->_sleepPauseToken = g_engine->pauseEngine();
|
||||
}
|
||||
osys->sleeping = true;
|
||||
if (R_SUCCEEDED(gspLcdInit())) {
|
||||
GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH);
|
||||
gspLcdExit();
|
||||
}
|
||||
break;
|
||||
case APTHOOK_ONRESTORE:
|
||||
case APTHOOK_ONWAKEUP:
|
||||
if (g_engine) {
|
||||
osys->_sleepPauseToken.clear();
|
||||
}
|
||||
osys->sleeping = false;
|
||||
osys->updateBacklight();
|
||||
osys->updateConfig();
|
||||
break;
|
||||
case APTHOOK_ONEXIT:
|
||||
break;
|
||||
default:
|
||||
warning("Unhandled APT hook, type: %d", hookType);
|
||||
}
|
||||
}
|
||||
|
||||
static void timerThreadFunc(void *arg) {
|
||||
OSystem_3DS *osys = (OSystem_3DS *)arg;
|
||||
DefaultTimerManager *tm = (DefaultTimerManager *)osys->getTimerManager();
|
||||
while (!osys->exiting) {
|
||||
g_system->delayMillis(10);
|
||||
tm->handler();
|
||||
}
|
||||
}
|
||||
|
||||
Common::HardwareInputSet *OSystem_3DS::getHardwareInputSet() {
|
||||
using namespace Common;
|
||||
|
||||
CompositeHardwareInputSet *inputSet = new CompositeHardwareInputSet();
|
||||
// Touch input sends mouse events for now, so we need to declare we have a mouse...
|
||||
inputSet->addHardwareInputSet(new MouseHardwareInputSet(ctrMouseButtons));
|
||||
inputSet->addHardwareInputSet(new JoystickHardwareInputSet(ctrJoystickButtons, ctrJoystickAxes));
|
||||
|
||||
return inputSet;
|
||||
}
|
||||
|
||||
void OSystem_3DS::initEvents() {
|
||||
eventMutex = new Common::Mutex();
|
||||
s32 prio = 0;
|
||||
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
||||
_timerThread = threadCreate(&timerThreadFunc, this, 32 * 1024, prio - 1, -2, false);
|
||||
_eventThread = threadCreate(&eventThreadFunc, &_eventQueue, 32 * 1024, prio - 1, -2, false);
|
||||
|
||||
aptHook(&cookie, aptHookFunc, this);
|
||||
_eventManager->getEventDispatcher()->registerObserver(this, 10, false);
|
||||
}
|
||||
|
||||
void OSystem_3DS::destroyEvents() {
|
||||
_eventManager->getEventDispatcher()->unregisterObserver(this);
|
||||
|
||||
threadJoin(_timerThread, U64_MAX);
|
||||
threadFree(_timerThread);
|
||||
|
||||
threadJoin(_eventThread, U64_MAX);
|
||||
threadFree(_eventThread);
|
||||
delete eventMutex;
|
||||
}
|
||||
|
||||
void OSystem_3DS::transformPoint(touchPosition &point) {
|
||||
if (!_overlayInGUI) {
|
||||
point.px = static_cast<float>(point.px) / _gameBottomTexture.getScaleX() - _gameBottomTexture.getPosX();
|
||||
point.py = static_cast<float>(point.py) / _gameBottomTexture.getScaleY() - _gameBottomTexture.getPosY();
|
||||
}
|
||||
|
||||
clipPoint(point);
|
||||
}
|
||||
|
||||
void OSystem_3DS::clipPoint(touchPosition &point) {
|
||||
if (_overlayInGUI) {
|
||||
point.px = CLIP<uint16>(point.px, 0, getOverlayWidth() - 1);
|
||||
point.py = CLIP<uint16>(point.py, 0, getOverlayHeight() - 1);
|
||||
} else {
|
||||
point.px = CLIP<uint16>(point.px, 0, _gameTopTexture.actualWidth - 1);
|
||||
point.py = CLIP<uint16>(point.py, 0, _gameTopTexture.actualHeight - 1);
|
||||
}
|
||||
}
|
||||
|
||||
enum _3DSCustomEvent {
|
||||
k3DSEventToggleDragMode,
|
||||
k3DSEventToggleMagnifyMode
|
||||
};
|
||||
|
||||
Common::KeymapArray OSystem_3DS::getGlobalKeymaps() {
|
||||
using namespace Common;
|
||||
|
||||
Keymap *keymap = new Keymap(Keymap::kKeymapTypeGlobal, "3ds", "3DS");
|
||||
|
||||
Action *act;
|
||||
|
||||
act = new Action("DRAGM", _("Toggle Drag Mode"));
|
||||
act->setCustomBackendActionEvent(k3DSEventToggleDragMode);
|
||||
act->addDefaultInputMapping("JOY_RIGHT_SHOULDER");
|
||||
keymap->addAction(act);
|
||||
|
||||
act = new Action("MAGM", _("Toggle Magnify Mode"));
|
||||
act->setCustomBackendActionEvent(k3DSEventToggleMagnifyMode);
|
||||
act->addDefaultInputMapping("JOY_LEFT_SHOULDER");
|
||||
keymap->addAction(act);
|
||||
|
||||
return Keymap::arrayOf(keymap);
|
||||
}
|
||||
|
||||
Common::KeymapperDefaultBindings *OSystem_3DS::getKeymapperDefaultBindings() {
|
||||
Common::KeymapperDefaultBindings *keymapperDefaultBindings = new Common::KeymapperDefaultBindings();
|
||||
|
||||
// Unmap the main menu standard action so LEFT_SHOULDER can be used for drag mode
|
||||
keymapperDefaultBindings->setDefaultBinding("engine-default", Common::kStandardActionOpenMainMenu, "");
|
||||
|
||||
return keymapperDefaultBindings;
|
||||
}
|
||||
|
||||
bool OSystem_3DS::pollEvent(Common::Event &event) {
|
||||
if (!aptMainLoop()) {
|
||||
// The system requested us to quit
|
||||
if (_sleepPauseToken.isActive()) {
|
||||
_sleepPauseToken.clear();
|
||||
}
|
||||
|
||||
event.type = Common::EVENT_QUIT;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If magnify mode is on when returning to Launcher, turn it off
|
||||
if (_eventManager->shouldReturnToLauncher()) {
|
||||
if (_magnifyMode == MODE_MAGON) {
|
||||
_magnifyMode = MODE_MAGOFF;
|
||||
updateSize();
|
||||
if (savedInputMode == MODE_DRAG) {
|
||||
inputMode = savedInputMode;
|
||||
displayMessageOnOSD(_("Magnify Mode Off. Reactivating Drag Mode.\nReturning to Launcher..."));
|
||||
} else {
|
||||
displayMessageOnOSD(_("Magnify Mode Off. Returning to Launcher..."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common::StackLock lock(*eventMutex);
|
||||
|
||||
if (_eventQueue.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
event = _eventQueue.pop();
|
||||
|
||||
if (Common::isMouseEvent(event)) {
|
||||
warpMouse(event.mouse.x, event.mouse.y);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OSystem_3DS::notifyEvent(const Common::Event &event) {
|
||||
if (event.type != Common::EVENT_CUSTOM_BACKEND_ACTION_START
|
||||
&& event.type != Common::EVENT_CUSTOM_BACKEND_ACTION_END) {
|
||||
return false; // We're only interested in custom backend events
|
||||
}
|
||||
|
||||
if (event.type == Common::EVENT_CUSTOM_BACKEND_ACTION_END) {
|
||||
return true; // We'll say we have handled the event so it is not propagated
|
||||
}
|
||||
|
||||
switch ((_3DSCustomEvent)event.customType) {
|
||||
case k3DSEventToggleDragMode:
|
||||
if (inputMode == MODE_DRAG) {
|
||||
inputMode = savedInputMode = MODE_HOVER;
|
||||
displayMessageOnOSD(_("Hover Mode"));
|
||||
} else {
|
||||
if (_magnifyMode == MODE_MAGOFF) {
|
||||
inputMode = savedInputMode = MODE_DRAG;
|
||||
displayMessageOnOSD(_("Drag Mode"));
|
||||
} else {
|
||||
displayMessageOnOSD(_("Cannot Switch to Drag Mode while Magnify Mode is On"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
case k3DSEventToggleMagnifyMode:
|
||||
if (_overlayVisible) {
|
||||
displayMessageOnOSD(_("Magnify Mode cannot be activated in menus."));
|
||||
} else if (_screen != kScreenBoth && _magnifyMode == MODE_MAGOFF) {
|
||||
// TODO: Automatically enable both screens while magnify mode is on
|
||||
displayMessageOnOSD(_("Magnify Mode can only be activated\n when both screens are enabled."));
|
||||
} else if (_gameWidth <= 400 && _gameHeight <= 240) {
|
||||
displayMessageOnOSD(_("In-game resolution too small to magnify."));
|
||||
} else {
|
||||
if (_magnifyMode == MODE_MAGOFF) {
|
||||
_magnifyMode = MODE_MAGON;
|
||||
if (inputMode == MODE_DRAG) {
|
||||
inputMode = MODE_HOVER;
|
||||
displayMessageOnOSD(_("Magnify Mode On. Switching to Hover Mode..."));
|
||||
} else {
|
||||
displayMessageOnOSD(_("Magnify Mode On"));
|
||||
}
|
||||
} else {
|
||||
_magnifyMode = MODE_MAGOFF;
|
||||
updateSize();
|
||||
if (savedInputMode == MODE_DRAG) {
|
||||
inputMode = savedInputMode;
|
||||
displayMessageOnOSD(_("Magnify Mode Off. Reactivating Drag Mode..."));
|
||||
} else {
|
||||
displayMessageOnOSD(_("Magnify Mode Off"));
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace N3DS
|
||||
887
backends/platform/3ds/osystem-graphics.cpp
Normal file
887
backends/platform/3ds/osystem-graphics.cpp
Normal file
@@ -0,0 +1,887 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This _program is free software; you can redistribute it and/or
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU 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.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "backends/platform/3ds/osystem.h"
|
||||
#include "backends/platform/3ds/shader_shbin.h"
|
||||
#include "common/rect.h"
|
||||
#include "graphics/blit.h"
|
||||
#include "graphics/fontman.h"
|
||||
#include "gui/gui-manager.h"
|
||||
|
||||
// Used to transfer the final rendered display to the framebuffer
|
||||
#define DISPLAY_TRANSFER_FLAGS \
|
||||
(GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | \
|
||||
GX_TRANSFER_RAW_COPY(0) | GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGB8) | \
|
||||
GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | \
|
||||
GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO))
|
||||
#define TEXTURE_TRANSFER_FLAGS(in, out) \
|
||||
(GX_TRANSFER_FLIP_VERT(1) | GX_TRANSFER_OUT_TILED(1) | \
|
||||
GX_TRANSFER_RAW_COPY(0) | GX_TRANSFER_IN_FORMAT(in) | \
|
||||
GX_TRANSFER_OUT_FORMAT(out) | GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO))
|
||||
#define DEFAULT_MODE _modeRGBA8
|
||||
|
||||
namespace N3DS {
|
||||
/* Group the various enums, values, etc. needed for
|
||||
* each graphics mode into instaces of GfxMode3DS */
|
||||
static const GfxMode3DS _modeRGBA8 = { Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0),
|
||||
GPU_RGBA8, TEXTURE_TRANSFER_FLAGS(GX_TRANSFER_FMT_RGBA8, GX_TRANSFER_FMT_RGBA8) };
|
||||
static const GfxMode3DS _modeRGBX8 = { Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0),
|
||||
GPU_RGB8, TEXTURE_TRANSFER_FLAGS(GX_TRANSFER_FMT_RGBA8, GX_TRANSFER_FMT_RGB8) };
|
||||
static const GfxMode3DS _modeRGB565 = { Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0),
|
||||
GPU_RGB565, TEXTURE_TRANSFER_FLAGS(GX_TRANSFER_FMT_RGB565, GX_TRANSFER_FMT_RGB565) };
|
||||
static const GfxMode3DS _modeRGB5A1 = { Graphics::PixelFormat(2, 5, 5, 5, 1, 11, 6, 1, 0),
|
||||
GPU_RGBA5551, TEXTURE_TRANSFER_FLAGS(GX_TRANSFER_FMT_RGB5A1, GX_TRANSFER_FMT_RGB5A1) };
|
||||
static const GfxMode3DS _modeRGBA4 = { Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0),
|
||||
GPU_RGBA4, TEXTURE_TRANSFER_FLAGS(GX_TRANSFER_FMT_RGBA4, GX_TRANSFER_FMT_RGBA4) };
|
||||
static const GfxMode3DS _modeCLUT8 = _modeRGBX8;
|
||||
|
||||
static const GfxMode3DS *gfxModes[] = { &_modeRGBX8, &_modeRGB565, &_modeRGB5A1, &_modeRGBA4, &_modeCLUT8 };
|
||||
|
||||
|
||||
void OSystem_3DS::init3DSGraphics() {
|
||||
_gfxState.gfxMode = gfxModes[CLUT8];
|
||||
_pfGame = Graphics::PixelFormat::createFormatCLUT8();
|
||||
_pfDefaultTexture = Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0);
|
||||
|
||||
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
|
||||
|
||||
// Initialize the render targets
|
||||
|
||||
int topScreenWidth = gfxIsWide() ? 800 : 400;
|
||||
|
||||
_renderTargetTop =
|
||||
C3D_RenderTargetCreate(240, topScreenWidth, GPU_RB_RGB8, -1);
|
||||
C3D_RenderTargetClear(_renderTargetTop, C3D_CLEAR_ALL, 0x0000000, 0);
|
||||
C3D_RenderTargetSetOutput(_renderTargetTop, GFX_TOP, GFX_LEFT,
|
||||
DISPLAY_TRANSFER_FLAGS);
|
||||
|
||||
_renderTargetBottom =
|
||||
C3D_RenderTargetCreate(240, 320, GPU_RB_RGB8, -1);
|
||||
C3D_RenderTargetClear(_renderTargetBottom, C3D_CLEAR_ALL, 0x00000000, 0);
|
||||
C3D_RenderTargetSetOutput(_renderTargetBottom, GFX_BOTTOM, GFX_LEFT,
|
||||
DISPLAY_TRANSFER_FLAGS);
|
||||
|
||||
// Load and bind simple default shader (shader.v.pica)
|
||||
_dvlb = DVLB_ParseFile((u32*)const_cast<u8 *>(shader_shbin), shader_shbin_size);
|
||||
shaderProgramInit(&_program);
|
||||
shaderProgramSetVsh(&_program, &_dvlb->DVLE[0]);
|
||||
C3D_BindProgram(&_program);
|
||||
|
||||
_projectionLocation = shaderInstanceGetUniformLocation(_program.vertexShader, "projection");
|
||||
_modelviewLocation = shaderInstanceGetUniformLocation(_program.vertexShader, "modelView");
|
||||
|
||||
C3D_AttrInfo *attrInfo = C3D_GetAttrInfo();
|
||||
AttrInfo_Init(attrInfo);
|
||||
AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3); // v0=position
|
||||
AttrInfo_AddLoader(attrInfo, 1, GPU_FLOAT, 2); // v1=texcoord
|
||||
|
||||
Mtx_OrthoTilt(&_projectionTop, 0.0, 400.0, 240.0, 0.0, 0.0, 1.0, true);
|
||||
Mtx_OrthoTilt(&_projectionBottom, 0.0, 320.0, 240.0, 0.0, 0.0, 1.0, true);
|
||||
|
||||
C3D_TexEnv *env = C3D_GetTexEnv(0);
|
||||
C3D_TexEnvSrc(env, C3D_Both, GPU_TEXTURE0, GPU_PRIMARY_COLOR, GPU_PRIMARY_COLOR);
|
||||
C3D_TexEnvOpRgb(env, GPU_TEVOP_RGB_SRC_COLOR, GPU_TEVOP_RGB_SRC_COLOR, GPU_TEVOP_RGB_SRC_COLOR);
|
||||
C3D_TexEnvFunc(env, C3D_Both, GPU_REPLACE);
|
||||
|
||||
C3D_DepthTest(false, GPU_GEQUAL, GPU_WRITE_ALL);
|
||||
C3D_CullFace(GPU_CULL_NONE);
|
||||
|
||||
// _overlay initialized in updateSize()
|
||||
}
|
||||
|
||||
void OSystem_3DS::destroy3DSGraphics() {
|
||||
_gameScreen.free();
|
||||
_cursor.free();
|
||||
|
||||
shaderProgramFree(&_program);
|
||||
DVLB_Free(_dvlb);
|
||||
|
||||
C3D_RenderTargetDelete(_renderTargetTop);
|
||||
C3D_RenderTargetDelete(_renderTargetBottom);
|
||||
|
||||
C3D_Fini();
|
||||
}
|
||||
|
||||
bool OSystem_3DS::hasFeature(OSystem::Feature f) {
|
||||
return (f == OSystem::kFeatureCursorPalette ||
|
||||
f == OSystem::kFeatureCursorAlpha ||
|
||||
f == OSystem::kFeatureFilteringMode ||
|
||||
f == OSystem::kFeatureOverlaySupportsAlpha ||
|
||||
f == OSystem::kFeatureKbdMouseSpeed ||
|
||||
f == OSystem::kFeatureJoystickDeadzone);
|
||||
}
|
||||
|
||||
void OSystem_3DS::setFeatureState(OSystem::Feature f, bool enable) {
|
||||
switch (f) {
|
||||
case OSystem::kFeatureCursorPalette:
|
||||
_cursorPaletteEnabled = enable;
|
||||
flushCursor();
|
||||
break;
|
||||
case OSystem::kFeatureFilteringMode:
|
||||
_filteringEnabled = enable;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool OSystem_3DS::getFeatureState(OSystem::Feature f) {
|
||||
switch (f) {
|
||||
case OSystem::kFeatureCursorPalette:
|
||||
return _cursorPaletteEnabled;
|
||||
case OSystem::kFeatureFilteringMode:
|
||||
return _filteringEnabled;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
GraphicsModeID OSystem_3DS::chooseMode(Graphics::PixelFormat *format) {
|
||||
if (format->bytesPerPixel > 2) {
|
||||
return RGBA8;
|
||||
} else if (format->bytesPerPixel > 1) {
|
||||
if (format->aBits() > 1) {
|
||||
return RGBA4;
|
||||
} else if (format->gBits() > 5) {
|
||||
return RGB565;
|
||||
} else {
|
||||
return RGB5A1;
|
||||
}
|
||||
}
|
||||
return CLUT8;
|
||||
}
|
||||
|
||||
bool OSystem_3DS::setGraphicsMode(GraphicsModeID modeID) {
|
||||
switch (modeID) {
|
||||
case RGBA8:
|
||||
case RGB565:
|
||||
case RGB5A1:
|
||||
case RGBA4:
|
||||
case CLUT8:
|
||||
_gfxState.gfxMode = gfxModes[modeID];
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::initSize(uint width, uint height,
|
||||
const Graphics::PixelFormat *format) {
|
||||
debug("3ds initsize w:%d h:%d", width, height);
|
||||
updateBacklight();
|
||||
updateConfig();
|
||||
|
||||
_gameWidth = width;
|
||||
_gameHeight = height;
|
||||
_magCenterX = _magWidth / 2;
|
||||
_magCenterY = _magHeight / 2;
|
||||
|
||||
_oldPfGame = _pfGame;
|
||||
if (!format) {
|
||||
_pfGame = Graphics::PixelFormat::createFormatCLUT8();
|
||||
} else {
|
||||
debug("pixelformat: %d %d %d %d %d", format->bytesPerPixel, format->rBits(), format->gBits(), format->bBits(), format->aBits());
|
||||
_pfGame = *format;
|
||||
}
|
||||
|
||||
/* If the current graphics mode does not fit with the pixel
|
||||
* format being requested, choose one that does and switch to it */
|
||||
assert(_pfGame.bytesPerPixel > 0);
|
||||
if (_pfGame != _oldPfGame) {
|
||||
assert(_transactionState == kTransactionActive);
|
||||
_gfxState.gfxModeID = chooseMode(&_pfGame);
|
||||
_transactionDetails.formatChanged = true;
|
||||
}
|
||||
|
||||
_gameTopTexture.create(width, height, _gfxState.gfxMode, true);
|
||||
|
||||
if (_pfGame == _gameTopTexture.format)
|
||||
_gameScreen.free();
|
||||
else
|
||||
_gameScreen.create(width, height, _pfGame);
|
||||
|
||||
_focusDirty = true;
|
||||
_focusRect = Common::Rect(_gameWidth, _gameHeight);
|
||||
|
||||
updateSize();
|
||||
}
|
||||
|
||||
void OSystem_3DS::updateSize() {
|
||||
// Initialize _overlay here so that it can be reinitialized when _screen is changed.
|
||||
|
||||
// Overlay sprite must have a width matching or exceeding that of the screen to
|
||||
// which it's set to render, otherwise portions of the screen will not render.
|
||||
// _screen == kScreenTop
|
||||
// >>> overlay renders to top screen
|
||||
// >>> top screen is 400 pixels wide
|
||||
// _screen == (kScreenBottom | kScreenBoth)
|
||||
// >>> overlay renders to bottom screen
|
||||
// >>> bottom screen is 320 pixels wide
|
||||
_overlay.create(_screen == kScreenTop ? 400 : 320, 240, &DEFAULT_MODE, true);
|
||||
|
||||
if (_stretchToFit) {
|
||||
_gameTopX = _gameTopY = _gameBottomX = _gameBottomY = 0;
|
||||
_gameTopTexture.setScale(400.f / _gameWidth, 240.f / _gameHeight);
|
||||
_gameBottomTexture.setScale(320.f / _gameWidth, 240.f / _gameHeight);
|
||||
} else {
|
||||
float ratio = static_cast<float>(_gameWidth) / _gameHeight;
|
||||
|
||||
if (ratio > 400.f / 240.f) {
|
||||
float r = 400.f / _gameWidth;
|
||||
_gameTopTexture.setScale(r, r);
|
||||
_gameTopX = 0;
|
||||
_gameTopY = (240.f / r - _gameHeight) / 2.f;
|
||||
} else {
|
||||
float r = 240.f / _gameHeight;
|
||||
_gameTopTexture.setScale(r, r);
|
||||
_gameTopY = 0;
|
||||
_gameTopX = (400.f / r - _gameWidth) / 2.f;
|
||||
}
|
||||
if (ratio > 320.f / 240.f) {
|
||||
float r = 320.f / _gameWidth;
|
||||
_gameBottomTexture.setScale(r, r);
|
||||
_gameBottomX = 0;
|
||||
_gameBottomY = (240.f / r - _gameHeight) / 2.f;
|
||||
} else {
|
||||
float r = 240.f / _gameHeight;
|
||||
_gameBottomTexture.setScale(r, r);
|
||||
_gameBottomY = 0;
|
||||
_gameBottomX = (320.f / r - _gameWidth) / 2.f;
|
||||
}
|
||||
}
|
||||
_gameTopTexture.setPosition(_gameTopX, _gameTopY);
|
||||
_gameBottomTexture.setPosition(_gameBottomX, _gameBottomY);
|
||||
_gameTopTexture.setOffset(0, 0);
|
||||
_gameBottomTexture.setOffset(0, 0);
|
||||
if (_overlayInGUI) {
|
||||
_cursorTexture.setScale(1.f, 1.f);
|
||||
} else if (_screen == kScreenTop) {
|
||||
_cursorTexture.setScale(_gameTopTexture.getScaleX(), _gameTopTexture.getScaleY());
|
||||
} else {
|
||||
_cursorTexture.setScale(_gameBottomTexture.getScaleX(), _gameBottomTexture.getScaleY());
|
||||
}
|
||||
}
|
||||
|
||||
Common::List<Graphics::PixelFormat> OSystem_3DS::getSupportedFormats() const {
|
||||
Common::List<Graphics::PixelFormat> list;
|
||||
// The following formats are supported natively by the GPU
|
||||
list.push_back(Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0)); // GPU_RGBA8
|
||||
list.push_back(Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); // GPU_RGB565
|
||||
list.push_back(Graphics::PixelFormat(2, 5, 5, 5, 1, 11, 6, 1, 0)); // GPU_RGBA5551
|
||||
list.push_back(Graphics::PixelFormat(2, 4, 4, 4, 4, 12, 8, 4, 0)); // GPU_RGBA4
|
||||
|
||||
// The following format requires software conversion
|
||||
list.push_back(Graphics::PixelFormat::createFormatCLUT8());
|
||||
return list;
|
||||
}
|
||||
|
||||
void OSystem_3DS::beginGFXTransaction() {
|
||||
assert(_transactionState == kTransactionNone);
|
||||
_transactionState = kTransactionActive;
|
||||
_transactionDetails.formatChanged = false;
|
||||
_oldGfxState = _gfxState;
|
||||
}
|
||||
|
||||
OSystem::TransactionError OSystem_3DS::endGFXTransaction() {
|
||||
int errors = OSystem::kTransactionSuccess;
|
||||
|
||||
assert(_transactionState != kTransactionNone);
|
||||
if (_transactionState == kTransactionRollback) {
|
||||
if (_gfxState.gfxModeID != _oldGfxState.gfxModeID) {
|
||||
errors |= OSystem::kTransactionModeSwitchFailed;
|
||||
_gfxState = _oldGfxState;
|
||||
} else if ((_gfxState.gfxMode != _oldGfxState.gfxMode) |
|
||||
(_gfxState.gfxMode != gfxModes[_gfxState.gfxModeID])) {
|
||||
errors |= OSystem::kTransactionFormatNotSupported;
|
||||
_gfxState = _oldGfxState;
|
||||
}
|
||||
|
||||
_oldGfxState.setup = false;
|
||||
}
|
||||
if (_transactionDetails.formatChanged) {
|
||||
if (!setGraphicsMode(_gfxState.gfxModeID)) {
|
||||
if (_oldGfxState.setup) {
|
||||
_transactionState = kTransactionRollback;
|
||||
errors |= endGFXTransaction();
|
||||
}
|
||||
} else if (_gfxState.gfxMode != gfxModes[_gfxState.gfxModeID]) {
|
||||
if (_oldGfxState.setup) {
|
||||
_transactionState = kTransactionRollback;
|
||||
errors |= endGFXTransaction();
|
||||
}
|
||||
} else {
|
||||
initSize(_gameWidth, _gameHeight, &_pfGame);
|
||||
clearOverlay();
|
||||
_gfxState.setup = true;
|
||||
_screenChangeId++;
|
||||
}
|
||||
}
|
||||
|
||||
_transactionState = kTransactionNone;
|
||||
return (OSystem::TransactionError)errors;
|
||||
}
|
||||
|
||||
float OSystem_3DS::getScaleRatio() const {
|
||||
if (_overlayInGUI) {
|
||||
return 1.0;
|
||||
} else if (_screen == kScreenTop) {
|
||||
return _gameTopTexture.getScaleX();
|
||||
} else {
|
||||
return _gameBottomTexture.getScaleX();
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::setPalette(const byte *colors, uint start, uint num) {
|
||||
assert(start + num <= 256);
|
||||
memcpy(_palette + 3 * start, colors, 3 * num);
|
||||
Graphics::convertPaletteToMap(_paletteMap + start, colors, num, _modeCLUT8.surfaceFormat);
|
||||
_gameTextureDirty = true;
|
||||
}
|
||||
|
||||
void OSystem_3DS::grabPalette(byte *colors, uint start, uint num) const {
|
||||
assert(start + num <= 256);
|
||||
memcpy(colors, _palette + 3 * start, 3 * num);
|
||||
}
|
||||
|
||||
// TODO: Move this into common code
|
||||
// TODO: Convert two pixels at once using 32-bit loads and stores
|
||||
static void copyRect555To5551(byte *dst, const byte *src, const uint dstPitch, const uint srcPitch,
|
||||
const uint w, const uint h) {
|
||||
// Faster, but larger, to provide optimized handling for each case.
|
||||
const uint srcDelta = (srcPitch - w * sizeof(uint16));
|
||||
const uint dstDelta = (dstPitch - w * sizeof(uint16));
|
||||
|
||||
for (uint y = 0; y < h; ++y) {
|
||||
for (uint x = 0; x < w; ++x) {
|
||||
uint16 col = *(const uint16 *)src;
|
||||
col = (col << 1) | 1;
|
||||
*(uint16 *)dst = col;
|
||||
|
||||
src += sizeof(uint16);
|
||||
dst += sizeof(uint16);
|
||||
}
|
||||
src += srcDelta;
|
||||
dst += dstDelta;
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::copyRectToScreen(const void *buf, int pitch, int x,
|
||||
int y, int w, int h) {
|
||||
if (_pfGame == _gameTopTexture.format) {
|
||||
_gameTopTexture.copyRectToSurface(buf, pitch, x, y, w, h);
|
||||
_gameTopTexture.markDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
_gameScreen.copyRectToSurface(buf, pitch, x, y, w, h);
|
||||
|
||||
if (_pfGame == Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)) {
|
||||
byte *dst = (byte *)_gameTopTexture.getBasePtr(x, y);
|
||||
copyRect555To5551(dst, (const byte *)buf, _gameTopTexture.pitch, pitch, w, h);
|
||||
} else if (_gfxState.gfxMode == &_modeCLUT8) {
|
||||
byte *dst = (byte *)_gameTopTexture.getBasePtr(x, y);
|
||||
Graphics::crossBlitMap(dst, (const byte *)buf, _gameTopTexture.pitch, pitch,
|
||||
w, h, _gameTopTexture.format.bytesPerPixel, _paletteMap);
|
||||
} else {
|
||||
byte *dst = (byte *)_gameTopTexture.getBasePtr(x, y);
|
||||
Graphics::crossBlit(dst, (const byte *)buf, _gameTopTexture.pitch, pitch,
|
||||
w, h, _gameTopTexture.format, _pfGame);
|
||||
}
|
||||
|
||||
_gameTopTexture.markDirty();
|
||||
}
|
||||
|
||||
void OSystem_3DS::flushGameScreen() {
|
||||
if (_pfGame == _gameTopTexture.format) {
|
||||
return;
|
||||
} else if (_pfGame == Graphics::PixelFormat(2, 5, 5, 5, 0, 10, 5, 0, 0)) {
|
||||
const byte *src = (const byte *)_gameScreen.getPixels();
|
||||
byte *dst = (byte *)_gameTopTexture.getPixels();
|
||||
copyRect555To5551(dst, src, _gameTopTexture.pitch, _gameScreen.pitch,
|
||||
_gameScreen.w, _gameScreen.h);
|
||||
} else if (_gfxState.gfxMode == &_modeCLUT8) {
|
||||
const byte *src = (const byte *)_gameScreen.getPixels();
|
||||
byte *dst = (byte *)_gameTopTexture.getPixels();
|
||||
Graphics::crossBlitMap(dst, src, _gameTopTexture.pitch, _gameScreen.pitch,
|
||||
_gameScreen.w, _gameScreen.h, _gameTopTexture.format.bytesPerPixel, _paletteMap);
|
||||
} else {
|
||||
const byte *src = (const byte *)_gameScreen.getPixels();
|
||||
byte *dst = (byte *)_gameTopTexture.getPixels();
|
||||
Graphics::crossBlit(dst, src, _gameTopTexture.pitch, _gameScreen.pitch,
|
||||
_gameScreen.w, _gameScreen.h, _gameTopTexture.format, _pfGame);
|
||||
}
|
||||
|
||||
_gameTopTexture.markDirty();
|
||||
}
|
||||
|
||||
Graphics::Surface *OSystem_3DS::lockScreen() {
|
||||
if (_pfGame == _gameTopTexture.format)
|
||||
return &_gameTopTexture;
|
||||
else
|
||||
return &_gameScreen;
|
||||
}
|
||||
void OSystem_3DS::unlockScreen() {
|
||||
_gameTextureDirty = true;
|
||||
}
|
||||
|
||||
void OSystem_3DS::updateScreen() {
|
||||
if (sleeping || exiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gameTextureDirty) {
|
||||
flushGameScreen();
|
||||
_gameTextureDirty = false;
|
||||
}
|
||||
|
||||
// updateFocus();
|
||||
updateMagnify();
|
||||
|
||||
if (_osdMessage.getPixels() && _osdMessageEndTime <= getMillis(true)) {
|
||||
_osdMessage.free();
|
||||
}
|
||||
|
||||
C3D_FrameBegin(0);
|
||||
_gameTopTexture.transfer();
|
||||
if (_overlayVisible) {
|
||||
_overlay.transfer();
|
||||
}
|
||||
if (_cursorVisible && _showCursor) {
|
||||
_cursorTexture.transfer();
|
||||
}
|
||||
_osdMessage.transfer();
|
||||
_activityIcon.transfer();
|
||||
C3D_FrameEnd(0);
|
||||
|
||||
C3D_FrameBegin(0);
|
||||
// Render top screen
|
||||
C3D_RenderTargetClear(_renderTargetTop, C3D_CLEAR_ALL, 0x00000000, 0);
|
||||
C3D_FrameDrawOn(_renderTargetTop);
|
||||
if (_screen == kScreenTop || _screen == kScreenBoth) {
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _projectionLocation, &_projectionTop);
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _gameTopTexture.getMatrix());
|
||||
_gameTopTexture.setFilteringMode(_magnifyMode != MODE_MAGON && _filteringEnabled);
|
||||
_gameTopTexture.render();
|
||||
if (_overlayVisible && _screen == kScreenTop) {
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _overlay.getMatrix());
|
||||
_overlay.render();
|
||||
}
|
||||
if (_activityIcon.getPixels() && _screen == kScreenTop) {
|
||||
_activityIcon.setPosition(400 - _activityIcon.actualWidth, 0);
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _activityIcon.getMatrix());
|
||||
_activityIcon.render();
|
||||
}
|
||||
if (_osdMessage.getPixels() && _screen == kScreenTop) {
|
||||
_osdMessage.setPosition((400 - _osdMessage.actualWidth) / 2, (240 - _osdMessage.actualHeight) / 2);
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _osdMessage.getMatrix());
|
||||
_osdMessage.render();
|
||||
}
|
||||
if (_cursorVisible && _showCursor && _screen == kScreenTop) {
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _cursorTexture.getMatrix());
|
||||
_cursorTexture.setFilteringMode(!_overlayInGUI && _filteringEnabled);
|
||||
_cursorTexture.render();
|
||||
}
|
||||
}
|
||||
|
||||
// Render bottom screen
|
||||
C3D_RenderTargetClear(_renderTargetBottom, C3D_CLEAR_ALL, 0x00000000, 0);
|
||||
C3D_FrameDrawOn(_renderTargetBottom);
|
||||
if (_screen == kScreenBottom || _screen == kScreenBoth) {
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _projectionLocation, &_projectionBottom);
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _gameBottomTexture.getMatrix());
|
||||
_gameTopTexture.setFilteringMode(_filteringEnabled);
|
||||
_gameTopTexture.render();
|
||||
if (_overlayVisible) {
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _overlay.getMatrix());
|
||||
_overlay.render();
|
||||
}
|
||||
if (_activityIcon.getPixels()) {
|
||||
_activityIcon.setPosition(320 - _activityIcon.actualWidth, 0);
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _activityIcon.getMatrix());
|
||||
_activityIcon.render();
|
||||
}
|
||||
if (_osdMessage.getPixels()) {
|
||||
_osdMessage.setPosition((320 - _osdMessage.actualWidth) / 2, (240 - _osdMessage.actualHeight) / 2);
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _osdMessage.getMatrix());
|
||||
_osdMessage.render();
|
||||
}
|
||||
if (_cursorVisible && _showCursor) {
|
||||
C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, _modelviewLocation, _cursorTexture.getMatrix());
|
||||
_cursorTexture.setFilteringMode(!_overlayInGUI && _filteringEnabled);
|
||||
_cursorTexture.render();
|
||||
}
|
||||
}
|
||||
C3D_FrameEnd(0);
|
||||
}
|
||||
|
||||
void OSystem_3DS::setShakePos(int shakeXOffset, int shakeYOffset) {
|
||||
// TODO: implement this in overlay, top screen, and mouse too
|
||||
_screenShakeXOffset = shakeXOffset;
|
||||
_screenShakeYOffset = shakeYOffset;
|
||||
int topX = _gameTopX + (_gameTopTexture.getScaleX() * shakeXOffset);
|
||||
int topY = _gameTopY + (_gameTopTexture.getScaleY() * shakeYOffset);
|
||||
_gameTopTexture.setPosition(topX, topY);
|
||||
int bottomX = _gameBottomX + (_gameBottomTexture.getScaleX() * shakeXOffset);
|
||||
int bottomY = _gameBottomY + (_gameBottomTexture.getScaleY() * shakeYOffset);
|
||||
_gameBottomTexture.setPosition(bottomX, bottomY);
|
||||
}
|
||||
|
||||
void OSystem_3DS::setFocusRectangle(const Common::Rect &rect) {
|
||||
debug("setfocus: %d %d %d %d", rect.left, rect.top, rect.width(), rect.height());
|
||||
_focusRect = rect;
|
||||
_focusDirty = true;
|
||||
_focusClearTime = 0;
|
||||
}
|
||||
|
||||
void OSystem_3DS::clearFocusRectangle() {
|
||||
_focusClearTime = getMillis();
|
||||
}
|
||||
|
||||
void OSystem_3DS::updateFocus() {
|
||||
|
||||
if (_focusClearTime && getMillis() - _focusClearTime > 5000) {
|
||||
_focusClearTime = 0;
|
||||
_focusDirty = true;
|
||||
_focusRect = Common::Rect(_gameWidth, _gameHeight);
|
||||
}
|
||||
|
||||
if (_focusDirty) {
|
||||
float duration = 1.f / 20.f; // Focus animation in frame duration
|
||||
float w = 400.f;
|
||||
float h = 240.f;
|
||||
float ratio = _focusRect.width() / _focusRect.height();
|
||||
if (ratio > w/h) {
|
||||
_focusTargetScaleX = w / _focusRect.width();
|
||||
float newHeight = (float)_focusRect.width() / w/h;
|
||||
_focusTargetScaleY = h / newHeight;
|
||||
_focusTargetPosX = _focusTargetScaleX * _focusRect.left;
|
||||
_focusTargetPosY = _focusTargetScaleY * ((float)_focusRect.top - (newHeight - _focusRect.height())/2.f);
|
||||
} else {
|
||||
_focusTargetScaleY = h / _focusRect.height();
|
||||
float newWidth = (float)_focusRect.height() * w/h;
|
||||
_focusTargetScaleX = w / newWidth;
|
||||
_focusTargetPosY = _focusTargetScaleY * _focusRect.top;
|
||||
_focusTargetPosX = _focusTargetScaleX * ((float)_focusRect.left - (newWidth - _focusRect.width())/2.f);
|
||||
}
|
||||
if (_focusTargetPosX < 0 && _focusTargetScaleY != 240.f / _gameHeight) {
|
||||
_focusTargetPosX = 0;
|
||||
}
|
||||
if (_focusTargetPosY < 0 && _focusTargetScaleX != 400.f / _gameWidth) {
|
||||
_focusTargetPosY = 0;
|
||||
}
|
||||
_focusStepPosX = duration * (_focusTargetPosX - _focusPosX);
|
||||
_focusStepPosY = duration * (_focusTargetPosY - _focusPosY);
|
||||
_focusStepScaleX = duration * (_focusTargetScaleX - _focusScaleX);
|
||||
_focusStepScaleY = duration * (_focusTargetScaleY - _focusScaleY);
|
||||
}
|
||||
|
||||
if (_focusDirty || _focusPosX != _focusTargetPosX || _focusPosY != _focusTargetPosY ||
|
||||
_focusScaleX != _focusTargetScaleX || _focusScaleY != _focusTargetScaleY) {
|
||||
_focusDirty = false;
|
||||
|
||||
if ((_focusStepPosX > 0 && _focusPosX > _focusTargetPosX) || (_focusStepPosX < 0 && _focusPosX < _focusTargetPosX)) {
|
||||
_focusPosX = _focusTargetPosX;
|
||||
} else if (_focusPosX != _focusTargetPosX) {
|
||||
_focusPosX += _focusStepPosX;
|
||||
}
|
||||
|
||||
if ((_focusStepPosY > 0 && _focusPosY > _focusTargetPosY) || (_focusStepPosY < 0 && _focusPosY < _focusTargetPosY)) {
|
||||
_focusPosY = _focusTargetPosY;
|
||||
} else if (_focusPosY != _focusTargetPosY) {
|
||||
_focusPosY += _focusStepPosY;
|
||||
}
|
||||
|
||||
if ((_focusStepScaleX > 0 && _focusScaleX > _focusTargetScaleX) || (_focusStepScaleX < 0 && _focusScaleX < _focusTargetScaleX)) {
|
||||
_focusScaleX = _focusTargetScaleX;
|
||||
} else if (_focusScaleX != _focusTargetScaleX) {
|
||||
_focusScaleX += _focusStepScaleX;
|
||||
}
|
||||
|
||||
if ((_focusStepScaleY > 0 && _focusScaleY > _focusTargetScaleY) || (_focusStepScaleY < 0 && _focusScaleY < _focusTargetScaleY)) {
|
||||
_focusScaleY = _focusTargetScaleY;
|
||||
} else if (_focusScaleY != _focusTargetScaleY) {
|
||||
_focusScaleY += _focusStepScaleY;
|
||||
}
|
||||
|
||||
Mtx_Identity(&_focusMatrix);
|
||||
Mtx_Translate(&_focusMatrix, -_focusPosX, -_focusPosY, 0, true);
|
||||
Mtx_Scale(&_focusMatrix, _focusScaleX, _focusScaleY, 1.f);
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::updateMagnify() {
|
||||
if (_magnifyMode == MODE_MAGON && _screen != kScreenBoth) {
|
||||
// Only allow to magnify when both screens are enabled
|
||||
_magnifyMode = MODE_MAGOFF;
|
||||
}
|
||||
|
||||
if (_magnifyMode == MODE_MAGON) {
|
||||
if (!_overlayVisible) {
|
||||
_magX = (_cursorScreenX < _magCenterX) ?
|
||||
0 : ((_cursorScreenX < (_gameWidth - _magCenterX)) ?
|
||||
_cursorScreenX - _magCenterX : _gameWidth - _magWidth);
|
||||
_magY = (_cursorScreenY < _magCenterY) ?
|
||||
0 : ((_cursorScreenY < _gameHeight - _magCenterY) ?
|
||||
_cursorScreenY - _magCenterY : _gameHeight - _magHeight);
|
||||
}
|
||||
_gameTopTexture.setScale(1.f,1.f);
|
||||
_gameTopTexture.setPosition(0,0);
|
||||
_gameTopTexture.setOffset(_magX, _magY);
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::showOverlay(bool inGUI) {
|
||||
_overlayInGUI = inGUI;
|
||||
_overlayVisible = true;
|
||||
updateSize();
|
||||
}
|
||||
|
||||
void OSystem_3DS::hideOverlay() {
|
||||
_overlayVisible = false;
|
||||
_overlayInGUI = false;
|
||||
updateSize();
|
||||
}
|
||||
|
||||
Graphics::PixelFormat OSystem_3DS::getOverlayFormat() const {
|
||||
return _overlay.format;
|
||||
}
|
||||
|
||||
void OSystem_3DS::clearOverlay() {
|
||||
_overlay.clear();
|
||||
}
|
||||
|
||||
void OSystem_3DS::grabOverlay(Graphics::Surface &surface) {
|
||||
assert(surface.w >= getOverlayWidth());
|
||||
assert(surface.h >= getOverlayHeight());
|
||||
assert(surface.format.bytesPerPixel == _overlay.format.bytesPerPixel);
|
||||
|
||||
byte *src = (byte *)_overlay.getPixels();
|
||||
byte *dst = (byte *)surface.getPixels();
|
||||
Graphics::copyBlit(dst, src, surface.pitch, _overlay.pitch,
|
||||
getOverlayWidth(), getOverlayHeight(), _overlay.format.bytesPerPixel);
|
||||
}
|
||||
|
||||
void OSystem_3DS::copyRectToOverlay(const void *buf, int pitch, int x,
|
||||
int y, int w, int h) {
|
||||
_overlay.copyRectToSurface(buf, pitch, x, y, w, h);
|
||||
_overlay.markDirty();
|
||||
}
|
||||
|
||||
void OSystem_3DS::displayMessageOnOSD(const Common::U32String &msg) {
|
||||
// The font we are going to use:
|
||||
const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont);
|
||||
if (!font) {
|
||||
warning("No available font to render OSD messages");
|
||||
return;
|
||||
}
|
||||
|
||||
// Split the message into separate lines.
|
||||
Common::Array<Common::U32String> lines;
|
||||
Common::U32String::const_iterator strLineItrBegin = msg.begin();
|
||||
|
||||
for (Common::U32String::const_iterator itr = msg.begin(); itr != msg.end(); itr++) {
|
||||
if (*itr == '\n') {
|
||||
lines.push_back(Common::U32String(strLineItrBegin, itr));
|
||||
strLineItrBegin = itr + 1;
|
||||
}
|
||||
}
|
||||
if (strLineItrBegin != msg.end())
|
||||
lines.push_back(Common::U32String(strLineItrBegin, msg.end()));
|
||||
|
||||
// Determine a rect which would contain the message string (clipped to the
|
||||
// screen dimensions).
|
||||
const int vOffset = 6;
|
||||
const int lineSpacing = 1;
|
||||
const int lineHeight = font->getFontHeight() + 2 * lineSpacing;
|
||||
int width = 0;
|
||||
int height = lineHeight * lines.size() + 2 * vOffset;
|
||||
uint i;
|
||||
for (i = 0; i < lines.size(); i++) {
|
||||
width = MAX(width, font->getStringWidth(lines[i]) + 14);
|
||||
}
|
||||
|
||||
// Clip the rect
|
||||
if (width > getOverlayWidth()) {
|
||||
width = getOverlayWidth();
|
||||
}
|
||||
if (height > getOverlayHeight()) {
|
||||
height = getOverlayHeight();
|
||||
}
|
||||
|
||||
_osdMessage.create(width, height, &DEFAULT_MODE);
|
||||
_osdMessage.fillRect(Common::Rect(width, height), _pfDefaultTexture.ARGBToColor(200, 0, 0, 0));
|
||||
|
||||
// Render the message, centered, and in white
|
||||
for (i = 0; i < lines.size(); i++) {
|
||||
font->drawString(&_osdMessage, lines[i],
|
||||
0, 0 + i * lineHeight + vOffset + lineSpacing, width,
|
||||
_pfDefaultTexture.RGBToColor(255, 255, 255),
|
||||
Graphics::kTextAlignCenter, 0, true);
|
||||
}
|
||||
|
||||
_osdMessageEndTime = getMillis(true) + kOSDMessageDuration;
|
||||
}
|
||||
|
||||
void OSystem_3DS::displayActivityIconOnOSD(const Graphics::Surface *icon) {
|
||||
if (!icon) {
|
||||
_activityIcon.free();
|
||||
} else {
|
||||
if (!_activityIcon.getPixels() || icon->w != _activityIcon.w || icon->h != _activityIcon.h) {
|
||||
_activityIcon.create(icon->w, icon->h, &DEFAULT_MODE);
|
||||
}
|
||||
|
||||
if (icon->format == _activityIcon.format) {
|
||||
_activityIcon.copyRectToSurface(*icon, 0, 0, Common::Rect(icon->w, icon->h));
|
||||
} else {
|
||||
Graphics::Surface *converted = icon->convertTo(_activityIcon.format);
|
||||
_activityIcon.copyRectToSurface(*converted, 0, 0, Common::Rect(converted->w, converted->h));
|
||||
converted->free();
|
||||
delete converted;
|
||||
}
|
||||
|
||||
_activityIcon.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
int16 OSystem_3DS::getOverlayHeight() const {
|
||||
return 240;
|
||||
}
|
||||
|
||||
int16 OSystem_3DS::getOverlayWidth() const {
|
||||
return _screen == kScreenTop ? 400 : 320;
|
||||
}
|
||||
|
||||
bool OSystem_3DS::showMouse(bool visible) {
|
||||
_cursorVisible = visible;
|
||||
flushCursor();
|
||||
return !visible;
|
||||
}
|
||||
|
||||
void OSystem_3DS::warpMouse(int x, int y) {
|
||||
if (!_overlayVisible) {
|
||||
_cursorScreenX = x;
|
||||
_cursorScreenY = y;
|
||||
} else {
|
||||
_cursorOverlayX = x;
|
||||
_cursorOverlayY = y;
|
||||
}
|
||||
|
||||
// TODO: adjust for _cursorScalable ?
|
||||
x -= _cursorHotspotX;
|
||||
y -= _cursorHotspotY;
|
||||
|
||||
int offsetx = 0;
|
||||
int offsety = 0;
|
||||
if (!_overlayVisible) {
|
||||
offsetx = _screen == kScreenTop ? _gameTopTexture.getPosX() : _gameBottomTexture.getPosX();
|
||||
offsety = _screen == kScreenTop ? _gameTopTexture.getPosY() : _gameBottomTexture.getPosY();
|
||||
}
|
||||
|
||||
_cursorTexture.setPosition(x + offsetx, y + offsety);
|
||||
}
|
||||
|
||||
void OSystem_3DS::setCursorDelta(float deltaX, float deltaY) {
|
||||
_cursorDeltaX = deltaX;
|
||||
_cursorDeltaY = deltaY;
|
||||
}
|
||||
|
||||
void OSystem_3DS::setMouseCursor(const void *buf, uint w, uint h,
|
||||
int hotspotX, int hotspotY,
|
||||
uint32 keycolor, bool dontScale,
|
||||
const Graphics::PixelFormat *format, const byte *mask) {
|
||||
_cursorScalable = !dontScale;
|
||||
_cursorHotspotX = hotspotX;
|
||||
_cursorHotspotY = hotspotY;
|
||||
_cursorKeyColor = keycolor;
|
||||
_pfCursor = !format ? Graphics::PixelFormat::createFormatCLUT8() : *format;
|
||||
|
||||
if (mask)
|
||||
warning("OSystem_3DS::setMouseCursor: Masks are not supported");
|
||||
|
||||
if (w != (uint)_cursor.w || h != (uint)_cursor.h || _cursor.format != _pfCursor) {
|
||||
_cursor.create(w, h, _pfCursor);
|
||||
_cursorTexture.create(w, h, &DEFAULT_MODE);
|
||||
}
|
||||
|
||||
if ( w != 0 && h != 0 ) {
|
||||
_cursor.copyRectToSurface(buf, w * _pfCursor.bytesPerPixel, 0, 0, w, h);
|
||||
}
|
||||
|
||||
flushCursor();
|
||||
|
||||
if (!_overlayVisible) {
|
||||
warpMouse(_cursorScreenX, _cursorScreenY);
|
||||
} else {
|
||||
warpMouse(_cursorOverlayX, _cursorOverlayY);
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::setCursorPalette(const byte *colors, uint start, uint num) {
|
||||
assert(start + num <= 256);
|
||||
memcpy(_cursorPalette + 3 * start, colors, 3 * num);
|
||||
_cursorPaletteEnabled = true;
|
||||
flushCursor();
|
||||
}
|
||||
|
||||
namespace {
|
||||
template<typename SrcColor>
|
||||
void applyKeyColor(Graphics::Surface *src, Graphics::Surface *dst, const SrcColor keyColor) {
|
||||
assert(dst->format.bytesPerPixel == 4);
|
||||
assert((dst->w >= src->w) && (dst->h >= src->h));
|
||||
|
||||
for (uint y = 0; y < (uint)src->h; ++y) {
|
||||
SrcColor *srcPtr = (SrcColor *)src->getBasePtr(0, y);
|
||||
uint32 *dstPtr = (uint32 *)dst->getBasePtr(0, y);
|
||||
|
||||
for (uint x = 0; x < (uint)src->w; ++x) {
|
||||
const SrcColor color = *srcPtr++;
|
||||
|
||||
if (color == keyColor) {
|
||||
*dstPtr = 0;
|
||||
}
|
||||
|
||||
dstPtr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // End of anonymous namespace
|
||||
|
||||
void OSystem_3DS::flushCursor() {
|
||||
if (_cursor.getPixels()) {
|
||||
Graphics::Surface *converted = _cursor.convertTo(_cursorTexture.format, _cursorPaletteEnabled ? _cursorPalette : _palette);
|
||||
_cursorTexture.copyRectToSurface(*converted, 0, 0, Common::Rect(converted->w, converted->h));
|
||||
_cursorTexture.markDirty();
|
||||
converted->free();
|
||||
delete converted;
|
||||
|
||||
if (_pfCursor.bytesPerPixel == 1) {
|
||||
applyKeyColor<byte>(&_cursor, &_cursorTexture, _cursorKeyColor);
|
||||
} else if (_pfCursor.bytesPerPixel == 2) {
|
||||
applyKeyColor<uint16>(&_cursor, &_cursorTexture, _cursorKeyColor);
|
||||
} else if (_pfCursor.bytesPerPixel == 4) {
|
||||
applyKeyColor<uint32>(&_cursor, &_cursorTexture, _cursorKeyColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace N3DS
|
||||
275
backends/platform/3ds/osystem.cpp
Normal file
275
backends/platform/3ds/osystem.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_printf
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_unistd_h
|
||||
|
||||
#include <3ds.h>
|
||||
#include "osystem.h"
|
||||
|
||||
#include "backends/mutex/3ds/3ds-mutex.h"
|
||||
#include "backends/saves/default/default-saves.h"
|
||||
#include "backends/timer/default/default-timer.h"
|
||||
#include "backends/events/default/default-events.h"
|
||||
#include "audio/mixer_intern.h"
|
||||
#include "common/scummsys.h"
|
||||
#include "common/config-manager.h"
|
||||
#include "common/str.h"
|
||||
|
||||
#include "backends/fs/posix-drives/posix-drives-fs-factory.h"
|
||||
#include "backends/fs/posix-drives/posix-drives-fs.h"
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
namespace N3DS {
|
||||
|
||||
OSystem_3DS::OSystem_3DS():
|
||||
_focusDirty(true),
|
||||
_focusRect(Common::Rect(1, 1)),
|
||||
_focusPosX(0),
|
||||
_focusPosY(0),
|
||||
_focusTargetPosX(0),
|
||||
_focusTargetPosY(0),
|
||||
_focusStepPosX(0),
|
||||
_focusStepPosY(0),
|
||||
_focusScaleX(1.f),
|
||||
_focusScaleY(1.f),
|
||||
_focusTargetScaleX(1.f),
|
||||
_focusTargetScaleY(1.f),
|
||||
_focusStepScaleX(0.f),
|
||||
_focusStepScaleY(0.f),
|
||||
_focusClearTime(0),
|
||||
_cursorPaletteEnabled(false),
|
||||
_cursorVisible(false),
|
||||
_cursorScalable(false),
|
||||
_cursorScreenX(0),
|
||||
_cursorScreenY(0),
|
||||
_cursorOverlayX(0),
|
||||
_cursorOverlayY(0),
|
||||
_cursorHotspotX(0),
|
||||
_cursorHotspotY(0),
|
||||
_gameTopX(0),
|
||||
_gameTopY(0),
|
||||
_gameBottomX(0),
|
||||
_gameBottomY(0),
|
||||
_gameWidth(320),
|
||||
_gameHeight(240),
|
||||
_magX(0),
|
||||
_magY(0),
|
||||
_magWidth(400),
|
||||
_magHeight(240),
|
||||
_gameTextureDirty(false),
|
||||
_filteringEnabled(true),
|
||||
_overlayVisible(false),
|
||||
_overlayInGUI(false),
|
||||
_screenChangeId(0),
|
||||
_magnifyMode(MODE_MAGOFF),
|
||||
exiting(false),
|
||||
sleeping(false),
|
||||
_logger(0),
|
||||
_showCursor(true),
|
||||
_snapToBorder(true),
|
||||
_stretchToFit(false),
|
||||
_screen(kScreenBoth)
|
||||
{
|
||||
chdir("sdmc:/");
|
||||
|
||||
DrivesPOSIXFilesystemFactory *fsFactory = new DrivesPOSIXFilesystemFactory();
|
||||
fsFactory->addDrive("sdmc:");
|
||||
fsFactory->addDrive("romfs:");
|
||||
|
||||
//
|
||||
// Disable newlib's buffered IO, and use ScummVM's own buffering stream wrappers.
|
||||
//
|
||||
// The newlib version in use in devkitPro has performance issues
|
||||
// when seeking with a relative offset. See:
|
||||
// https://sourceware.org/git/gitweb.cgi?p=newlib-cygwin.git;a=commit;h=59362c80e3a02c011fd0ef3d7f07a20098d2a9d5
|
||||
//
|
||||
// devKitPro has a patch to newlib that can cause data corruption when
|
||||
// seeking back in files and then reading. See:
|
||||
// https://github.com/devkitPro/newlib/issues/16
|
||||
//
|
||||
fsFactory->configureBuffering(DrivePOSIXFilesystemNode::kBufferingModeScummVM, 2048);
|
||||
|
||||
_fsFactory = fsFactory;
|
||||
|
||||
Posix::assureDirectoryExists("/3ds/scummvm/saves/");
|
||||
}
|
||||
|
||||
OSystem_3DS::~OSystem_3DS() {
|
||||
exiting = true;
|
||||
destroyEvents();
|
||||
destroyAudio();
|
||||
destroy3DSGraphics();
|
||||
|
||||
delete _logger;
|
||||
_logger = 0;
|
||||
|
||||
delete _timerManager;
|
||||
_timerManager = 0;
|
||||
}
|
||||
|
||||
void OSystem_3DS::quit() {
|
||||
printf("OSystem_3DS::quit()\n");
|
||||
}
|
||||
|
||||
void OSystem_3DS::initBackend() {
|
||||
if (!_logger)
|
||||
_logger = new Backends::Log::Log(this);
|
||||
|
||||
if (_logger) {
|
||||
Common::WriteStream *logFile = createLogFile();
|
||||
if (logFile)
|
||||
_logger->open(logFile);
|
||||
}
|
||||
|
||||
updateBacklight();
|
||||
updateConfig();
|
||||
ConfMan.registerDefault("fullscreen", true);
|
||||
ConfMan.registerDefault("aspect_ratio", true);
|
||||
ConfMan.registerDefault("filtering", true);
|
||||
if (!ConfMan.hasKey("vkeybd_pack_name")) {
|
||||
ConfMan.set("vkeybd_pack_name", "vkeybd_small");
|
||||
}
|
||||
|
||||
_timerManager = new DefaultTimerManager();
|
||||
_savefileManager = new DefaultSaveFileManager("sdmc:/3ds/scummvm/saves/");
|
||||
|
||||
init3DSGraphics();
|
||||
initAudio();
|
||||
EventsBaseBackend::initBackend();
|
||||
initEvents();
|
||||
}
|
||||
|
||||
void OSystem_3DS::updateBacklight() {
|
||||
// Turn off the backlight of any screen not used
|
||||
if (R_SUCCEEDED(gspLcdInit())) {
|
||||
if (_screen == kScreenTop) {
|
||||
GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_TOP);
|
||||
GSPLCD_PowerOffBacklight(GSPLCD_SCREEN_BOTTOM);
|
||||
} else if (_screen == kScreenBottom) {
|
||||
GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTTOM);
|
||||
GSPLCD_PowerOffBacklight(GSPLCD_SCREEN_TOP);
|
||||
} else {
|
||||
GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH);
|
||||
}
|
||||
gspLcdExit();
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::updateConfig() {
|
||||
if (_gameScreen.getPixels()) {
|
||||
updateSize();
|
||||
(!_overlayVisible) ? warpMouse(_cursorScreenX, _cursorScreenY) :
|
||||
warpMouse(_cursorOverlayX, _cursorOverlayY);
|
||||
}
|
||||
}
|
||||
|
||||
Common::Path OSystem_3DS::getDefaultConfigFileName() {
|
||||
return "sdmc:/3ds/scummvm/scummvm.ini";
|
||||
}
|
||||
|
||||
Common::Path OSystem_3DS::getDefaultLogFileName() {
|
||||
return "sdmc:/3ds/scummvm/scummvm.log";
|
||||
}
|
||||
|
||||
void OSystem_3DS::addSysArchivesToSearchSet(Common::SearchSet &s, int priority) {
|
||||
s.add("RomFS", new Common::FSDirectory(DATA_PATH"/"), priority);
|
||||
}
|
||||
|
||||
uint32 OSystem_3DS::getMillis(bool skipRecord) {
|
||||
return svcGetSystemTick() / TICKS_PER_MSEC;
|
||||
}
|
||||
|
||||
void OSystem_3DS::delayMillis(uint msecs) {
|
||||
svcSleepThread(msecs * 1000000);
|
||||
}
|
||||
|
||||
void OSystem_3DS::getTimeAndDate(TimeDate& td, bool skipRecord) const {
|
||||
time_t curTime = time(0);
|
||||
struct tm t = *localtime(&curTime);
|
||||
td.tm_sec = t.tm_sec;
|
||||
td.tm_min = t.tm_min;
|
||||
td.tm_hour = t.tm_hour;
|
||||
td.tm_mday = t.tm_mday;
|
||||
td.tm_mon = t.tm_mon;
|
||||
td.tm_year = t.tm_year;
|
||||
td.tm_wday = t.tm_wday;
|
||||
}
|
||||
|
||||
Common::MutexInternal *OSystem_3DS::createMutex() {
|
||||
return create3DSMutexInternal();
|
||||
}
|
||||
|
||||
Common::String OSystem_3DS::getSystemLanguage() const {
|
||||
u8 langcode;
|
||||
CFGU_GetSystemLanguage(&langcode);
|
||||
switch (langcode) {
|
||||
case CFG_LANGUAGE_JP: return "ja_JP";
|
||||
case CFG_LANGUAGE_EN: return "en_US";
|
||||
case CFG_LANGUAGE_FR: return "fr_FR";
|
||||
case CFG_LANGUAGE_DE: return "de_DE";
|
||||
case CFG_LANGUAGE_IT: return "it_IT";
|
||||
case CFG_LANGUAGE_ES: return "es_ES";
|
||||
case CFG_LANGUAGE_ZH: return "zh_CN";
|
||||
case CFG_LANGUAGE_KO: return "ko_KR";
|
||||
case CFG_LANGUAGE_NL: return "nl_NL";
|
||||
case CFG_LANGUAGE_PT: return "pt_BR";
|
||||
case CFG_LANGUAGE_RU: return "ru_RU";
|
||||
case CFG_LANGUAGE_TW: return "zh_HK";
|
||||
default: return "en_US";
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_3DS::fatalError() {
|
||||
printf("FatalError!\n");
|
||||
}
|
||||
|
||||
void OSystem_3DS::logMessage(LogMessageType::Type type, const char *message) {
|
||||
printf("%s", message);
|
||||
|
||||
// Then log into file (via the logger)
|
||||
if (_logger)
|
||||
_logger->print(message);
|
||||
}
|
||||
|
||||
Common::WriteStream *OSystem_3DS::createLogFile() {
|
||||
// Start out by resetting _logFilePath, so that in case
|
||||
// of a failure, we know that no log file is open.
|
||||
_logFilePath.clear();
|
||||
|
||||
Common::Path logFile;
|
||||
if (ConfMan.hasKey("logfile"))
|
||||
logFile = ConfMan.getPath("logfile");
|
||||
else
|
||||
logFile = getDefaultLogFileName();
|
||||
if (logFile.empty())
|
||||
return nullptr;
|
||||
|
||||
Common::FSNode file(logFile);
|
||||
Common::WriteStream *stream = file.createWriteStream(false);
|
||||
if (stream)
|
||||
_logFilePath = logFile;
|
||||
return stream;
|
||||
}
|
||||
|
||||
} // namespace N3DS
|
||||
316
backends/platform/3ds/osystem.h
Normal file
316
backends/platform/3ds/osystem.h
Normal file
@@ -0,0 +1,316 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_3DS_H
|
||||
#define PLATFORM_3DS_H
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
|
||||
#include "backends/base-backend.h"
|
||||
#include "graphics/paletteman.h"
|
||||
#include "base/main.h"
|
||||
#include "audio/mixer_intern.h"
|
||||
#include "backends/graphics/graphics.h"
|
||||
#include "backends/log/log.h"
|
||||
#include "backends/platform/3ds/sprite.h"
|
||||
#include "common/rect.h"
|
||||
#include "common/queue.h"
|
||||
#include "common/ustr.h"
|
||||
#include "engines/engine.h"
|
||||
|
||||
#define TICKS_PER_MSEC 268123
|
||||
|
||||
namespace N3DS {
|
||||
|
||||
enum MagnifyMode {
|
||||
MODE_MAGON,
|
||||
MODE_MAGOFF,
|
||||
};
|
||||
|
||||
enum InputMode {
|
||||
MODE_HOVER,
|
||||
MODE_DRAG,
|
||||
};
|
||||
|
||||
enum GraphicsModeID {
|
||||
RGBA8,
|
||||
RGB565,
|
||||
RGB5A1,
|
||||
RGBA4,
|
||||
CLUT8
|
||||
};
|
||||
|
||||
enum Screen {
|
||||
kScreenTop = 0x10000002,
|
||||
kScreenBottom,
|
||||
kScreenBoth,
|
||||
};
|
||||
|
||||
enum TransactionState {
|
||||
kTransactionNone = 0,
|
||||
kTransactionActive = 1,
|
||||
kTransactionRollback = 2
|
||||
};
|
||||
|
||||
|
||||
struct TransactionDetails {
|
||||
bool formatChanged, modeChanged;
|
||||
|
||||
TransactionDetails() {
|
||||
formatChanged = false;
|
||||
modeChanged = false;
|
||||
}
|
||||
};
|
||||
|
||||
typedef struct GfxMode3DS {
|
||||
Graphics::PixelFormat surfaceFormat;
|
||||
GPU_TEXCOLOR textureFormat;
|
||||
uint32 textureTransferFlags;
|
||||
} GfxMode3DS;
|
||||
|
||||
struct GfxState {
|
||||
bool setup;
|
||||
GraphicsModeID gfxModeID;
|
||||
const GfxMode3DS *gfxMode;
|
||||
|
||||
GfxState() {
|
||||
setup = false;
|
||||
gfxModeID = CLUT8;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class OSystem_3DS : public EventsBaseBackend, public PaletteManager, public Common::EventObserver {
|
||||
public:
|
||||
OSystem_3DS();
|
||||
virtual ~OSystem_3DS();
|
||||
|
||||
volatile bool exiting;
|
||||
volatile bool sleeping;
|
||||
|
||||
virtual void initBackend();
|
||||
|
||||
virtual bool hasFeature(OSystem::Feature f);
|
||||
virtual void setFeatureState(OSystem::Feature f, bool enable);
|
||||
virtual bool getFeatureState(OSystem::Feature f);
|
||||
|
||||
bool pollEvent(Common::Event &event) override;
|
||||
bool notifyEvent(const Common::Event &event) override;
|
||||
Common::HardwareInputSet *getHardwareInputSet() override;
|
||||
Common::KeymapArray getGlobalKeymaps() override;
|
||||
Common::KeymapperDefaultBindings *getKeymapperDefaultBindings() override;
|
||||
|
||||
void registerDefaultSettings(const Common::String &target) const override;
|
||||
GUI::OptionsContainerWidget *buildBackendOptionsWidget(GUI::GuiObject *boss, const Common::String &name, const Common::String &target) const override;
|
||||
void applyBackendSettings() override;
|
||||
|
||||
virtual uint32 getMillis(bool skipRecord = false);
|
||||
virtual void delayMillis(uint msecs);
|
||||
virtual void getTimeAndDate(TimeDate &td, bool skipRecord = false) const;
|
||||
|
||||
virtual Common::MutexInternal *createMutex();
|
||||
|
||||
virtual void logMessage(LogMessageType::Type type, const char *message);
|
||||
|
||||
virtual Audio::Mixer *getMixer();
|
||||
virtual PaletteManager *getPaletteManager() { return this; }
|
||||
virtual Common::String getSystemLanguage() const;
|
||||
virtual void fatalError();
|
||||
virtual void quit();
|
||||
|
||||
virtual Common::Path getDefaultConfigFileName();
|
||||
void addSysArchivesToSearchSet(Common::SearchSet &s, int priority) override;
|
||||
|
||||
// Graphics
|
||||
inline Graphics::PixelFormat getScreenFormat() const { return _pfGame; }
|
||||
virtual Common::List<Graphics::PixelFormat> getSupportedFormats() const;
|
||||
void initSize(uint width, uint height,
|
||||
const Graphics::PixelFormat *format = NULL);
|
||||
virtual int getScreenChangeID() const { return _screenChangeId; };
|
||||
GraphicsModeID chooseMode(Graphics::PixelFormat *format);
|
||||
bool setGraphicsMode(GraphicsModeID modeID);
|
||||
|
||||
void beginGFXTransaction();
|
||||
OSystem::TransactionError endGFXTransaction();
|
||||
int16 getHeight(){ return _gameHeight; }
|
||||
int16 getWidth(){ return _gameWidth; }
|
||||
float getScaleRatio() const;
|
||||
void setPalette(const byte *colors, uint start, uint num);
|
||||
void grabPalette(byte *colors, uint start, uint num) const;
|
||||
void copyRectToScreen(const void *buf, int pitch, int x, int y, int w,
|
||||
int h);
|
||||
Graphics::Surface *lockScreen();
|
||||
void unlockScreen();
|
||||
void updateScreen();
|
||||
void setShakePos(int shakeXOffset, int shakeYOffset);
|
||||
void setFocusRectangle(const Common::Rect &rect);
|
||||
void clearFocusRectangle();
|
||||
void showOverlay(bool inGUI);
|
||||
void hideOverlay();
|
||||
bool isOverlayVisible() const { return _overlayVisible; }
|
||||
Graphics::PixelFormat getOverlayFormat() const;
|
||||
void clearOverlay();
|
||||
void grabOverlay(Graphics::Surface &surface);
|
||||
void copyRectToOverlay(const void *buf, int pitch, int x, int y, int w,
|
||||
int h);
|
||||
virtual int16 getOverlayHeight() const;
|
||||
virtual int16 getOverlayWidth() const;
|
||||
void displayMessageOnOSD(const Common::U32String &msg) override;
|
||||
void displayActivityIconOnOSD(const Graphics::Surface *icon) override;
|
||||
|
||||
bool showMouse(bool visible);
|
||||
void warpMouse(int x, int y);
|
||||
void setMouseCursor(const void *buf, uint w, uint h, int hotspotX,
|
||||
int hotspotY, uint32 keycolor, bool dontScale = false,
|
||||
const Graphics::PixelFormat *format = NULL, const byte *mask = NULL);
|
||||
void setCursorPalette(const byte *colors, uint start, uint num);
|
||||
|
||||
// Transform point from touchscreen coords into gamescreen coords
|
||||
void transformPoint(touchPosition &point);
|
||||
// Clip point to gamescreen coords
|
||||
void clipPoint(touchPosition &point);
|
||||
|
||||
void setCursorDelta(float deltaX, float deltaY);
|
||||
|
||||
void updateFocus();
|
||||
void updateMagnify();
|
||||
void updateBacklight();
|
||||
void updateConfig();
|
||||
void updateSize();
|
||||
|
||||
private:
|
||||
void init3DSGraphics();
|
||||
void destroy3DSGraphics();
|
||||
void initAudio();
|
||||
void destroyAudio();
|
||||
void initEvents();
|
||||
void destroyEvents();
|
||||
|
||||
void flushGameScreen();
|
||||
void flushCursor();
|
||||
|
||||
virtual Common::Path getDefaultLogFileName();
|
||||
virtual Common::WriteStream *createLogFile();
|
||||
|
||||
protected:
|
||||
Audio::MixerImpl *_mixer;
|
||||
Backends::Log::Log *_logger;
|
||||
|
||||
private:
|
||||
u16 _gameWidth, _gameHeight;
|
||||
u16 _gameTopX, _gameTopY;
|
||||
u16 _gameBottomX, _gameBottomY;
|
||||
|
||||
// Audio
|
||||
Thread audioThread;
|
||||
|
||||
// Graphics
|
||||
GraphicsModeID _graphicsModeID;
|
||||
TransactionState _transactionState;
|
||||
TransactionDetails _transactionDetails;
|
||||
|
||||
GfxState _gfxState, _oldGfxState;
|
||||
Graphics::PixelFormat _pfDefaultTexture;
|
||||
Graphics::PixelFormat _pfGame, _oldPfGame;
|
||||
Graphics::PixelFormat _pfCursor;
|
||||
byte _palette[3 * 256];
|
||||
byte _cursorPalette[3 * 256];
|
||||
uint32 _paletteMap[256];
|
||||
|
||||
Graphics::Surface _gameScreen;
|
||||
bool _gameTextureDirty;
|
||||
Sprite _gameTopTexture;
|
||||
Sprite _gameBottomTexture;
|
||||
Sprite _overlay;
|
||||
Sprite _activityIcon;
|
||||
Sprite _osdMessage;
|
||||
bool _filteringEnabled;
|
||||
|
||||
enum {
|
||||
kOSDMessageDuration = 800
|
||||
};
|
||||
uint32 _osdMessageEndTime;
|
||||
|
||||
int _screenShakeXOffset;
|
||||
int _screenShakeYOffset;
|
||||
bool _overlayVisible;
|
||||
bool _overlayInGUI;
|
||||
int _screenChangeId;
|
||||
|
||||
DVLB_s *_dvlb;
|
||||
shaderProgram_s _program;
|
||||
int _projectionLocation;
|
||||
int _modelviewLocation;
|
||||
C3D_Mtx _projectionTop;
|
||||
C3D_Mtx _projectionBottom;
|
||||
C3D_RenderTarget* _renderTargetTop;
|
||||
C3D_RenderTarget* _renderTargetBottom;
|
||||
|
||||
// Focus
|
||||
Common::Rect _focusRect;
|
||||
bool _focusDirty;
|
||||
C3D_Mtx _focusMatrix;
|
||||
int _focusPosX, _focusPosY;
|
||||
int _focusTargetPosX, _focusTargetPosY;
|
||||
float _focusStepPosX, _focusStepPosY;
|
||||
float _focusScaleX, _focusScaleY;
|
||||
float _focusTargetScaleX, _focusTargetScaleY;
|
||||
float _focusStepScaleX, _focusStepScaleY;
|
||||
uint32 _focusClearTime;
|
||||
|
||||
// Events
|
||||
Thread _eventThread;
|
||||
Thread _timerThread;
|
||||
Common::Queue<Common::Event> _eventQueue;
|
||||
|
||||
// Cursor
|
||||
Graphics::Surface _cursor;
|
||||
Sprite _cursorTexture;
|
||||
bool _cursorPaletteEnabled;
|
||||
bool _cursorVisible;
|
||||
bool _cursorScalable;
|
||||
float _cursorScreenX, _cursorScreenY;
|
||||
float _cursorOverlayX, _cursorOverlayY;
|
||||
float _cursorDeltaX, _cursorDeltaY;
|
||||
int _cursorHotspotX, _cursorHotspotY;
|
||||
uint32 _cursorKeyColor;
|
||||
|
||||
// Magnify
|
||||
MagnifyMode _magnifyMode;
|
||||
u16 _magX, _magY;
|
||||
u16 _magWidth, _magHeight;
|
||||
u16 _magCenterX, _magCenterY;
|
||||
|
||||
Common::Path _logFilePath;
|
||||
|
||||
public:
|
||||
// Pause
|
||||
PauseToken _sleepPauseToken;
|
||||
|
||||
bool _showCursor;
|
||||
bool _snapToBorder;
|
||||
bool _stretchToFit;
|
||||
Screen _screen;
|
||||
};
|
||||
|
||||
} // namespace N3DS
|
||||
|
||||
#endif
|
||||
56
backends/platform/3ds/shader.v.pica
Normal file
56
backends/platform/3ds/shader.v.pica
Normal file
@@ -0,0 +1,56 @@
|
||||
;* ScummVM - Graphic Adventure Engine
|
||||
;*
|
||||
;* ScummVM is the legal property of its developers, whose names
|
||||
;* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
;* file distributed with this source distribution.
|
||||
;*
|
||||
;* This program is free software: you can redistribute it and/or modify
|
||||
;* it under the terms of the GNU General Public License as published by
|
||||
;* the Free Software Foundation, either version 3 of the License, or
|
||||
;* 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 General Public License for more details.
|
||||
;*
|
||||
;* You should have received a copy of the GNU General Public License
|
||||
;* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
; Uniforms
|
||||
.fvec projection[4], modelView[4]
|
||||
|
||||
; Constants
|
||||
.constf myconst(0.0, 1.0, -1.0, 0.1)
|
||||
.alias zeros myconst.xxxx ; Vector full of zeros
|
||||
.alias ones myconst.yyyy ; Vector full of ones
|
||||
|
||||
; Outputs
|
||||
.out outpos position
|
||||
.out outtex texcoord0
|
||||
|
||||
; Inputs (defined as aliases for convenience)
|
||||
.alias inpos v0
|
||||
.alias intex v1
|
||||
|
||||
.proc main
|
||||
; Force the w component of inpos to be 1.0
|
||||
mov r0.xyz, inpos
|
||||
mov r0.w, ones
|
||||
|
||||
; r1 = modelView * inpos
|
||||
dp4 r1.x, modelView[0], r0
|
||||
dp4 r1.y, modelView[1], r0
|
||||
dp4 r1.z, modelView[2], r0
|
||||
dp4 r1.w, modelView[3], r0
|
||||
|
||||
; outpos = projection * r1
|
||||
dp4 outpos.x, projection[0], r1
|
||||
dp4 outpos.y, projection[1], r1
|
||||
dp4 outpos.z, projection[2], r1
|
||||
dp4 outpos.w, projection[3], r1
|
||||
|
||||
mov outtex, intex
|
||||
|
||||
end
|
||||
.end
|
||||
175
backends/platform/3ds/sprite.cpp
Normal file
175
backends/platform/3ds/sprite.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "backends/platform/3ds/osystem.h"
|
||||
#include "backends/platform/3ds/sprite.h"
|
||||
#include "common/algorithm.h"
|
||||
#include "common/util.h"
|
||||
|
||||
namespace N3DS {
|
||||
|
||||
Sprite::Sprite()
|
||||
: textureTransferFlags(0)
|
||||
, dirtyPixels(true)
|
||||
, dirtyMatrix(true)
|
||||
, actualWidth(0)
|
||||
, actualHeight(0)
|
||||
, posX(0)
|
||||
, posY(0)
|
||||
, offsetX(0)
|
||||
, offsetY(0)
|
||||
, scaleX(1.f)
|
||||
, scaleY(1.f)
|
||||
{
|
||||
Mtx_Identity(&modelview);
|
||||
|
||||
vertices = (vertex *)linearAlloc(sizeof(vertex) * 4);
|
||||
}
|
||||
|
||||
Sprite::~Sprite() {
|
||||
free();
|
||||
linearFree(vertices);
|
||||
}
|
||||
|
||||
void Sprite::create(uint16 width, uint16 height, const GfxMode3DS *mode, bool vram) {
|
||||
int16 wPow = MAX<uint16>(Common::nextHigher2(width), 64u);
|
||||
int16 hPow = MAX<uint16>(Common::nextHigher2(height), 64u);
|
||||
|
||||
bool pwrW_hChanged = (wPow != w) || (hPow != h);
|
||||
bool sfcBppChanged = (mode->surfaceFormat.bytesPerPixel != format.bytesPerPixel);
|
||||
bool texFmtChanged = (mode->textureFormat != texture.fmt);
|
||||
|
||||
bool srfDataReinitNeeded = (pwrW_hChanged || sfcBppChanged || !pixels);
|
||||
bool textureReinitNeeded = (pwrW_hChanged || texFmtChanged || !texture.data);
|
||||
|
||||
actualWidth = width;
|
||||
actualHeight = height;
|
||||
format = mode->surfaceFormat;
|
||||
textureTransferFlags = mode->textureTransferFlags;
|
||||
w = wPow;
|
||||
h = hPow;
|
||||
pitch = w * format.bytesPerPixel;
|
||||
dirtyPixels = true;
|
||||
|
||||
if (width && height) {
|
||||
// Don't needlessly reinitialize surface pixels.
|
||||
if (srfDataReinitNeeded) {
|
||||
linearFree(pixels);
|
||||
pixels = linearAlloc(h * pitch);
|
||||
}
|
||||
// Don't needlessly reinitialize C3D_Tex data.
|
||||
if (textureReinitNeeded) {
|
||||
C3D_TexDelete(&texture);
|
||||
if (vram) {
|
||||
if (!C3D_TexInitVRAM(&texture, w, h, mode->textureFormat))
|
||||
C3D_TexInit(&texture, w, h, mode->textureFormat);
|
||||
} else
|
||||
C3D_TexInit(&texture, w, h, mode->textureFormat);
|
||||
}
|
||||
assert(pixels && texture.data);
|
||||
clear();
|
||||
}
|
||||
|
||||
float x = 0.f, y = 0.f;
|
||||
float u = (float)width/w;
|
||||
float v = (float)height/h;
|
||||
vertex tmp[4] = {
|
||||
{{x, y, 0.5f}, {0, 0}},
|
||||
{{x+width, y, 0.5f}, {u, 0}},
|
||||
{{x, y+height, 0.5f}, {0, v}},
|
||||
{{x+width, y+height, 0.5f}, {u, v}},
|
||||
};
|
||||
memcpy(vertices, tmp, sizeof(vertex) * 4);
|
||||
}
|
||||
|
||||
void Sprite::free() {
|
||||
linearFree(pixels);
|
||||
C3D_TexDelete(&texture);
|
||||
pixels = 0;
|
||||
w = h = pitch = 0;
|
||||
actualWidth = actualHeight = 0;
|
||||
format = Graphics::PixelFormat();
|
||||
}
|
||||
|
||||
void Sprite::convertToInPlace(const Graphics::PixelFormat &dstFormat, const byte *palette) {
|
||||
//
|
||||
}
|
||||
|
||||
void Sprite::transfer() {
|
||||
if (pixels && dirtyPixels) {
|
||||
dirtyPixels = false;
|
||||
GSPGPU_FlushDataCache(pixels, w * h * format.bytesPerPixel);
|
||||
C3D_SyncDisplayTransfer((u32*)pixels, GX_BUFFER_DIM(w, h), (u32*)texture.data, GX_BUFFER_DIM(w, h), textureTransferFlags);
|
||||
}
|
||||
}
|
||||
|
||||
void Sprite::render() {
|
||||
C3D_TexBind(0, &texture);
|
||||
|
||||
C3D_BufInfo *bufInfo = C3D_GetBufInfo();
|
||||
BufInfo_Init(bufInfo);
|
||||
BufInfo_Add(bufInfo, vertices, sizeof(vertex), 2, 0x10);
|
||||
C3D_DrawArrays(GPU_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
void Sprite::clear(uint32 color) {
|
||||
dirtyPixels = true;
|
||||
memset(pixels, color, w * h * format.bytesPerPixel);
|
||||
}
|
||||
|
||||
void Sprite::setScale (float x, float y) {
|
||||
if (x != scaleX || y != scaleY) {
|
||||
scaleX = x;
|
||||
scaleY = y;
|
||||
dirtyMatrix = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Sprite::setPosition(int x, int y) {
|
||||
if (x != posX || y != posY) {
|
||||
posX = x;
|
||||
posY = y;
|
||||
dirtyMatrix = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Sprite::setOffset(uint16 x, uint16 y) {
|
||||
offsetX = x;
|
||||
offsetY = y;
|
||||
dirtyMatrix = true;
|
||||
}
|
||||
|
||||
C3D_Mtx* Sprite::getMatrix() {
|
||||
if (dirtyMatrix) {
|
||||
dirtyMatrix = false;
|
||||
Mtx_Identity(&modelview);
|
||||
Mtx_Scale(&modelview, scaleX, scaleY, 1.f);
|
||||
Mtx_Translate(&modelview, posX - offsetX, posY - offsetY, 0, true);
|
||||
}
|
||||
return &modelview;
|
||||
}
|
||||
|
||||
void Sprite::setFilteringMode(bool enableLinearFiltering) {
|
||||
GPU_TEXTURE_FILTER_PARAM filteringMode = enableLinearFiltering ? GPU_LINEAR : GPU_NEAREST;
|
||||
C3D_TexSetFilter(&texture, filteringMode, filteringMode);
|
||||
}
|
||||
|
||||
} // namespace N3DS
|
||||
83
backends/platform/3ds/sprite.h
Normal file
83
backends/platform/3ds/sprite.h
Normal file
@@ -0,0 +1,83 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef GRAPHICS_SPRITE_3DS_H
|
||||
#define GRAPHICS_SPRITE_3DS_H
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
|
||||
#include "graphics/surface.h"
|
||||
#include <3ds.h>
|
||||
#include <citro3d.h>
|
||||
|
||||
namespace N3DS {
|
||||
|
||||
typedef struct {
|
||||
float position[3];
|
||||
float texcoord[2];
|
||||
} vertex;
|
||||
|
||||
struct GfxMode3DS;
|
||||
|
||||
class Sprite : public Graphics::Surface {
|
||||
public:
|
||||
Sprite();
|
||||
~Sprite();
|
||||
void create(uint16 width, uint16 height, const GfxMode3DS *mode, bool vram = false);
|
||||
void free();
|
||||
void convertToInPlace(const Graphics::PixelFormat &dstFormat, const byte *palette = 0);
|
||||
void transfer();
|
||||
void render();
|
||||
void clear(uint32 color = 0);
|
||||
void markDirty(){ dirtyPixels = true; }
|
||||
|
||||
void setPosition(int x, int y);
|
||||
void setOffset(uint16 x, uint16 y);
|
||||
void setScale(float x, float y);
|
||||
float getScaleX() const { return scaleX; }
|
||||
float getScaleY() const { return scaleY; }
|
||||
int getPosX() const { return posX; }
|
||||
int getPosY() const { return posY; }
|
||||
C3D_Mtx* getMatrix();
|
||||
|
||||
void setFilteringMode(bool enableLinearFiltering);
|
||||
|
||||
uint16 actualWidth;
|
||||
uint16 actualHeight;
|
||||
|
||||
private:
|
||||
uint32 textureTransferFlags;
|
||||
bool dirtyPixels;
|
||||
bool dirtyMatrix;
|
||||
C3D_Mtx modelview;
|
||||
C3D_Tex texture;
|
||||
vertex* vertices;
|
||||
int posX;
|
||||
int posY;
|
||||
uint16 offsetX;
|
||||
uint16 offsetY;
|
||||
float scaleX;
|
||||
float scaleY;
|
||||
};
|
||||
|
||||
} // namespace N3DS
|
||||
|
||||
#endif
|
||||
1182
backends/platform/android/android.cpp
Normal file
1182
backends/platform/android/android.cpp
Normal file
File diff suppressed because it is too large
Load Diff
288
backends/platform/android/android.h
Normal file
288
backends/platform/android/android.h
Normal file
@@ -0,0 +1,288 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _ANDROID_H_
|
||||
#define _ANDROID_H_
|
||||
|
||||
#include "backends/platform/android/portdefs.h"
|
||||
#include "common/fs.h"
|
||||
#include "common/archive.h"
|
||||
#include "common/mutex.h"
|
||||
#include "common/ustr.h"
|
||||
#include "audio/mixer_intern.h"
|
||||
#include "backends/modular-backend.h"
|
||||
#include "backends/plugins/posix/posix-provider.h"
|
||||
#include "backends/fs/posix/posix-fs-factory.h"
|
||||
#include "backends/fs/posix/posix-fs-factory.h"
|
||||
#include "backends/log/log.h"
|
||||
#include "backends/platform/android/touchcontrols.h"
|
||||
#include "engines/engine.h"
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
// toggles start
|
||||
//#define ANDROID_DEBUG_ENTER
|
||||
//#define ANDROID_DEBUG_GL
|
||||
//#define ANDROID_DEBUG_GL_CALLS
|
||||
// toggles end
|
||||
|
||||
extern const char *android_log_tag;
|
||||
|
||||
#define _ANDROID_LOG(prio, fmt, args...) __android_log_print(prio, android_log_tag, fmt, ## args)
|
||||
#define LOGD(fmt, args...) _ANDROID_LOG(ANDROID_LOG_DEBUG, fmt, ##args)
|
||||
#define LOGI(fmt, args...) _ANDROID_LOG(ANDROID_LOG_INFO, fmt, ##args)
|
||||
#define LOGW(fmt, args...) _ANDROID_LOG(ANDROID_LOG_WARN, fmt, ##args)
|
||||
#define LOGE(fmt, args...) _ANDROID_LOG(ANDROID_LOG_ERROR, fmt, ##args)
|
||||
|
||||
#define MAX_ANDROID_SCUMMVM_LOG_FILESIZE_IN_BYTES (100*1024)
|
||||
|
||||
#ifdef ANDROID_DEBUG_ENTER
|
||||
#define ENTER(fmt, args...) LOGD("%s(" fmt ")", __FUNCTION__, ##args)
|
||||
#else
|
||||
#define ENTER(fmt, args...) do { } while (false)
|
||||
#endif
|
||||
|
||||
#ifdef ANDROID_DEBUG_GL
|
||||
extern void checkGlError(const char *expr, const char *file, int line);
|
||||
|
||||
#ifdef ANDROID_DEBUG_GL_CALLS
|
||||
#define GLCALLLOG(x, before) \
|
||||
do { \
|
||||
if (before) \
|
||||
LOGD("calling '%s' (%s:%d)", x, __FILE__, __LINE__); \
|
||||
else \
|
||||
LOGD("returned from '%s' (%s:%d)", x, __FILE__, __LINE__); \
|
||||
} while (false)
|
||||
#else
|
||||
#define GLCALLLOG(x, before) do { } while (false)
|
||||
#endif
|
||||
|
||||
#define GLCALL(x) \
|
||||
do { \
|
||||
GLCALLLOG(#x, true); \
|
||||
(x); \
|
||||
GLCALLLOG(#x, false); \
|
||||
checkGlError(#x, __FILE__, __LINE__); \
|
||||
} while (false)
|
||||
|
||||
#define GLTHREADCHECK \
|
||||
do { \
|
||||
assert(dynamic_cast<OSystem_Android *>(g_system)->isRunningInMainThread()); \
|
||||
} while (false)
|
||||
|
||||
#else
|
||||
#define GLCALL(x) do { (x); } while (false)
|
||||
#define GLTHREADCHECK do { } while (false)
|
||||
#endif
|
||||
|
||||
void *androidGLgetProcAddress(const char *name);
|
||||
|
||||
class OSystem_Android : public ModularGraphicsBackend, Common::EventSource {
|
||||
private:
|
||||
static const int kQueuedInputEventDelay = 50;
|
||||
|
||||
struct EventWithDelay : public Common::Event {
|
||||
/** An original timestamp that identifies this event and the delayed ones connected to it that will follow */
|
||||
uint32 originTimeMillis;
|
||||
|
||||
/** The time which the delay starts counting from. It can be set to be later than originTimeMillis */
|
||||
uint32 referTimeMillis;
|
||||
|
||||
/** The delay for the event to be handled */
|
||||
uint32 delayMillis;
|
||||
|
||||
/** The connected EventType of the "connected" event that should be handled before this one */
|
||||
Common::EventType connectedType;
|
||||
|
||||
/** A status flag indicating whether the "connected" event was handled */
|
||||
bool connectedTypeExecuted;
|
||||
|
||||
EventWithDelay() : originTimeMillis(0), referTimeMillis(0), delayMillis(0), connectedType(Common::EVENT_INVALID), connectedTypeExecuted(false) {
|
||||
}
|
||||
|
||||
void reset() {
|
||||
originTimeMillis = 0;
|
||||
referTimeMillis = 0;
|
||||
delayMillis = 0;
|
||||
connectedType = Common::EVENT_INVALID;
|
||||
connectedTypeExecuted = false;
|
||||
}
|
||||
};
|
||||
|
||||
// passed from the dark side
|
||||
int _audio_sample_rate;
|
||||
int _audio_buffer_size;
|
||||
|
||||
int _screen_changeid;
|
||||
|
||||
pthread_t _main_thread;
|
||||
|
||||
bool _timer_thread_exit;
|
||||
pthread_t _timer_thread;
|
||||
|
||||
bool _audio_thread_exit;
|
||||
pthread_t _audio_thread;
|
||||
|
||||
bool _virtkeybd_on;
|
||||
|
||||
Audio::MixerImpl *_mixer;
|
||||
timeval _startTime;
|
||||
|
||||
PauseToken _pauseToken;
|
||||
|
||||
Common::Queue<Common::Event> _event_queue;
|
||||
EventWithDelay _delayedMouseBtnUpEvent;
|
||||
EventWithDelay _delayedMouseBtnDownEvent;
|
||||
Common::Mutex *_event_queue_lock;
|
||||
|
||||
Common::Point _touch_pt_down, _touch_pt_scroll, _touch_pt_dt, _touch_pt_multi;
|
||||
int _eventScaleX;
|
||||
int _eventScaleY;
|
||||
int _touch_mode;
|
||||
int _touchpad_scale; // Used in events.cpp
|
||||
int _trackball_scale; // Used in events.cpp
|
||||
int _dpad_scale; // Used in events.cpp
|
||||
int _joystick_scale; // TODO This seems currently unused. Is it needed?
|
||||
// int _fingersDown;
|
||||
int _firstPointerId;
|
||||
int _secondPointerId;
|
||||
int _thirdPointerId;
|
||||
|
||||
TouchControls _touchControls;
|
||||
|
||||
bool _engineRunning;
|
||||
|
||||
Common::Path _defaultConfigFileName;
|
||||
Common::Path _defaultLogFileName;
|
||||
Common::String _systemPropertiesSummaryStr;
|
||||
Common::String _systemSDKdetectedStr;
|
||||
|
||||
Backends::Log::Log *_logger;
|
||||
|
||||
#if defined(USE_OPENGL) && defined(USE_GLAD)
|
||||
// Cached dlopen object
|
||||
mutable void *_gles2DL;
|
||||
#endif
|
||||
|
||||
static void *timerThreadFunc(void *arg);
|
||||
static void *audioThreadFunc(void *arg);
|
||||
Common::String getSystemProperty(const char *name) const;
|
||||
|
||||
Common::WriteStream *createLogFileForAppending();
|
||||
|
||||
public:
|
||||
enum {
|
||||
TOUCH_MODE_TOUCHPAD = 0,
|
||||
TOUCH_MODE_MOUSE = 1,
|
||||
TOUCH_MODE_GAMEPAD = 2,
|
||||
TOUCH_MODE_MAX = 3
|
||||
};
|
||||
|
||||
enum {
|
||||
SCREEN_ORIENTATION_UNSPECIFIED = 0xffffffff,
|
||||
SCREEN_ORIENTATION_LANDSCAPE = 0,
|
||||
SCREEN_ORIENTATION_PORTRAIT = 1
|
||||
};
|
||||
|
||||
enum {
|
||||
SHOW_ON_SCREEN_NONE = 0,
|
||||
SHOW_ON_SCREEN_MENU = 1,
|
||||
SHOW_ON_SCREEN_INPUT_MODE = 2,
|
||||
SHOW_ON_SCREEN_ALL = 0xffffffff,
|
||||
};
|
||||
|
||||
OSystem_Android(int audio_sample_rate, int audio_buffer_size);
|
||||
virtual ~OSystem_Android();
|
||||
|
||||
void initBackend() override;
|
||||
void engineInit() override;
|
||||
void engineDone() override;
|
||||
|
||||
void updateStartSettings(const Common::String &executable, Common::String &command, Common::StringMap &startSettings, Common::StringArray& additionalArgs) override;
|
||||
|
||||
bool hasFeature(OSystem::Feature f) override;
|
||||
void setFeatureState(OSystem::Feature f, bool enable) override;
|
||||
bool getFeatureState(OSystem::Feature f) override;
|
||||
|
||||
void setPause(bool pause);
|
||||
|
||||
void pushEvent(int type, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6);
|
||||
void pushEvent(const Common::Event &event);
|
||||
void pushEvent(const Common::Event &event1, const Common::Event &event2);
|
||||
void pushDelayedTouchMouseBtnEvents();
|
||||
|
||||
TouchControls &getTouchControls() { return _touchControls; }
|
||||
void applyTouchSettings(bool _3dMode, bool overlayShown);
|
||||
void setupTouchMode(int oldValue, int newValue);
|
||||
|
||||
void applyOrientationSettings();
|
||||
|
||||
void updateOnScreenControls();
|
||||
|
||||
bool pollEvent(Common::Event &event) override;
|
||||
Common::HardwareInputSet *getHardwareInputSet() override;
|
||||
Common::KeymapArray getGlobalKeymaps() override;
|
||||
Common::KeymapperDefaultBindings *getKeymapperDefaultBindings() override;
|
||||
|
||||
Common::Path getDefaultConfigFileName() override;
|
||||
Common::Path getDefaultLogFileName() override;
|
||||
|
||||
void registerDefaultSettings(const Common::String &target) const override;
|
||||
GUI::OptionsContainerWidget *buildBackendOptionsWidget(GUI::GuiObject *boss, const Common::String &name, const Common::String &target) const override;
|
||||
void applyBackendSettings() override;
|
||||
|
||||
uint32 getMillis(bool skipRecord = false) override;
|
||||
void delayMillis(uint msecs) override;
|
||||
Common::MutexInternal *createMutex() override;
|
||||
|
||||
void quit() override;
|
||||
|
||||
void setWindowCaption(const Common::U32String &caption) override;
|
||||
|
||||
Audio::Mixer *getMixer() override;
|
||||
void getTimeAndDate(TimeDate &td, bool skipRecord = false) const override;
|
||||
void logMessage(LogMessageType::Type type, const char *message) override;
|
||||
void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0) override;
|
||||
bool openUrl(const Common::String &url) override;
|
||||
bool hasTextInClipboard() override;
|
||||
Common::U32String getTextFromClipboard() override;
|
||||
bool setTextInClipboard(const Common::U32String &text) override;
|
||||
bool isConnectionLimited() override;
|
||||
Common::String getSystemLanguage() const override;
|
||||
|
||||
#if defined(USE_OPENGL_GAME) || defined(USE_OPENGL_SHADERS)
|
||||
OpenGL::ContextType getOpenGLType() const override { return OpenGL::kContextGLES2; }
|
||||
Common::Array<uint> getSupportedAntiAliasingLevels() const override;
|
||||
#endif
|
||||
#if defined(USE_OPENGL) && defined(USE_GLAD)
|
||||
void *getOpenGLProcAddress(const char *name) const override;
|
||||
#endif
|
||||
|
||||
#ifdef ANDROID_DEBUG_GL
|
||||
bool isRunningInMainThread() { return pthread_self() == _main_thread; }
|
||||
#endif
|
||||
|
||||
virtual const char * const *buildHelpDialogData() override;
|
||||
};
|
||||
|
||||
#endif
|
||||
111
backends/platform/android/android.mk
Normal file
111
backends/platform/android/android.mk
Normal file
@@ -0,0 +1,111 @@
|
||||
# Android specific build targets
|
||||
PATH_DIST = $(srcdir)/dists/android
|
||||
|
||||
GRADLE_FILES = $(shell find $(PATH_DIST)/gradle -type f)
|
||||
|
||||
PATH_BUILD = ./android_project
|
||||
PATH_BUILD_GRADLE = $(PATH_BUILD)/gradle/.timestamp $(PATH_BUILD)/gradlew $(PATH_BUILD)/build.gradle $(PATH_BUILD)/settings.gradle $(PATH_BUILD)/mainAssets/build.gradle $(PATH_BUILD)/gradle.properties $(PATH_BUILD)/local.properties $(PATH_BUILD)/src.properties
|
||||
# $(PATH_BUILD)/mainAssets/src/main/assets is the root and everything is placed in the assets subdirectory
|
||||
PATH_BUILD_ASSETS = $(PATH_BUILD)/mainAssets/src/main/assets
|
||||
PATH_BUILD_LIB = $(PATH_BUILD)/lib/$(ABI)
|
||||
PATH_BUILD_LIBSCUMMVM = $(PATH_BUILD)/lib/$(ABI)/libscummvm.so
|
||||
|
||||
APK_MAIN = ScummVM-debug.apk
|
||||
APK_MAIN_RELEASE = ScummVM-release-unsigned.apk
|
||||
AAB_MAIN_RELEASE = ScummVM-release.aab
|
||||
|
||||
DIST_FILES_PLATFORM = $(PATH_DIST)/android-help.zip $(PATH_DIST)/gamepad.svg
|
||||
|
||||
$(PATH_BUILD):
|
||||
$(MKDIR) $(PATH_BUILD)
|
||||
|
||||
$(PATH_BUILD)/gradle/.timestamp: $(GRADLE_FILES) | $(PATH_BUILD)
|
||||
$(MKDIR) $(PATH_BUILD)/gradle
|
||||
$(CP) -r $(PATH_DIST)/gradle/. $(PATH_BUILD)/gradle/
|
||||
touch "$@"
|
||||
|
||||
$(PATH_BUILD)/gradlew: $(PATH_DIST)/gradlew | $(PATH_BUILD)
|
||||
$(INSTALL) -c -m 755 $< $@
|
||||
|
||||
$(PATH_BUILD)/build.gradle: $(PATH_DIST)/build.gradle | $(PATH_BUILD)
|
||||
$(INSTALL) -c -m 644 $< $@
|
||||
|
||||
$(PATH_BUILD)/settings.gradle: $(PATH_DIST)/settings.gradle | $(PATH_BUILD)
|
||||
$(INSTALL) -c -m 644 $< $@
|
||||
|
||||
$(PATH_BUILD)/gradle.properties: $(PATH_DIST)/gradle.properties | $(PATH_BUILD)
|
||||
$(INSTALL) -c -m 644 $< $@
|
||||
|
||||
$(PATH_BUILD)/local.properties: configure.stamp | $(PATH_BUILD)
|
||||
$(ECHO) "sdk.dir=$(realpath $(ANDROID_SDK_ROOT))\n" > $(PATH_BUILD)/local.properties
|
||||
|
||||
$(PATH_BUILD)/src.properties: configure.stamp | $(PATH_BUILD)
|
||||
$(ECHO) "srcdir=$(realpath $(srcdir))\n" > $(PATH_BUILD)/src.properties
|
||||
|
||||
$(PATH_BUILD)/mainAssets/build.gradle: $(PATH_DIST)/mainAssets.gradle | $(PATH_BUILD_ASSETS)/MD5SUMS
|
||||
$(INSTALL) -c -m 644 $< $@
|
||||
|
||||
$(PATH_BUILD_ASSETS)/MD5SUMS: config.mk $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_DOCS) $(DIST_FILES_PLATFORM) $(DIST_FILES_SHADERS) | $(PATH_BUILD)
|
||||
$(INSTALL) -d $(PATH_BUILD_ASSETS)/assets/
|
||||
$(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) $(DIST_FILES_SOUNDFONTS) $(DIST_FILES_NETWORKING) $(DIST_FILES_VKEYBD) $(DIST_FILES_PLATFORM) $(PATH_BUILD_ASSETS)/assets/
|
||||
ifneq ($(DIST_FILES_SHADERS),)
|
||||
$(INSTALL) -d $(PATH_BUILD_ASSETS)/assets/shaders
|
||||
$(INSTALL) -c -m 644 $(DIST_FILES_SHADERS) $(PATH_BUILD_ASSETS)/assets/shaders
|
||||
endif
|
||||
ifneq ($(GAMES_BUNDLE_DIRECTORY),)
|
||||
$(INSTALL) -d $(PATH_BUILD_ASSETS)/assets/games
|
||||
$(CP) -r $(GAMES_BUNDLE_DIRECTORY)/. $(PATH_BUILD_ASSETS)/assets/games/
|
||||
endif
|
||||
$(INSTALL) -d $(PATH_BUILD_ASSETS)/doc/
|
||||
$(INSTALL) -c -m 644 $(DIST_FILES_DOCS) $(PATH_BUILD_ASSETS)/doc/
|
||||
(cd $(PATH_BUILD_ASSETS)/ && find assets doc -type f | sort | xargs md5sum) > $@
|
||||
|
||||
$(PATH_BUILD_LIBSCUMMVM): libscummvm.so | $(PATH_BUILD)
|
||||
$(INSTALL) -d $(PATH_BUILD_LIB)
|
||||
$(INSTALL) -c -m 644 libscummvm.so $(PATH_BUILD_LIBSCUMMVM)
|
||||
|
||||
$(APK_MAIN): $(PATH_BUILD_GRADLE) $(PATH_BUILD_ASSETS)/MD5SUMS $(PATH_BUILD_LIBSCUMMVM) | $(PATH_BUILD)
|
||||
(cd $(PATH_BUILD); ./gradlew assembleDebug)
|
||||
$(CP) $(PATH_BUILD)/build/outputs/apk/debug/$(APK_MAIN) $@
|
||||
|
||||
$(APK_MAIN_RELEASE): $(PATH_BUILD_GRADLE) $(PATH_BUILD_ASSETS)/MD5SUMS $(PATH_BUILD_LIBSCUMMVM) | $(PATH_BUILD)
|
||||
(cd $(PATH_BUILD); ./gradlew assembleRelease)
|
||||
$(CP) $(PATH_BUILD)/build/outputs/apk/release/$(APK_MAIN_RELEASE) $@
|
||||
|
||||
$(AAB_MAIN_RELEASE): $(PATH_BUILD_GRADLE) $(PATH_BUILD_ASSETS)/MD5SUMS $(PATH_BUILD_LIBSCUMMVM) | $(PATH_BUILD)
|
||||
(cd $(PATH_BUILD); ./gradlew bundleRelease -PsplitAssets)
|
||||
$(CP) $(PATH_BUILD)/build/outputs/bundle/release/$(AAB_MAIN_RELEASE) $@
|
||||
|
||||
clean: androidclean
|
||||
|
||||
# SUBPATH_BUILDS is set in fatbundle.mk
|
||||
androidclean:
|
||||
$(RM) -rf $(PATH_BUILD) $(SUBPATH_BUILDS) *.apk *.aab
|
||||
|
||||
all: $(APK_MAIN)
|
||||
|
||||
androidrelease: $(APK_MAIN_RELEASE)
|
||||
androidbundlerelease: $(AAB_MAIN_RELEASE)
|
||||
|
||||
androidtest: $(APK_MAIN)
|
||||
(cd $(PATH_BUILD); ./gradlew installDebug)
|
||||
|
||||
# used by buildbot!
|
||||
androiddistdebug: all
|
||||
$(MKDIR) debug
|
||||
$(CP) $(APK_MAIN) debug/
|
||||
for i in $(DIST_FILES_DOCS); do \
|
||||
sed 's/$$/\r/' < $$i > debug/`basename $$i`.txt; \
|
||||
done
|
||||
|
||||
androiddistrelease: androidrelease
|
||||
$(MKDIR) release
|
||||
$(CP) $(APK_MAIN_RELEASE) release/
|
||||
for i in $(DIST_FILES_DOCS); do \
|
||||
sed 's/$$/\r/' < $$i > release/`basename $$i`.txt; \
|
||||
done
|
||||
|
||||
ANDROID_BUILD_RULES := androidrelease androidbundlerelease androidtest androiddistdebug androiddistrelease
|
||||
.PHONY: androidclean $(ANDROID_BUILD_RULES)
|
||||
|
||||
include $(srcdir)/backends/platform/android/fatbundle.mk
|
||||
162
backends/platform/android/asset-archive.cpp
Normal file
162
backends/platform/android/asset-archive.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <android/asset_manager.h>
|
||||
#include <android/asset_manager_jni.h>
|
||||
|
||||
#include "backends/platform/android/jni-android.h"
|
||||
#include "backends/platform/android/asset-archive.h"
|
||||
|
||||
#include "common/str.h"
|
||||
#include "common/stream.h"
|
||||
#include "common/util.h"
|
||||
#include "common/archive.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/textconsole.h"
|
||||
|
||||
class AssetInputStream : public Common::SeekableReadStream {
|
||||
public:
|
||||
AssetInputStream(AAssetManager *as, const Common::Path &path);
|
||||
virtual ~AssetInputStream();
|
||||
|
||||
virtual bool eos() const { return _eos; }
|
||||
|
||||
virtual void clearErr() {_eos = false; }
|
||||
|
||||
virtual uint32 read(void *dataPtr, uint32 dataSize);
|
||||
|
||||
virtual int64 pos() const { return _pos; }
|
||||
|
||||
virtual int64 size() const { return _len; }
|
||||
|
||||
virtual bool seek(int64 offset, int whence = SEEK_SET);
|
||||
|
||||
private:
|
||||
void close();
|
||||
AAsset *_asset;
|
||||
|
||||
int64 _pos;
|
||||
int64 _len;
|
||||
bool _eos;
|
||||
};
|
||||
|
||||
AssetInputStream::AssetInputStream(AAssetManager *as, const Common::Path &path) :
|
||||
_eos(false), _pos(0) {
|
||||
_asset = AAssetManager_open(as, path.toString(Common::Path::kNativeSeparator).c_str(), AASSET_MODE_RANDOM);
|
||||
_len = AAsset_getLength64(_asset);
|
||||
}
|
||||
|
||||
AssetInputStream::~AssetInputStream() {
|
||||
if (_asset != NULL) {
|
||||
AAsset_close(_asset);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetInputStream::close() {
|
||||
AAsset_close(_asset);
|
||||
}
|
||||
|
||||
uint32 AssetInputStream::read(void *dataPtr, uint32 dataSize) {
|
||||
uint32 readlen = AAsset_read(_asset, dataPtr, dataSize);
|
||||
_pos += readlen;
|
||||
if (readlen != dataSize) {
|
||||
_eos = true;
|
||||
}
|
||||
return readlen;
|
||||
}
|
||||
|
||||
bool AssetInputStream::seek(int64 offset, int whence) {
|
||||
int64 res = AAsset_seek64(_asset, offset, whence);
|
||||
if (res == -1) {
|
||||
return false;
|
||||
}
|
||||
if (whence == SEEK_CUR) {
|
||||
_pos += offset;
|
||||
} else if (whence == SEEK_SET) {
|
||||
_pos = offset;
|
||||
} else if (whence == SEEK_END) {
|
||||
_pos = _len + offset;
|
||||
}
|
||||
assert(_pos <= _len);
|
||||
_eos = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
AndroidAssetArchive::AndroidAssetArchive(jobject am) : _hasCached(false) {
|
||||
JNIEnv *env = JNI::getEnv();
|
||||
|
||||
_am = AAssetManager_fromJava(env, am);
|
||||
}
|
||||
|
||||
AndroidAssetArchive::~AndroidAssetArchive() {
|
||||
}
|
||||
|
||||
bool AndroidAssetArchive::hasFile(const Common::Path &path) const {
|
||||
Common::String name = path.toString(Common::Path::kNativeSeparator);
|
||||
AAsset *asset = AAssetManager_open(_am, name.c_str(), AASSET_MODE_RANDOM);
|
||||
bool exists = false;
|
||||
if (asset != NULL) {
|
||||
exists = true;
|
||||
AAsset_close(asset);
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
int AndroidAssetArchive::listMembers(Common::ArchiveMemberList &member_list) const {
|
||||
if (_hasCached) {
|
||||
member_list.insert(member_list.end(), _cachedMembers.begin(), _cachedMembers.end());
|
||||
return _cachedMembers.size();
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
Common::List<Common::Path> dirs;
|
||||
dirs.push_back("");
|
||||
for (const auto& currentDir : dirs) {
|
||||
AAssetDir *dir = AAssetManager_openDir(_am, "");
|
||||
const char *file = AAssetDir_getNextFileName(dir);
|
||||
|
||||
while (file) {
|
||||
member_list.push_back(getMember(currentDir.appendComponent(file)));
|
||||
++count;
|
||||
file = AAssetDir_getNextFileName(dir);
|
||||
}
|
||||
AAssetDir_close(dir);
|
||||
}
|
||||
|
||||
_cachedMembers = Common::ArchiveMemberList(member_list);
|
||||
_hasCached = true;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
const Common::ArchiveMemberPtr AndroidAssetArchive::getMember(const Common::Path &path) const {
|
||||
return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(path, *this));
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *AndroidAssetArchive::createReadStreamForMember(const Common::Path &path) const {
|
||||
if (!hasFile(path)) {
|
||||
return nullptr;
|
||||
}
|
||||
return new AssetInputStream(_am, path);
|
||||
}
|
||||
50
backends/platform/android/asset-archive.h
Normal file
50
backends/platform/android/asset-archive.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _ANDROID_ASSET_H_
|
||||
#define _ANDROID_ASSET_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include "common/str.h"
|
||||
#include "common/stream.h"
|
||||
#include "common/util.h"
|
||||
#include "common/archive.h"
|
||||
|
||||
#include <android/asset_manager.h>
|
||||
|
||||
class AndroidAssetArchive : public Common::Archive {
|
||||
public:
|
||||
AndroidAssetArchive(jobject am);
|
||||
virtual ~AndroidAssetArchive();
|
||||
|
||||
bool hasFile(const Common::Path &path) const override;
|
||||
int listMembers(Common::ArchiveMemberList &list) const override;
|
||||
const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
|
||||
Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
|
||||
|
||||
private:
|
||||
AAssetManager *_am;
|
||||
mutable Common::ArchiveMemberList _cachedMembers;
|
||||
mutable bool _hasCached;
|
||||
};
|
||||
|
||||
#endif
|
||||
90
backends/platform/android/build-release.sh
Normal file
90
backends/platform/android/build-release.sh
Normal file
@@ -0,0 +1,90 @@
|
||||
#! /bin/sh
|
||||
|
||||
set -ex
|
||||
|
||||
# Run from the build folder
|
||||
|
||||
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../../.." && pwd)
|
||||
NPROC=$(nproc)
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
GAMES_FOLDER=$(CDPATH= cd -- "$1" && pwd)
|
||||
fi
|
||||
|
||||
# Get the version code and patch it for every build
|
||||
VERSION_CODE=$(sed -n -e '/versionCode /s/[\t ]*versionCode //p' "${ROOT}/dists/android/build.gradle")
|
||||
# Make sure the last digit is 0
|
||||
VERSION_CODE=$((${VERSION_CODE} / 10 * 10))
|
||||
|
||||
# Get the version name to name assets
|
||||
VERSION_NAME=$(sed -n -e '/versionName /s/[\t ]*versionName "\(.*\)"$/\1/p' "${ROOT}/dists/android/build.gradle")
|
||||
|
||||
patch_version() {
|
||||
local dir
|
||||
dir=$2
|
||||
if [ -z "$dir" ]; then
|
||||
dir=.
|
||||
fi
|
||||
# Make sure the file exists before patching
|
||||
make -C "$dir" android_project/build.gradle
|
||||
sed -i -e "/versionCode /s/\\([\t ]*versionCode \\).*/\\1$1/" "$dir/android_project/build.gradle"
|
||||
}
|
||||
|
||||
# We don't handle games change correctly, force refresh
|
||||
rm -rf "./android_project/mainAssets/src/main/assets/assets/games"
|
||||
|
||||
"${ROOT}/configure" --host=android-arm-v7a --disable-debug --enable-release
|
||||
|
||||
# Make sure we use the proper versionCode
|
||||
patch_version ${VERSION_CODE}
|
||||
|
||||
# Build an AAB bundle with games
|
||||
make -j${NPROC} androidfatbundlerelease GAMES_BUNDLE_DIRECTORY="$GAMES_FOLDER"
|
||||
|
||||
mv ScummVM-release.aab scummvm-${VERSION_NAME}-android.aab
|
||||
|
||||
# For APK strip out the games
|
||||
if [ -n "$GAMES_FOLDER" ]; then
|
||||
rm -rf "./android_project/mainAssets/src/main/assets/assets/games"
|
||||
fi
|
||||
|
||||
# Reuse what we just built to create APKs
|
||||
|
||||
# Cleanup fat shared objects
|
||||
rm -rf ./android_project/lib/*
|
||||
|
||||
plat_build() {
|
||||
local subcode subarch subbuild
|
||||
subcode=$1
|
||||
subarch=$2
|
||||
subbuild=$3
|
||||
if [ -z "$subbuild" ]; then
|
||||
subbuild=build-android${subarch}
|
||||
fi
|
||||
patch_version $((${VERSION_CODE} + ${subcode})) "${subbuild}"
|
||||
make -j${NPROC} -C ${subbuild} androidrelease
|
||||
mv ${subbuild}/ScummVM-release-unsigned.apk scummvm-${VERSION_NAME}-android-${subarch}-unsigned.apk
|
||||
}
|
||||
|
||||
# Build ARMv7a with versionCode 1
|
||||
plat_build 1 armeabi-v7a .
|
||||
|
||||
# Build ARM64 with versionCode 2
|
||||
plat_build 2 arm64-v8a
|
||||
|
||||
# Build x86 with versionCode 3
|
||||
plat_build 3 x86
|
||||
|
||||
# Build x86_64 with versionCode 4
|
||||
plat_build 4 x86_64
|
||||
|
||||
# Finally build ARMv7a without NEON with versionCode 1
|
||||
|
||||
# Configure...
|
||||
mkdir -p build-androidarmeabi-v7a-nn
|
||||
cd build-androidarmeabi-v7a-nn
|
||||
"${ROOT}/configure" --host=android-arm-v7a --disable-ext-neon --disable-debug --enable-release
|
||||
cd ..
|
||||
|
||||
# ...build like the others
|
||||
plat_build 1 armeabi-v7a-nn
|
||||
1595
backends/platform/android/events.cpp
Normal file
1595
backends/platform/android/events.cpp
Normal file
File diff suppressed because it is too large
Load Diff
35
backends/platform/android/fatbundle.mk
Normal file
35
backends/platform/android/fatbundle.mk
Normal file
@@ -0,0 +1,35 @@
|
||||
ALL_ABIS = armeabi-v7a arm64-v8a x86 x86_64
|
||||
OTHER_ABIS = $(filter-out $(ABI), $(ALL_ABIS))
|
||||
|
||||
PATH_BUILD_LIBSSCUMMVM = $(foreach abi, $(OTHER_ABIS), $(PATH_BUILD)/lib/$(abi)/libscummvm.so)
|
||||
|
||||
ANDROID_CONFIGURE_PATH := $(realpath $(srcdir)/configure)
|
||||
ANDROID_CONFIGFLAGS := $(filter-out --host=android-%, $(SAVED_CONFIGFLAGS))
|
||||
define BUILD_ANDROID
|
||||
SUBPATH_BUILD_LIBSCUMMVM_abi := ./build-android$(1)/libscummvm.so
|
||||
PATH_BUILD_LIBSCUMMVM_abi := $(PATH_BUILD)/lib/$(1)/libscummvm.so
|
||||
|
||||
SUBPATH_BUILDS += ./build-android$(1)
|
||||
|
||||
$$(SUBPATH_BUILD_LIBSCUMMVM_abi): SUBPATH_BUILD=./build-android$(1)
|
||||
$$(SUBPATH_BUILD_LIBSCUMMVM_abi): config.mk $$(EXECUTABLE)
|
||||
$$(INSTALL) -d "$$(SUBPATH_BUILD)"
|
||||
(cd "$$(SUBPATH_BUILD)" && \
|
||||
$$(foreach VAR,$$(SAVED_ENV_VARS),$$(VAR)="$$(SAVED_$$(VAR))") \
|
||||
"$$(ANDROID_CONFIGURE_PATH)" --host=android-$(1) $$(ANDROID_CONFIGFLAGS))
|
||||
$$(MAKE) -C "$$(SUBPATH_BUILD)" $$(EXECUTABLE)
|
||||
|
||||
$$(PATH_BUILD_LIBSCUMMVM_abi): PATH_BUILD_LIB=$(PATH_BUILD)/lib/$(1)
|
||||
$$(PATH_BUILD_LIBSCUMMVM_abi): $$(SUBPATH_BUILD_LIBSCUMMVM_abi)
|
||||
$$(INSTALL) -d "$$(PATH_BUILD_LIB)"
|
||||
$$(INSTALL) -c -m 644 "$$<" "$$@"
|
||||
|
||||
endef
|
||||
|
||||
SUBPATH_BUILDS :=
|
||||
$(foreach abi,$(OTHER_ABIS),$(eval $(call BUILD_ANDROID,$(abi))))
|
||||
|
||||
androidfatall $(subst android,androidfat,$(ANDROID_BUILD_RULES)): androidfat%: $(PATH_BUILD_LIBSSCUMMVM)
|
||||
$(MAKE) $(if $(filter all,$*),$*,android$*)
|
||||
|
||||
.PHONY: androidfatall $(subst android,androidfat,$(ANDROID_BUILD_RULES))
|
||||
1190
backends/platform/android/jni-android.cpp
Normal file
1190
backends/platform/android/jni-android.cpp
Normal file
File diff suppressed because it is too large
Load Diff
234
backends/platform/android/jni-android.h
Normal file
234
backends/platform/android/jni-android.h
Normal file
@@ -0,0 +1,234 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _ANDROID_JNI_H_
|
||||
#define _ANDROID_JNI_H_
|
||||
|
||||
#include <jni.h>
|
||||
#include <semaphore.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "common/fs.h"
|
||||
#include "common/archive.h"
|
||||
#include "common/array.h"
|
||||
#include "common/ustr.h"
|
||||
|
||||
namespace Graphics {
|
||||
struct Surface;
|
||||
}
|
||||
|
||||
class OSystem_Android;
|
||||
|
||||
class JNI {
|
||||
private:
|
||||
JNI();
|
||||
virtual ~JNI();
|
||||
|
||||
public:
|
||||
static bool assets_updated;
|
||||
|
||||
static bool pause;
|
||||
static sem_t pause_sem;
|
||||
|
||||
static int surface_changeid;
|
||||
static int egl_surface_width;
|
||||
static int egl_surface_height;
|
||||
static int egl_bits_per_pixel;
|
||||
|
||||
static bool virt_keyboard_state;
|
||||
|
||||
static int32 gestures_insets[4];
|
||||
static int32 cutout_insets[4];
|
||||
|
||||
static jint onLoad(JavaVM *vm);
|
||||
|
||||
static inline JNIEnv *getEnv() {
|
||||
JNIEnv *env = (JNIEnv*) pthread_getspecific(_env_tls);
|
||||
if (env != nullptr) {
|
||||
return env;
|
||||
}
|
||||
|
||||
return fetchEnv();
|
||||
}
|
||||
|
||||
static void attachThread();
|
||||
static void detachThread();
|
||||
|
||||
static void setReadyForEvents(bool ready);
|
||||
static void wakeupForQuit();
|
||||
|
||||
static void setWindowCaption(const Common::U32String &caption);
|
||||
|
||||
/**
|
||||
* Array members of DPIValues are xdpi, ydpi, density
|
||||
**/
|
||||
typedef float DPIValues[3];
|
||||
static void getDPI(DPIValues &values);
|
||||
static void displayMessageOnOSD(const Common::U32String &msg);
|
||||
static bool openUrl(const Common::String &url);
|
||||
static bool hasTextInClipboard();
|
||||
static Common::U32String getTextFromClipboard();
|
||||
static bool setTextInClipboard(const Common::U32String &text);
|
||||
static bool isConnectionLimited();
|
||||
static void showVirtualKeyboard(bool enable);
|
||||
static void showOnScreenControls(int enableMask);
|
||||
static void setTouchMode(int touchMode);
|
||||
static int getTouchMode();
|
||||
static void setOrientation(int touchMode);
|
||||
static void addSysArchivesToSearchSet(Common::SearchSet &s, int priority);
|
||||
static Common::String getScummVMBasePath();
|
||||
static Common::String getScummVMAssetsPath();
|
||||
static Common::String getScummVMConfigPath();
|
||||
static Common::String getScummVMLogPath();
|
||||
static jint getAndroidSDKVersionId();
|
||||
static void setCurrentGame(const Common::String &target);
|
||||
|
||||
static inline bool haveSurface();
|
||||
static inline bool swapBuffers();
|
||||
static bool initSurface();
|
||||
static void deinitSurface();
|
||||
static int eglVersion() {
|
||||
if (_egl_version) {
|
||||
return _egl_version;
|
||||
}
|
||||
return fetchEGLVersion();
|
||||
}
|
||||
|
||||
static void setAudioPause();
|
||||
static void setAudioPlay();
|
||||
static void setAudioStop();
|
||||
|
||||
static inline int writeAudio(JNIEnv *env, jbyteArray &data, int offset,
|
||||
int size);
|
||||
|
||||
static Common::Array<Common::String> getAllStorageLocations();
|
||||
|
||||
static jobject getNewSAFTree(bool writable, const Common::String &initURI, const Common::String &prompt);
|
||||
static Common::Array<jobject> getSAFTrees();
|
||||
static jobject findSAFTree(const Common::String &name);
|
||||
|
||||
static int exportBackup(const Common::U32String &prompt);
|
||||
static int importBackup(const Common::U32String &prompt, const Common::String &path);
|
||||
|
||||
private:
|
||||
static pthread_key_t _env_tls;
|
||||
|
||||
static JavaVM *_vm;
|
||||
// back pointer to (java) peer instance
|
||||
static jobject _jobj;
|
||||
static jobject _jobj_audio_track;
|
||||
static jobject _jobj_egl;
|
||||
static jobject _jobj_egl_display;
|
||||
static jobject _jobj_egl_surface;
|
||||
// cached EGL version
|
||||
static int _egl_version;
|
||||
|
||||
static Common::Archive *_asset_archive;
|
||||
static OSystem_Android *_system;
|
||||
|
||||
static bool _ready_for_events;
|
||||
|
||||
static jmethodID _MID_getDPI;
|
||||
static jmethodID _MID_displayMessageOnOSD;
|
||||
static jmethodID _MID_openUrl;
|
||||
static jmethodID _MID_hasTextInClipboard;
|
||||
static jmethodID _MID_getTextFromClipboard;
|
||||
static jmethodID _MID_setTextInClipboard;
|
||||
static jmethodID _MID_isConnectionLimited;
|
||||
static jmethodID _MID_setWindowCaption;
|
||||
static jmethodID _MID_showVirtualKeyboard;
|
||||
static jmethodID _MID_showOnScreenControls;
|
||||
static jmethodID _MID_setTouchMode;
|
||||
static jmethodID _MID_getTouchMode;
|
||||
static jmethodID _MID_setOrientation;
|
||||
static jmethodID _MID_getScummVMBasePath;
|
||||
static jmethodID _MID_getScummVMConfigPath;
|
||||
static jmethodID _MID_getScummVMLogPath;
|
||||
static jmethodID _MID_setCurrentGame;
|
||||
static jmethodID _MID_getSysArchives;
|
||||
static jmethodID _MID_getAllStorageLocations;
|
||||
static jmethodID _MID_initSurface;
|
||||
static jmethodID _MID_deinitSurface;
|
||||
static jmethodID _MID_eglVersion;
|
||||
static jmethodID _MID_getNewSAFTree;
|
||||
static jmethodID _MID_getSAFTrees;
|
||||
static jmethodID _MID_findSAFTree;
|
||||
static jmethodID _MID_exportBackup;
|
||||
static jmethodID _MID_importBackup;
|
||||
|
||||
static jmethodID _MID_EGL10_eglSwapBuffers;
|
||||
|
||||
static jmethodID _MID_AudioTrack_flush;
|
||||
static jmethodID _MID_AudioTrack_pause;
|
||||
static jmethodID _MID_AudioTrack_play;
|
||||
static jmethodID _MID_AudioTrack_stop;
|
||||
static jmethodID _MID_AudioTrack_write;
|
||||
|
||||
static const JNINativeMethod _natives[];
|
||||
|
||||
static void throwByName(JNIEnv *env, const char *name, const char *msg);
|
||||
static void throwRuntimeException(JNIEnv *env, const char *msg);
|
||||
|
||||
// natives for the dark side
|
||||
static void create(JNIEnv *env, jobject self, jobject asset_manager,
|
||||
jobject egl, jobject egl_display,
|
||||
jobject at, jint audio_sample_rate,
|
||||
jint audio_buffer_size,
|
||||
jboolean assets_updated_);
|
||||
static void destroy(JNIEnv *env, jobject self);
|
||||
|
||||
static void setSurface(JNIEnv *env, jobject self, jint width, jint height, jint bpp);
|
||||
static jint main(JNIEnv *env, jobject self, jobjectArray args);
|
||||
|
||||
static void pushEvent(JNIEnv *env, jobject self, int type, int arg1,
|
||||
int arg2, int arg3, int arg4, int arg5, int arg6);
|
||||
static void updateTouch(JNIEnv *env, jobject self, int action, int ptr, int x, int y);
|
||||
static void setupTouchMode(JNIEnv *env, jobject self, jint oldValue, jint newValue);
|
||||
static void syncVirtkeyboardState(JNIEnv *env, jobject self, jboolean newState);
|
||||
static void setPause(JNIEnv *env, jobject self, jboolean value);
|
||||
|
||||
static void systemInsetsUpdated(JNIEnv *env, jobject self, jintArray gestureInsets, jintArray systemInsets, jintArray cutoutInsets);
|
||||
|
||||
static jstring getNativeVersionInfo(JNIEnv *env, jobject self);
|
||||
static jstring convertToJString(JNIEnv *env, const Common::U32String &str);
|
||||
static Common::U32String convertFromJString(JNIEnv *env, const jstring &jstr);
|
||||
|
||||
static JNIEnv *fetchEnv();
|
||||
static int fetchEGLVersion();
|
||||
};
|
||||
|
||||
inline bool JNI::haveSurface() {
|
||||
return _jobj_egl_surface != 0;
|
||||
}
|
||||
|
||||
inline bool JNI::swapBuffers() {
|
||||
JNIEnv *env = JNI::getEnv();
|
||||
|
||||
return env->CallBooleanMethod(_jobj_egl, _MID_EGL10_eglSwapBuffers,
|
||||
_jobj_egl_display, _jobj_egl_surface);
|
||||
}
|
||||
|
||||
inline int JNI::writeAudio(JNIEnv *env, jbyteArray &data, int offset, int size) {
|
||||
return env->CallIntMethod(_jobj_audio_track, _MID_AudioTrack_write, data,
|
||||
offset, size);
|
||||
}
|
||||
|
||||
#endif
|
||||
25
backends/platform/android/module.mk
Normal file
25
backends/platform/android/module.mk
Normal file
@@ -0,0 +1,25 @@
|
||||
MODULE := backends/platform/android
|
||||
|
||||
MODULE_OBJS := \
|
||||
jni-android.o \
|
||||
asset-archive.o \
|
||||
android.o \
|
||||
events.o \
|
||||
options.o \
|
||||
snprintf.o \
|
||||
touchcontrols.o
|
||||
|
||||
ifdef NEED_ANDROID_CPUFEATURES
|
||||
MODULE_OBJS += \
|
||||
cpu-features.o
|
||||
$(MODULE)/android.o: CXXFLAGS += "-I$(ANDROID_NDK_ROOT)/sources/android/cpufeatures"
|
||||
# We don't configure a C compiler, use a C++ one in C mode
|
||||
$(MODULE)/cpu-features.o: $(ANDROID_NDK_ROOT)/sources/android/cpufeatures/cpu-features.c
|
||||
$(QUIET)$(MKDIR) $(*D)
|
||||
$(QUIET_CXX)$(CXX) $(CXXFLAGS) $(CPPFLAGS) -x c -std=c99 -c $(<) -o $@
|
||||
endif
|
||||
|
||||
# We don't use rules.mk but rather manually update OBJS and MODULE_DIRS.
|
||||
MODULE_OBJS := $(addprefix $(MODULE)/, $(MODULE_OBJS))
|
||||
OBJS := $(MODULE_OBJS) $(OBJS)
|
||||
MODULE_DIRS += $(sort $(dir $(MODULE_OBJS)))
|
||||
658
backends/platform/android/options.cpp
Normal file
658
backends/platform/android/options.cpp
Normal file
@@ -0,0 +1,658 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Allow use of stuff in <time.h>
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
|
||||
// Disable printf override in common/forbidden.h to avoid
|
||||
// clashes with log.h from the Android SDK.
|
||||
// That header file uses
|
||||
// __attribute__ ((format(printf, 3, 4)))
|
||||
// which gets messed up by our override mechanism; this could
|
||||
// be avoided by either changing the Android SDK to use the equally
|
||||
// legal and valid
|
||||
// __attribute__ ((format(__printf__, 3, 4)))
|
||||
// or by refining our printf override to use a varadic macro
|
||||
// (which then wouldn't be portable, though).
|
||||
// Anyway, for now we just disable the printf override globally
|
||||
// for the Android port
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_printf
|
||||
|
||||
#include "backends/platform/android/android.h"
|
||||
#include "backends/platform/android/jni-android.h"
|
||||
#include "backends/fs/android/android-fs-factory.h"
|
||||
#include "backends/fs/android/android-saf-fs.h"
|
||||
#include "backends/graphics/android/android-graphics.h"
|
||||
|
||||
#include "gui/browser.h"
|
||||
#include "gui/gui-manager.h"
|
||||
#include "gui/message.h"
|
||||
#include "gui/ThemeEval.h"
|
||||
#include "gui/widget.h"
|
||||
#include "gui/widgets/list.h"
|
||||
#include "gui/widgets/popup.h"
|
||||
|
||||
#include "common/translation.h"
|
||||
|
||||
enum {
|
||||
kRemoveCmd = 'RemS',
|
||||
kExportBackupCmd = 'ExpD',
|
||||
kImportBackupCmd = 'ImpD',
|
||||
};
|
||||
|
||||
class AndroidOptionsWidget final : public GUI::OptionsContainerWidget {
|
||||
public:
|
||||
explicit AndroidOptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain);
|
||||
~AndroidOptionsWidget() override;
|
||||
|
||||
// OptionsContainerWidget API
|
||||
void load() override;
|
||||
bool save() override;
|
||||
bool hasKeys() override;
|
||||
void setEnabled(bool e) override;
|
||||
|
||||
private:
|
||||
// OptionsContainerWidget API
|
||||
void defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const override;
|
||||
void handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
|
||||
GUI::CheckboxWidget *_onscreenCheckbox;
|
||||
GUI::CheckboxWidget *_ignoreGameSafeAreaCheckbox;
|
||||
GUI::StaticTextWidget *_preferredTouchModeDesc;
|
||||
GUI::StaticTextWidget *_preferredTMMenusDesc;
|
||||
GUI::PopUpWidget *_preferredTMMenusPopUp;
|
||||
GUI::StaticTextWidget *_preferredTM2DGamesDesc;
|
||||
GUI::PopUpWidget *_preferredTM2DGamesPopUp;
|
||||
GUI::StaticTextWidget *_preferredTM3DGamesDesc;
|
||||
GUI::PopUpWidget *_preferredTM3DGamesPopUp;
|
||||
GUI::StaticTextWidget *_orientationDesc;
|
||||
GUI::StaticTextWidget *_orientationMenusDesc;
|
||||
GUI::PopUpWidget *_orientationMenusPopUp;
|
||||
GUI::StaticTextWidget *_orientationGamesDesc;
|
||||
GUI::PopUpWidget *_orientationGamesPopUp;
|
||||
|
||||
bool _enabled;
|
||||
|
||||
uint32 loadTouchMode(const Common::String &setting, bool acceptDefault, uint32 defaultValue);
|
||||
void saveTouchMode(const Common::String &setting, uint32 touchMode);
|
||||
uint32 loadOrientation(const Common::String &setting, bool acceptDefault, uint32 defaultValue);
|
||||
void saveOrientation(const Common::String &setting, uint32 orientation);
|
||||
};
|
||||
|
||||
class SAFRemoveDialog : public GUI::Dialog {
|
||||
public:
|
||||
SAFRemoveDialog();
|
||||
virtual ~SAFRemoveDialog();
|
||||
|
||||
void open() override;
|
||||
void reflowLayout() override;
|
||||
|
||||
void handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) override;
|
||||
|
||||
protected:
|
||||
GUI::ListWidget *_safList;
|
||||
AbstractFSList _safTrees;
|
||||
|
||||
void clearListing();
|
||||
void updateListing();
|
||||
};
|
||||
|
||||
enum {
|
||||
kTouchModeDefault = -1,
|
||||
kTouchModeTouchpad = 0,
|
||||
kTouchModeMouse,
|
||||
kTouchModeGamepad,
|
||||
};
|
||||
|
||||
enum {
|
||||
kOrientationDefault = -1,
|
||||
kOrientationAuto = 0,
|
||||
kOrientationPortrait,
|
||||
kOrientationLandscape,
|
||||
};
|
||||
|
||||
AndroidOptionsWidget::AndroidOptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain) :
|
||||
OptionsContainerWidget(boss, name, "AndroidOptionsDialog", domain), _enabled(true) {
|
||||
|
||||
const bool inAppDomain = domain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain);;
|
||||
|
||||
_onscreenCheckbox = new GUI::CheckboxWidget(widgetsBoss(), "AndroidOptionsDialog.OnScreenControl", _("Show On-screen control"));
|
||||
_ignoreGameSafeAreaCheckbox = new GUI::CheckboxWidget(widgetsBoss(), "AndroidOptionsDialog.IgnoreGameSafeArea", _("Ignore safe areas in-game"));
|
||||
_preferredTouchModeDesc = new GUI::StaticTextWidget(widgetsBoss(), "AndroidOptionsDialog.PreferredTouchModeText", _("Choose the preferred touch mode:"));
|
||||
if (inAppDomain) {
|
||||
_preferredTMMenusDesc = new GUI::StaticTextWidget(widgetsBoss(), "AndroidOptionsDialog.TMMenusText", _("In menus"));
|
||||
_preferredTMMenusPopUp = new GUI::PopUpWidget(widgetsBoss(), "AndroidOptionsDialog.TMMenus");
|
||||
_preferredTMMenusPopUp->appendEntry(_("Touchpad emulation"), kTouchModeTouchpad);
|
||||
_preferredTMMenusPopUp->appendEntry(_("Direct mouse"), kTouchModeMouse); // TODO: Find a better name
|
||||
_preferredTMMenusPopUp->appendEntry(_("Gamepad emulation"), kTouchModeGamepad);
|
||||
} else {
|
||||
_preferredTMMenusDesc = nullptr;
|
||||
_preferredTMMenusPopUp = nullptr;
|
||||
}
|
||||
|
||||
_preferredTM2DGamesDesc = new GUI::StaticTextWidget(widgetsBoss(), "AndroidOptionsDialog.TM2DGamesText", _("In 2D games"));
|
||||
_preferredTM2DGamesPopUp = new GUI::PopUpWidget(widgetsBoss(), "AndroidOptionsDialog.TM2DGames");
|
||||
_preferredTM3DGamesDesc = new GUI::StaticTextWidget(widgetsBoss(), "AndroidOptionsDialog.TM3DGamesText", _("In 3D games"));
|
||||
_preferredTM3DGamesPopUp = new GUI::PopUpWidget(widgetsBoss(), "AndroidOptionsDialog.TM3DGames");
|
||||
|
||||
if (!inAppDomain) {
|
||||
_preferredTM2DGamesPopUp->appendEntry(_("<default>"), kTouchModeDefault);
|
||||
_preferredTM3DGamesPopUp->appendEntry(_("<default>"), kTouchModeDefault);
|
||||
}
|
||||
|
||||
_preferredTM2DGamesPopUp->appendEntry(_("Touchpad emulation"), kTouchModeTouchpad);
|
||||
_preferredTM3DGamesPopUp->appendEntry(_("Touchpad emulation"), kTouchModeTouchpad);
|
||||
|
||||
_preferredTM2DGamesPopUp->appendEntry(_("Direct mouse"), kTouchModeMouse); // TODO: Find a better name
|
||||
_preferredTM3DGamesPopUp->appendEntry(_("Direct mouse"), kTouchModeMouse);
|
||||
|
||||
_preferredTM2DGamesPopUp->appendEntry(_("Gamepad emulation"), kTouchModeGamepad);
|
||||
_preferredTM3DGamesPopUp->appendEntry(_("Gamepad emulation"), kTouchModeGamepad);
|
||||
|
||||
_orientationDesc = new GUI::StaticTextWidget(widgetsBoss(), "AndroidOptionsDialog.OrientationText", _("Select the orientation:"));
|
||||
if (inAppDomain) {
|
||||
_orientationMenusDesc = new GUI::StaticTextWidget(widgetsBoss(), "AndroidOptionsDialog.OMenusText", _("In menus"));
|
||||
_orientationMenusPopUp = new GUI::PopUpWidget(widgetsBoss(), "AndroidOptionsDialog.OMenus");
|
||||
_orientationMenusPopUp->appendEntry(_("Automatic"), kOrientationAuto);
|
||||
_orientationMenusPopUp->appendEntry(_("Portrait"), kOrientationPortrait);
|
||||
_orientationMenusPopUp->appendEntry(_("Landscape"), kOrientationLandscape);
|
||||
} else {
|
||||
_orientationMenusDesc = nullptr;
|
||||
_orientationMenusPopUp = nullptr;
|
||||
}
|
||||
|
||||
_orientationGamesDesc = new GUI::StaticTextWidget(widgetsBoss(), "AndroidOptionsDialog.OGamesText", _("In games"));
|
||||
_orientationGamesPopUp = new GUI::PopUpWidget(widgetsBoss(), "AndroidOptionsDialog.OGames");
|
||||
|
||||
if (!inAppDomain) {
|
||||
_orientationGamesPopUp->appendEntry(_("<default>"), kOrientationDefault);
|
||||
}
|
||||
|
||||
_orientationGamesPopUp->appendEntry(_("Automatic"), kOrientationAuto);
|
||||
_orientationGamesPopUp->appendEntry(_("Portrait"), kOrientationPortrait);
|
||||
_orientationGamesPopUp->appendEntry(_("Landscape"), kOrientationLandscape);
|
||||
|
||||
if (inAppDomain) {
|
||||
// Only show these buttons in Options (via Options... in the launcher), and not at game domain level (via Edit Game...)
|
||||
(new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ExportDataButton", _("Export backup"), _("Export a backup of the configuration and save files"), kExportBackupCmd))->setTarget(this);
|
||||
(new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ImportDataButton", _("Import backup"), _("Import a previously exported backup file"), kImportBackupCmd))->setTarget(this);
|
||||
if (AndroidFilesystemFactory::instance().hasSAF()) {
|
||||
// I18N: This button opens a list of all folders added for Android Storage Attached Framework
|
||||
(new GUI::ButtonWidget(widgetsBoss(), "AndroidOptionsDialog.ForgetSAFButton", _("Remove folder authorizations..."), Common::U32String(), kRemoveCmd))->setTarget(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AndroidOptionsWidget::~AndroidOptionsWidget() {
|
||||
}
|
||||
|
||||
void AndroidOptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const {
|
||||
const bool inAppDomain = _domain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain);;
|
||||
|
||||
layouts.addDialog(layoutName, overlayedLayout)
|
||||
.addLayout(GUI::ThemeLayout::kLayoutVertical)
|
||||
.addPadding(0, 0, 0, 0)
|
||||
.addWidget("OnScreenControl", "Checkbox")
|
||||
.addWidget("IgnoreGameSafeArea", "Checkbox")
|
||||
.addWidget("PreferredTouchModeText", "", -1, layouts.getVar("Globals.Line.Height"));
|
||||
|
||||
if (inAppDomain) {
|
||||
layouts.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
|
||||
.addPadding(16, 16, 0, 0)
|
||||
.addWidget("TMMenusText", "OptionsLabel")
|
||||
.addWidget("TMMenus", "PopUp")
|
||||
.closeLayout();
|
||||
}
|
||||
layouts.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
|
||||
.addPadding(16, 16, 0, 0)
|
||||
.addWidget("TM2DGamesText", "OptionsLabel")
|
||||
.addWidget("TM2DGames", "PopUp")
|
||||
.closeLayout()
|
||||
.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
|
||||
.addPadding(16, 16, 0, 0)
|
||||
.addWidget("TM3DGamesText", "OptionsLabel")
|
||||
.addWidget("TM3DGames", "PopUp")
|
||||
.closeLayout();
|
||||
|
||||
layouts.addWidget("OrientationText", "", -1, layouts.getVar("Globals.Line.Height"));
|
||||
if (inAppDomain) {
|
||||
layouts.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
|
||||
.addPadding(16, 16, 0, 0)
|
||||
.addWidget("OMenusText", "OptionsLabel")
|
||||
.addWidget("OMenus", "PopUp")
|
||||
.closeLayout();
|
||||
}
|
||||
layouts.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
|
||||
.addPadding(16, 16, 0, 0)
|
||||
.addWidget("OGamesText", "OptionsLabel")
|
||||
.addWidget("OGames", "PopUp")
|
||||
.closeLayout();
|
||||
|
||||
if (inAppDomain) {
|
||||
layouts.addWidget("ExportDataButton", "WideButton");
|
||||
layouts.addWidget("ImportDataButton", "WideButton");
|
||||
if (AndroidFilesystemFactory::instance().hasSAF()) {
|
||||
layouts.addWidget("ForgetSAFButton", "WideButton");
|
||||
}
|
||||
}
|
||||
layouts.closeLayout()
|
||||
.closeDialog();
|
||||
}
|
||||
|
||||
void AndroidOptionsWidget::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
|
||||
switch (cmd) {
|
||||
case kRemoveCmd: {
|
||||
if (!AndroidFilesystemFactory::instance().hasSAF()) {
|
||||
break;
|
||||
}
|
||||
SAFRemoveDialog removeDlg;
|
||||
removeDlg.runModal();
|
||||
break;
|
||||
}
|
||||
case kExportBackupCmd:
|
||||
{
|
||||
Common::U32String prompt(_("Select backup destination"));
|
||||
int ret = JNI::exportBackup(prompt);
|
||||
if (ret == 1) {
|
||||
// BackupManager.ERROR_CANCELLED
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret == 0 && AndroidFilesystemFactory::instance().hasSAF()) {
|
||||
prompt = _("The backup has been saved successfully.");
|
||||
} else if (ret == 0) {
|
||||
prompt = _("The backup has been saved successfully to the Downloads folder.");
|
||||
} else if (ret == -2) {
|
||||
prompt = _("The game saves couldn't be backed up");
|
||||
} else {
|
||||
prompt = _("An error occured while saving the backup.");
|
||||
}
|
||||
g_system->displayMessageOnOSD(prompt);
|
||||
break;
|
||||
}
|
||||
case kImportBackupCmd:
|
||||
{
|
||||
GUI::MessageDialog alert(_("Restoring a backup will erase the current configuration and overwrite existing saves. Do you want to proceed?"), _("Proceed"), _("Cancel"));
|
||||
if (alert.runModal() != GUI::kMessageOK) {
|
||||
break;
|
||||
}
|
||||
|
||||
Common::U32String prompt(_("Select backup file"));
|
||||
Common::Path path;
|
||||
if (!AndroidFilesystemFactory::instance().hasSAF()) {
|
||||
GUI::BrowserDialog browser(prompt, false);
|
||||
if (browser.runModal() <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
path = browser.getResult().getPath();
|
||||
}
|
||||
int ret = JNI::importBackup(prompt, path.toString());
|
||||
if (ret == 1) {
|
||||
// BackupManager.ERROR_CANCELLED
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret == 0) {
|
||||
prompt = _("The backup has been restored successfully.");
|
||||
} else if (ret == -2) {
|
||||
prompt = _("The game saves couldn't be backed up");
|
||||
} else {
|
||||
prompt = _("An error occured while restoring the backup.");
|
||||
}
|
||||
g_system->displayMessageOnOSD(prompt);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
GUI::OptionsContainerWidget::handleCommand(sender, cmd, data);
|
||||
}
|
||||
}
|
||||
|
||||
uint32 AndroidOptionsWidget::loadTouchMode(const Common::String &setting, bool acceptDefault, uint32 defaultValue) {
|
||||
if (!acceptDefault || ConfMan.hasKey(setting, _domain)) {
|
||||
Common::String preferredTouchMode = ConfMan.get(setting, _domain);
|
||||
if (preferredTouchMode == "mouse") {
|
||||
return kTouchModeMouse;
|
||||
} else if (preferredTouchMode == "gamepad") {
|
||||
return kTouchModeGamepad;
|
||||
} else if (preferredTouchMode == "touchpad") {
|
||||
return kTouchModeTouchpad;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
} else {
|
||||
return kTouchModeDefault;
|
||||
}
|
||||
}
|
||||
|
||||
uint32 AndroidOptionsWidget::loadOrientation(const Common::String &setting, bool acceptDefault, uint32 defaultValue) {
|
||||
if (!acceptDefault || ConfMan.hasKey(setting, _domain)) {
|
||||
Common::String orientation = ConfMan.get(setting, _domain);
|
||||
if (orientation == "auto") {
|
||||
return kOrientationAuto;
|
||||
} else if (orientation == "portrait") {
|
||||
return kOrientationPortrait;
|
||||
} else if (orientation == "landscape") {
|
||||
return kOrientationLandscape;
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
} else {
|
||||
return kOrientationDefault;
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidOptionsWidget::load() {
|
||||
const bool inAppDomain = _domain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain);
|
||||
|
||||
_onscreenCheckbox->setState(ConfMan.getBool("onscreen_control", _domain));
|
||||
_ignoreGameSafeAreaCheckbox->setState(ConfMan.getBool("ignore_game_safe_area", _domain));
|
||||
|
||||
// When in application domain, we don't have default entry so we must have a value
|
||||
if (inAppDomain) {
|
||||
_preferredTMMenusPopUp->setSelectedTag(loadTouchMode("touch_mode_menus", !inAppDomain, kTouchModeMouse));
|
||||
}
|
||||
_preferredTM2DGamesPopUp->setSelectedTag(loadTouchMode("touch_mode_2d_games", !inAppDomain, kTouchModeTouchpad));
|
||||
_preferredTM3DGamesPopUp->setSelectedTag(loadTouchMode("touch_mode_3d_games", !inAppDomain, kTouchModeGamepad));
|
||||
|
||||
if (inAppDomain) {
|
||||
_orientationMenusPopUp->setSelectedTag(loadOrientation("orientation_menus", !inAppDomain, kOrientationAuto));
|
||||
}
|
||||
_orientationGamesPopUp->setSelectedTag(loadOrientation("orientation_games", !inAppDomain, kOrientationAuto));
|
||||
}
|
||||
|
||||
void AndroidOptionsWidget::saveTouchMode(const Common::String &setting, uint32 touchMode) {
|
||||
switch (touchMode) {
|
||||
case kTouchModeTouchpad:
|
||||
ConfMan.set(setting, "touchpad", _domain);
|
||||
break;
|
||||
case kTouchModeMouse:
|
||||
ConfMan.set(setting, "mouse", _domain);
|
||||
break;
|
||||
case kTouchModeGamepad:
|
||||
ConfMan.set(setting, "gamepad", _domain);
|
||||
break;
|
||||
default:
|
||||
// default
|
||||
ConfMan.removeKey(setting, _domain);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AndroidOptionsWidget::saveOrientation(const Common::String &setting, uint32 orientation) {
|
||||
switch (orientation) {
|
||||
case kOrientationAuto:
|
||||
ConfMan.set(setting, "auto", _domain);
|
||||
break;
|
||||
case kOrientationPortrait:
|
||||
ConfMan.set(setting, "portrait", _domain);
|
||||
break;
|
||||
case kOrientationLandscape:
|
||||
ConfMan.set(setting, "landscape", _domain);
|
||||
break;
|
||||
default:
|
||||
// default
|
||||
ConfMan.removeKey(setting, _domain);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool AndroidOptionsWidget::save() {
|
||||
const bool inAppDomain = _domain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain);
|
||||
|
||||
if (_enabled) {
|
||||
ConfMan.setBool("onscreen_control", _onscreenCheckbox->getState(), _domain);
|
||||
ConfMan.setBool("ignore_game_safe_area", _ignoreGameSafeAreaCheckbox->getState(), _domain);
|
||||
|
||||
if (inAppDomain) {
|
||||
saveTouchMode("touch_mode_menus", _preferredTMMenusPopUp->getSelectedTag());
|
||||
}
|
||||
saveTouchMode("touch_mode_2d_games", _preferredTM2DGamesPopUp->getSelectedTag());
|
||||
saveTouchMode("touch_mode_3d_games", _preferredTM3DGamesPopUp->getSelectedTag());
|
||||
|
||||
if (inAppDomain) {
|
||||
saveOrientation("orientation_menus", _orientationMenusPopUp->getSelectedTag());
|
||||
}
|
||||
saveOrientation("orientation_games", _orientationGamesPopUp->getSelectedTag());
|
||||
} else {
|
||||
ConfMan.removeKey("onscreen_control", _domain);
|
||||
ConfMan.removeKey("ignore_game_safe_area", _domain);
|
||||
|
||||
if (inAppDomain) {
|
||||
ConfMan.removeKey("touch_mode_menus", _domain);
|
||||
}
|
||||
ConfMan.removeKey("touch_mode_2d_games", _domain);
|
||||
ConfMan.removeKey("touch_mode_3d_games", _domain);
|
||||
|
||||
if (inAppDomain) {
|
||||
ConfMan.removeKey("orientation_menus", _domain);
|
||||
}
|
||||
ConfMan.removeKey("orientation_games", _domain);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidOptionsWidget::hasKeys() {
|
||||
return ConfMan.hasKey("onscreen_control", _domain) ||
|
||||
ConfMan.hasKey("ignore_game_safe_area", _domain) ||
|
||||
(_domain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain) && ConfMan.hasKey("touch_mode_menus", _domain)) ||
|
||||
ConfMan.hasKey("touch_mode_2d_games", _domain) ||
|
||||
ConfMan.hasKey("touch_mode_3d_games", _domain) ||
|
||||
(_domain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain) && ConfMan.hasKey("orientation_menus", _domain)) ||
|
||||
ConfMan.hasKey("orientation_games", _domain);
|
||||
}
|
||||
|
||||
void AndroidOptionsWidget::setEnabled(bool e) {
|
||||
const bool inAppDomain = _domain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain);
|
||||
|
||||
_enabled = e;
|
||||
|
||||
_onscreenCheckbox->setEnabled(e);
|
||||
_ignoreGameSafeAreaCheckbox->setEnabled(e);
|
||||
|
||||
if (inAppDomain) {
|
||||
_preferredTMMenusDesc->setEnabled(e);
|
||||
_preferredTMMenusPopUp->setEnabled(e);
|
||||
}
|
||||
_preferredTM2DGamesDesc->setEnabled(e);
|
||||
_preferredTM2DGamesPopUp->setEnabled(e);
|
||||
_preferredTM3DGamesDesc->setEnabled(e);
|
||||
_preferredTM3DGamesPopUp->setEnabled(e);
|
||||
if (inAppDomain) {
|
||||
_orientationMenusDesc->setEnabled(e);
|
||||
_orientationMenusPopUp->setEnabled(e);
|
||||
}
|
||||
_orientationGamesDesc->setEnabled(e);
|
||||
_orientationGamesPopUp->setEnabled(e);
|
||||
}
|
||||
|
||||
|
||||
GUI::OptionsContainerWidget *OSystem_Android::buildBackendOptionsWidget(GUI::GuiObject *boss, const Common::String &name, const Common::String &target) const {
|
||||
return new AndroidOptionsWidget(boss, name, target);
|
||||
}
|
||||
|
||||
void OSystem_Android::registerDefaultSettings(const Common::String &target) const {
|
||||
ConfMan.registerDefault("onscreen_control", true);
|
||||
ConfMan.registerDefault("ignore_game_safe_area", false);
|
||||
ConfMan.registerDefault("touch_mode_menus", "mouse");
|
||||
ConfMan.registerDefault("touch_mode_2d_games", "touchpad");
|
||||
ConfMan.registerDefault("touch_mode_3d_games", "gamepad");
|
||||
ConfMan.registerDefault("orientation_menus", "auto");
|
||||
ConfMan.registerDefault("orientation_games", "auto");
|
||||
}
|
||||
|
||||
void OSystem_Android::applyTouchSettings(bool _3dMode, bool overlayShown) {
|
||||
Common::String setting;
|
||||
int defaultMode;
|
||||
|
||||
if (overlayShown) {
|
||||
setting = "touch_mode_menus";
|
||||
defaultMode = TOUCH_MODE_MOUSE;
|
||||
} else if (_3dMode) {
|
||||
setting = "touch_mode_3d_games";
|
||||
defaultMode = TOUCH_MODE_GAMEPAD;
|
||||
} else {
|
||||
setting = "touch_mode_2d_games";
|
||||
defaultMode = TOUCH_MODE_TOUCHPAD;
|
||||
}
|
||||
|
||||
Common::String preferredTouchMode = ConfMan.get(setting);
|
||||
if (preferredTouchMode == "mouse") {
|
||||
JNI::setTouchMode(TOUCH_MODE_MOUSE);
|
||||
} else if (preferredTouchMode == "gamepad") {
|
||||
JNI::setTouchMode(TOUCH_MODE_GAMEPAD);
|
||||
} else if (preferredTouchMode == "touchpad") {
|
||||
JNI::setTouchMode(TOUCH_MODE_TOUCHPAD);
|
||||
} else {
|
||||
JNI::setTouchMode(defaultMode);
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Android::applyOrientationSettings() {
|
||||
const Common::String activeDomain = ConfMan.getActiveDomainName();
|
||||
const bool inAppDomain = activeDomain.empty() ||
|
||||
activeDomain.equalsIgnoreCase(Common::ConfigManager::kApplicationDomain);
|
||||
|
||||
Common::String setting;
|
||||
|
||||
if (inAppDomain) {
|
||||
setting = "orientation_menus";
|
||||
} else {
|
||||
setting = "orientation_games";
|
||||
}
|
||||
|
||||
Common::String orientation = ConfMan.get(setting);
|
||||
if (orientation == "portrait") {
|
||||
JNI::setOrientation(SCREEN_ORIENTATION_PORTRAIT);
|
||||
} else if (orientation == "landscape") {
|
||||
JNI::setOrientation(SCREEN_ORIENTATION_LANDSCAPE);
|
||||
// auto and everything else
|
||||
} else {
|
||||
JNI::setOrientation(SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Android::applyBackendSettings() {
|
||||
updateOnScreenControls();
|
||||
if (_graphicsManager) {
|
||||
dynamic_cast<AndroidGraphicsManager *>(_graphicsManager)->setIgnoreGameSafeArea(ConfMan.getBool("ignore_game_safe_area"));
|
||||
}
|
||||
}
|
||||
|
||||
SAFRemoveDialog::SAFRemoveDialog() : GUI::Dialog("SAFBrowser") {
|
||||
|
||||
// Add file list
|
||||
_safList = new GUI::ListWidget(this, "SAFBrowser.List");
|
||||
_safList->setNumberingMode(GUI::kListNumberingOff);
|
||||
_safList->setEditable(false);
|
||||
|
||||
_backgroundType = GUI::ThemeEngine::kDialogBackgroundPlain;
|
||||
|
||||
// Buttons
|
||||
new GUI::ButtonWidget(this, "SAFBrowser.Close", _("Close"), Common::U32String(), GUI::kCloseCmd);
|
||||
new GUI::ButtonWidget(this, "SAFBrowser.Remove", _("Remove"), Common::U32String(), kRemoveCmd);
|
||||
}
|
||||
|
||||
SAFRemoveDialog::~SAFRemoveDialog() {
|
||||
clearListing();
|
||||
}
|
||||
|
||||
void SAFRemoveDialog::open() {
|
||||
// Call super implementation
|
||||
Dialog::open();
|
||||
|
||||
updateListing();
|
||||
}
|
||||
|
||||
void SAFRemoveDialog::reflowLayout() {
|
||||
GUI::ThemeEval &layouts = *g_gui.xmlEval();
|
||||
layouts.addDialog(_name, "GlobalOptions", -1, -1, 16)
|
||||
.addLayout(GUI::ThemeLayout::kLayoutVertical)
|
||||
.addPadding(16, 16, 16, 16)
|
||||
.addWidget("List", "")
|
||||
.addLayout(GUI::ThemeLayout::kLayoutVertical)
|
||||
.addPadding(0, 0, 16, 0)
|
||||
.addLayout(GUI::ThemeLayout::kLayoutHorizontal)
|
||||
.addPadding(0, 0, 0, 0)
|
||||
.addWidget("Remove", "Button")
|
||||
.addSpace(-1)
|
||||
.addWidget("Close", "Button")
|
||||
.closeLayout()
|
||||
.closeLayout()
|
||||
.closeLayout()
|
||||
.closeDialog();
|
||||
|
||||
layouts.setVar("Dialog.SAFBrowser.Shading", 1);
|
||||
|
||||
Dialog::reflowLayout();
|
||||
}
|
||||
|
||||
void SAFRemoveDialog::handleCommand(GUI::CommandSender *sender, uint32 cmd, uint32 data) {
|
||||
switch (cmd) {
|
||||
case kRemoveCmd:
|
||||
{
|
||||
int id = _safList->getSelected();
|
||||
if (id == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
AndroidSAFFilesystemNode *node = reinterpret_cast<AndroidSAFFilesystemNode *>(_safTrees[id]);
|
||||
node->removeTree();
|
||||
|
||||
updateListing();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
GUI::Dialog::handleCommand(sender, cmd, data);
|
||||
}
|
||||
}
|
||||
|
||||
void SAFRemoveDialog::clearListing() {
|
||||
for (AbstractFSList::iterator it = _safTrees.begin(); it != _safTrees.end(); it++) {
|
||||
delete *it;
|
||||
}
|
||||
_safTrees.clear();
|
||||
}
|
||||
|
||||
void SAFRemoveDialog::updateListing() {
|
||||
int oldSel = _safList->getSelected();
|
||||
|
||||
clearListing();
|
||||
|
||||
AndroidFilesystemFactory::instance().getSAFTrees(_safTrees, false);
|
||||
|
||||
Common::U32StringArray list;
|
||||
list.reserve(_safTrees.size());
|
||||
for (AbstractFSList::iterator it = _safTrees.begin(); it != _safTrees.end(); it++) {
|
||||
list.push_back((*it)->getDisplayName());
|
||||
}
|
||||
|
||||
_safList->setList(list);
|
||||
if (oldSel >= 0 && (size_t)oldSel < list.size()) {
|
||||
_safList->setSelected(oldSel);
|
||||
} else {
|
||||
_safList->scrollTo(0);
|
||||
}
|
||||
|
||||
// Finally, redraw
|
||||
g_gui.scheduleTopDialogRedraw();
|
||||
}
|
||||
519
backends/platform/android/org/scummvm/scummvm/BackupManager.java
Normal file
519
backends/platform/android/org/scummvm/scummvm/BackupManager.java
Normal file
@@ -0,0 +1,519 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.scummvm.scummvm.zip.ZipFile;
|
||||
|
||||
public class BackupManager {
|
||||
public static final int ERROR_CANCELLED = 1;
|
||||
public static final int ERROR_NO_ERROR = 0;
|
||||
public static final int ERROR_INVALID_BACKUP = -1;
|
||||
public static final int ERROR_INVALID_SAVES = -2;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static int exportBackup(Context context, Uri output) {
|
||||
try (ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(output, "wt")) {
|
||||
if (pfd == null) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
return exportBackup(context, new FileOutputStream(pfd.getFileDescriptor()));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
public static int exportBackup(Context context, File output) {
|
||||
try {
|
||||
return exportBackup(context, new FileOutputStream(output, false));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public static int importBackup(ScummVMActivity context, Uri input) {
|
||||
try (ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(input, "r")) {
|
||||
if (pfd == null) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
return importBackup(context, new FileInputStream(pfd.getFileDescriptor()));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
public static int importBackup(ScummVMActivity context, File input) {
|
||||
try {
|
||||
return importBackup(context, new FileInputStream(input));
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
private static int exportBackup(Context context, FileOutputStream output) {
|
||||
File configuration = new File(context.getFilesDir(), "scummvm.ini");
|
||||
|
||||
Map<String, Map<String, String>> parsedIniMap;
|
||||
try (FileReader reader = new FileReader(configuration)) {
|
||||
parsedIniMap = INIParser.parse(reader);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
parsedIniMap = null;
|
||||
} catch(IOException ignored) {
|
||||
parsedIniMap = null;
|
||||
}
|
||||
|
||||
if (parsedIniMap == null) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
ZipOutputStream zos = new ZipOutputStream(output);
|
||||
|
||||
try (FileInputStream stream = new FileInputStream(configuration)) {
|
||||
ZipEntry entry = new ZipEntry("scummvm.ini");
|
||||
entry.setSize(configuration.length());
|
||||
entry.setTime(configuration.lastModified());
|
||||
|
||||
zos.putNextEntry(entry);
|
||||
copyStream(zos, stream);
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
try {
|
||||
ZipEntry entry;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
entry = new ZipEntry("saf");
|
||||
zos.putNextEntry(entry);
|
||||
if (!exportTrees(context, zos)) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
entry = new ZipEntry("saves/");
|
||||
zos.putNextEntry(entry);
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
File savesPath = INIParser.getPath(parsedIniMap, "scummvm", "savepath",
|
||||
new File(context.getFilesDir(), "saves"));
|
||||
boolean ret = exportSaves(context, savesPath, zos, "saves/");
|
||||
if (!ret) {
|
||||
try {
|
||||
zos.close();
|
||||
} catch(IOException ignored) {
|
||||
}
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
HashSet<File> savesPaths = new HashSet<>();
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
int sectionId = -1;
|
||||
for (String section : parsedIniMap.keySet()) {
|
||||
sectionId++;
|
||||
if ("scummvm".equals(section)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
savesPath = INIParser.getPath(parsedIniMap, section, "savepath", null);
|
||||
if (savesPath == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (savesPaths.contains(savesPath)) {
|
||||
continue;
|
||||
}
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
String folderName = "saves-" + sectionId + "/";
|
||||
ZipEntry entry = new ZipEntry(folderName);
|
||||
try {
|
||||
zos.putNextEntry(entry);
|
||||
zos.closeEntry();
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
ret = exportSaves(context, savesPath, zos, folderName);
|
||||
if (!ret) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
zos.close();
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
return ret ? ERROR_NO_ERROR : ERROR_INVALID_SAVES;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
static private boolean exportTrees(Context context, ZipOutputStream zos) throws IOException {
|
||||
ObjectOutputStream oos = new ObjectOutputStream(zos);
|
||||
// Version 1
|
||||
oos.writeInt(1);
|
||||
|
||||
SAFFSTree[] trees = SAFFSTree.getTrees(context);
|
||||
|
||||
oos.writeInt(trees.length);
|
||||
for (SAFFSTree tree : trees) {
|
||||
oos.writeObject(tree.getTreeName());
|
||||
oos.writeObject(tree.getTreeId());
|
||||
oos.writeObject(tree.getTreeDocumentUri().toString());
|
||||
}
|
||||
|
||||
// Don't close as it would close the underlying ZipOutputStream
|
||||
oos.flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static private boolean exportSaves(Context context, File folder, ZipOutputStream zos, String folderName) {
|
||||
SAFFSTree.PathResult pr;
|
||||
try {
|
||||
pr = SAFFSTree.fullPathToNode(context, folder.getPath(), false);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This version check is only to make Android Studio linter happy
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// This is a standard filesystem path
|
||||
File[] children = folder.listFiles();
|
||||
if (children == null) {
|
||||
return true;
|
||||
}
|
||||
Arrays.sort(children);
|
||||
for (File f: children) {
|
||||
if ("timestamps".equals(f.getName())) {
|
||||
continue;
|
||||
}
|
||||
try (FileInputStream stream = new FileInputStream(f)) {
|
||||
ZipEntry entry = new ZipEntry(folderName + f.getName());
|
||||
entry.setSize(f.length());
|
||||
entry.setTime(f.lastModified());
|
||||
|
||||
zos.putNextEntry(entry);
|
||||
copyStream(zos, stream);
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
SAFFSTree.SAFFSNode[] children = pr.tree.getChildren(pr.node);
|
||||
if (children == null) {
|
||||
return false;
|
||||
}
|
||||
Arrays.sort(children);
|
||||
|
||||
for (SAFFSTree.SAFFSNode child : children) {
|
||||
if ((child._flags & SAFFSTree.SAFFSNode.DIRECTORY) != 0) {
|
||||
continue;
|
||||
}
|
||||
String component = child._path.substring(child._path.lastIndexOf('/') + 1);
|
||||
if ("timestamps".equals(component)) {
|
||||
continue;
|
||||
}
|
||||
try (ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "r")) {
|
||||
ZipEntry entry = new ZipEntry(folderName + component);
|
||||
|
||||
zos.putNextEntry(entry);
|
||||
copyStream(zos, new ParcelFileDescriptor.AutoCloseInputStream(pfd));
|
||||
zos.closeEntry();
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int importBackup(ScummVMActivity context, FileInputStream input) {
|
||||
ZipFile zf;
|
||||
try {
|
||||
zf = new ZipFile(input);
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
org.scummvm.scummvm.zip.ZipEntry ze = zf.getEntry("scummvm.ini");
|
||||
if (ze == null) {
|
||||
// No configuration file, not a backup
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
// Avoid using tmp suffix as it's used by atomic file support
|
||||
File configurationTmp = new File(context.getFilesDir(), "scummvm.ini.tmp2");
|
||||
|
||||
try (FileOutputStream os = new FileOutputStream(configurationTmp);
|
||||
InputStream is = zf.getInputStream(ze)) {
|
||||
copyStream(os, is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
// Load the new configuration to know where to put saves
|
||||
Map<String, Map<String, String>> parsedIniMap;
|
||||
try (FileReader reader = new FileReader(configurationTmp)) {
|
||||
parsedIniMap = INIParser.parse(reader);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
parsedIniMap = null;
|
||||
} catch(IOException ignored) {
|
||||
parsedIniMap = null;
|
||||
}
|
||||
|
||||
if (parsedIniMap == null) {
|
||||
configurationTmp.delete();
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Restore the missing SAF trees
|
||||
ze = zf.getEntry("saf");
|
||||
if (ze == null) {
|
||||
// No configuration file, not a backup
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
try (InputStream is = zf.getInputStream(ze)) {
|
||||
if (importTrees(context, is) == ERROR_INVALID_BACKUP) {
|
||||
// Try to continue except obvious error
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
}
|
||||
|
||||
// Move the configuration back now that we know it's parsable and that SAF is set up
|
||||
Log.i(ScummVM.LOG_TAG, "Writing new ScummVM configuration");
|
||||
File configuration = new File(context.getFilesDir(), "scummvm.ini");
|
||||
if (!configurationTmp.renameTo(configuration)) {
|
||||
try (FileOutputStream os = new FileOutputStream(configuration);
|
||||
FileInputStream is = new FileInputStream(configurationTmp)) {
|
||||
copyStream(os, is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
} catch(IOException ignored) {
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
configurationTmp.delete();
|
||||
}
|
||||
|
||||
File savesPath = INIParser.getPath(parsedIniMap, "scummvm", "savepath",
|
||||
new File(context.getFilesDir(), "saves"));
|
||||
|
||||
boolean ret = importSaves(context, savesPath, zf, "saves/");
|
||||
if (!ret) {
|
||||
try {
|
||||
zf.close();
|
||||
} catch(IOException ignored) {
|
||||
}
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
HashSet<File> savesPaths = new HashSet<>();
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
int sectionId = -1;
|
||||
for (String section : parsedIniMap.keySet()) {
|
||||
sectionId++;
|
||||
if ("scummvm".equals(section)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
savesPath = INIParser.getPath(parsedIniMap, section, "savepath", null);
|
||||
if (savesPath == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (savesPaths.contains(savesPath)) {
|
||||
continue;
|
||||
}
|
||||
savesPaths.add(savesPath);
|
||||
|
||||
String folderName = "saves-" + sectionId + "/";
|
||||
ret = importSaves(context, savesPath, zf, folderName);
|
||||
if (!ret) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
zf.close();
|
||||
} catch(IOException ignored) {
|
||||
}
|
||||
|
||||
return ret ? ERROR_NO_ERROR : ERROR_INVALID_SAVES;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
static private int importTrees(ScummVMActivity context, InputStream is) throws IOException, ClassNotFoundException {
|
||||
boolean failed = false;
|
||||
|
||||
ObjectInputStream ois = new ObjectInputStream(is);
|
||||
// Version 1
|
||||
if (ois.readInt() != 1) {
|
||||
// Invalid version
|
||||
return ERROR_INVALID_BACKUP;
|
||||
}
|
||||
|
||||
for (int length = ois.readInt(); length > 0; length--) {
|
||||
String treeName = (String)ois.readObject();
|
||||
String treeId = (String)ois.readObject();
|
||||
String treeUri = (String)ois.readObject();
|
||||
|
||||
if (SAFFSTree.findTree(context, treeId) != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uri uri = context.selectWithNativeUI(true, true, Uri.parse(treeUri), treeName, null, null);
|
||||
if (uri == null) {
|
||||
failed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register the new selected tree
|
||||
SAFFSTree.newTree(context, uri);
|
||||
}
|
||||
|
||||
ois.close();
|
||||
|
||||
return failed ? ERROR_CANCELLED : ERROR_NO_ERROR;
|
||||
}
|
||||
|
||||
static private boolean importSaves(Context context, File folder, ZipFile zf, String folderName) {
|
||||
SAFFSTree.PathResult pr;
|
||||
try {
|
||||
pr = SAFFSTree.fullPathToNode(context, folder.getPath(), true);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This version check is only to make Android Studio linter happy
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// This is a standard filesystem path
|
||||
if (!folder.isDirectory() && !folder.mkdirs()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Enumeration<? extends org.scummvm.scummvm.zip.ZipEntry> entries = zf.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
org.scummvm.scummvm.zip.ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
if (!name.startsWith(folderName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the base name (this avoids directory traversal)
|
||||
name = name.substring(name.lastIndexOf("/") + 1);
|
||||
if (name.isEmpty() || "timestamps".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File f = new File(folder, name);
|
||||
try (InputStream is = zf.getInputStream(entry);
|
||||
FileOutputStream os = new FileOutputStream(f)) {
|
||||
copyStream(os, is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
Enumeration<? extends org.scummvm.scummvm.zip.ZipEntry> entries = zf.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
org.scummvm.scummvm.zip.ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
if (!name.startsWith(folderName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the base name (this avoids directory traversal)
|
||||
name = name.substring(name.lastIndexOf("/") + 1);
|
||||
if (name.isEmpty() || "timestamps".equals(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SAFFSTree.SAFFSNode child = pr.tree.getChild(pr.node, name);
|
||||
if (child == null) {
|
||||
child = pr.tree.createFile(pr.node, name);
|
||||
}
|
||||
if (child == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (InputStream is = zf.getInputStream(entry);
|
||||
ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "wt")) {
|
||||
copyStream(new FileOutputStream(pfd.getFileDescriptor()), is);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
return false;
|
||||
} catch(IOException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void copyStream(OutputStream out, InputStream in) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
int sz;
|
||||
while((sz = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, sz);
|
||||
}
|
||||
}
|
||||
}
|
||||
566
backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
Normal file
566
backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
Normal file
@@ -0,0 +1,566 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
class CompatHelpers {
|
||||
static class HideSystemStatusBar {
|
||||
|
||||
public static void hide(final Window window) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
HideSystemStatusBarR.hide(window);
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
HideSystemStatusBarKitKat.hide(window);
|
||||
} else {
|
||||
HideSystemStatusBarJellyBean.hide(window);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.JELLY_BEAN)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class HideSystemStatusBarJellyBean {
|
||||
public static void hide(final Window window) {
|
||||
View view = window.getDecorView();
|
||||
view.setSystemUiVisibility(
|
||||
view.getSystemUiVisibility() |
|
||||
View.SYSTEM_UI_FLAG_LOW_PROFILE);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.KITKAT)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class HideSystemStatusBarKitKat {
|
||||
public static void hide(final Window window) {
|
||||
View view = window.getDecorView();
|
||||
view.setSystemUiVisibility(
|
||||
(view.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_IMMERSIVE) |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class HideSystemStatusBarR {
|
||||
public static void hide(final Window window) {
|
||||
WindowInsetsController insetsController = window.getInsetsController();
|
||||
insetsController.hide(WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars());
|
||||
insetsController.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class SystemInsets {
|
||||
public interface SystemInsetsListener {
|
||||
void systemInsetsUpdated(int[] gestureInsets, int[] systemInsets, int[] cutoutInsets);
|
||||
}
|
||||
|
||||
public static void registerSystemInsetsListener(View v, SystemInsetsListener l) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerR(l));
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerQ(l));
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerP(l));
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
v.setOnApplyWindowInsetsListener(new OnApplyWindowInsetsListenerLollipop(l));
|
||||
} else {
|
||||
// Not available
|
||||
int[] gestureInsets = new int[] { 0, 0, 0, 0 };
|
||||
int[] systemInsets = new int[] { 0, 0, 0, 0 };
|
||||
int[] cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class OnApplyWindowInsetsListenerLollipop implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerLollipop(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
// No system gestures inset before Android Q
|
||||
int[] gestureInsets = new int[] {
|
||||
insets.getStableInsetLeft(),
|
||||
insets.getStableInsetTop(),
|
||||
insets.getStableInsetRight(),
|
||||
insets.getStableInsetBottom()
|
||||
};
|
||||
int[] systemInsets = new int[] {
|
||||
insets.getSystemWindowInsetLeft(),
|
||||
insets.getSystemWindowInsetTop(),
|
||||
insets.getSystemWindowInsetRight(),
|
||||
insets.getSystemWindowInsetBottom()
|
||||
};
|
||||
// No cutouts before Android P
|
||||
int[] cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.P)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class OnApplyWindowInsetsListenerP implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerP(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
// No system gestures inset before Android Q
|
||||
int[] gestureInsets = new int[] {
|
||||
insets.getStableInsetLeft(),
|
||||
insets.getStableInsetTop(),
|
||||
insets.getStableInsetRight(),
|
||||
insets.getStableInsetBottom()
|
||||
};
|
||||
int[] systemInsets = new int[] {
|
||||
insets.getSystemWindowInsetLeft(),
|
||||
insets.getSystemWindowInsetTop(),
|
||||
insets.getSystemWindowInsetRight(),
|
||||
insets.getSystemWindowInsetBottom()
|
||||
};
|
||||
int[] cutoutInsets;
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout == null) {
|
||||
cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
} else {
|
||||
cutoutInsets = new int[] {
|
||||
cutout.getSafeInsetLeft(),
|
||||
cutout.getSafeInsetTop(),
|
||||
cutout.getSafeInsetRight(),
|
||||
cutout.getSafeInsetBottom()
|
||||
};
|
||||
}
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.Q)
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class OnApplyWindowInsetsListenerQ implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerQ(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
Insets insetsStruct = insets.getSystemGestureInsets();
|
||||
int[] gestureInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
insetsStruct = insets.getSystemWindowInsets();
|
||||
int[] systemInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
int[] cutoutInsets;
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout == null) {
|
||||
cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
} else {
|
||||
cutoutInsets = new int[] {
|
||||
cutout.getSafeInsetLeft(),
|
||||
cutout.getSafeInsetTop(),
|
||||
cutout.getSafeInsetRight(),
|
||||
cutout.getSafeInsetBottom()
|
||||
};
|
||||
}
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class OnApplyWindowInsetsListenerR implements View.OnApplyWindowInsetsListener {
|
||||
final private SystemInsetsListener l;
|
||||
|
||||
public OnApplyWindowInsetsListenerR(SystemInsetsListener l) {
|
||||
this.l = l;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
Insets insetsStruct = insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemGestures());
|
||||
int[] gestureInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
insetsStruct = insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars());
|
||||
int[] systemInsets = new int[] {
|
||||
insetsStruct.left,
|
||||
insetsStruct.top,
|
||||
insetsStruct.right,
|
||||
insetsStruct.bottom,
|
||||
};
|
||||
int[] cutoutInsets;
|
||||
DisplayCutout cutout = insets.getDisplayCutout();
|
||||
if (cutout == null) {
|
||||
cutoutInsets = new int[] { 0, 0, 0, 0 };
|
||||
} else {
|
||||
cutoutInsets = new int[] {
|
||||
cutout.getSafeInsetLeft(),
|
||||
cutout.getSafeInsetTop(),
|
||||
cutout.getSafeInsetRight(),
|
||||
cutout.getSafeInsetBottom()
|
||||
};
|
||||
}
|
||||
l.systemInsetsUpdated(gestureInsets, systemInsets, cutoutInsets);
|
||||
return v.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class AudioTrackCompat {
|
||||
public static class AudioTrackCompatReturn {
|
||||
public AudioTrack audioTrack;
|
||||
public int bufferSize;
|
||||
}
|
||||
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
return AudioTrackCompatM.make(sample_rate, buffer_size);
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
return AudioTrackCompatLollipop.make(sample_rate, buffer_size);
|
||||
} else {
|
||||
return AudioTrackCompatOld.make(sample_rate, buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Support for Android KitKat or lower
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class AudioTrackCompatOld {
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
|
||||
ret.audioTrack = new AudioTrack(
|
||||
AudioManager.STREAM_MUSIC,
|
||||
sample_rate,
|
||||
AudioFormat.CHANNEL_OUT_STEREO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
buffer_size,
|
||||
AudioTrack.MODE_STREAM);
|
||||
ret.bufferSize = buffer_size;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
|
||||
private static class AudioTrackCompatLollipop {
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
|
||||
ret.audioTrack = new AudioTrack(
|
||||
new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build(),
|
||||
new AudioFormat.Builder()
|
||||
.setSampleRate(sample_rate)
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build(),
|
||||
buffer_size,
|
||||
AudioTrack.MODE_STREAM,
|
||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||
ret.bufferSize = buffer_size;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.M)
|
||||
private static class AudioTrackCompatM {
|
||||
public static AudioTrackCompatReturn make(int sample_rate, int buffer_size) {
|
||||
AudioTrackCompatReturn ret = new AudioTrackCompatReturn();
|
||||
ret.audioTrack = new AudioTrack(
|
||||
new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build(),
|
||||
new AudioFormat.Builder()
|
||||
.setSampleRate(sample_rate)
|
||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
||||
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO).build(),
|
||||
buffer_size,
|
||||
AudioTrack.MODE_STREAM,
|
||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
||||
// Keep track of the actual obtained audio buffer size, if supported.
|
||||
// We just requested 16 bit PCM stereo pcm so there are 4 bytes per frame.
|
||||
ret.bufferSize = ret.audioTrack.getBufferSizeInFrames() * 4;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class AccessibilityEventConstructor {
|
||||
public static AccessibilityEvent make(int eventType) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
return AccessibilityEventConstructorR.make(eventType);
|
||||
} else {
|
||||
return AccessibilityEventConstructorOld.make(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class AccessibilityEventConstructorOld {
|
||||
public static AccessibilityEvent make(int eventType) {
|
||||
return AccessibilityEvent.obtain(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class AccessibilityEventConstructorR {
|
||||
public static AccessibilityEvent make(int eventType) {
|
||||
return new AccessibilityEvent(eventType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ShortcutCreator {
|
||||
public static Intent createShortcutResultIntent(@NonNull Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
|
||||
return ShortcutCreatorN_MR1.createShortcutResultIntent(context, id, intent, label, icon, fallbackIconId);
|
||||
} else {
|
||||
return ShortcutCreatorOld.createShortcutResultIntent(context, id, intent, label, icon, fallbackIconId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void pushDynamicShortcut(@NonNull Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
ShortcutCreatorR.pushDynamicShortcut(context, id, intent, label, icon, fallbackIconId);
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutCreatorN_MR1.pushDynamicShortcut(context, id, intent, label, icon, fallbackIconId);
|
||||
}
|
||||
// No support for older versions
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class ShortcutCreatorOld {
|
||||
public static Intent createShortcutResultIntent(@NonNull Context context, String ignoredId, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
Intent result = new Intent();
|
||||
|
||||
if (icon == null) {
|
||||
icon = DrawableCompat.getDrawable(context, fallbackIconId);
|
||||
}
|
||||
Objects.requireNonNull(icon);
|
||||
Bitmap bmIcon = drawableToBitmap(icon);
|
||||
|
||||
addToIntent(result, intent, label, bmIcon);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void addToIntent(Intent result, @NonNull Intent intent, @NonNull String label, @NonNull Bitmap icon) {
|
||||
result.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent);
|
||||
result.putExtra(Intent.EXTRA_SHORTCUT_NAME, label);
|
||||
result.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.N_MR1)
|
||||
private static class ShortcutCreatorN_MR1 {
|
||||
public static ShortcutInfo createShortcutInfo(Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Icon icon) {
|
||||
ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, id);
|
||||
builder.setIntent(intent);
|
||||
builder.setShortLabel(label);
|
||||
builder.setIcon(icon);
|
||||
HashSet<String> categories = new HashSet<>(1);
|
||||
categories.add("actions.intent.START_GAME_EVENT");
|
||||
builder.setCategories(categories);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Intent createShortcutResultIntent(Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
Bitmap bm;
|
||||
Icon ic;
|
||||
if (icon != null) {
|
||||
bm = drawableToBitmap(icon);
|
||||
ic = Icon.createWithBitmap(bm);
|
||||
} else {
|
||||
icon = DrawableCompat.getDrawable(context, fallbackIconId);
|
||||
Objects.requireNonNull(icon);
|
||||
bm = drawableToBitmap(icon);
|
||||
ic = Icon.createWithResource(context, fallbackIconId);
|
||||
}
|
||||
ShortcutInfo si = createShortcutInfo(context, id, intent, label, ic);
|
||||
|
||||
Intent result = null;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
final ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||
result = shortcutManager.createShortcutResultIntent(si);
|
||||
}
|
||||
if (result == null) {
|
||||
result = new Intent();
|
||||
}
|
||||
ShortcutCreatorOld.addToIntent(result, intent, label, bm);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void pushDynamicShortcut(@NonNull Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
Icon ic;
|
||||
if (icon != null) {
|
||||
ic = Icon.createWithBitmap(drawableToBitmap(icon));
|
||||
} else {
|
||||
ic = Icon.createWithResource(context, fallbackIconId);
|
||||
}
|
||||
ShortcutInfo si = createShortcutInfo(context, id, intent, label, ic);
|
||||
|
||||
final ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager.isRateLimitingActive()) {
|
||||
return;
|
||||
}
|
||||
List<ShortcutInfo> shortcuts = shortcutManager.getDynamicShortcuts();
|
||||
// Sort shortcuts by rank, timestamp and id
|
||||
Collections.sort(shortcuts, new Comparator<ShortcutInfo>() {
|
||||
@Override
|
||||
public int compare(ShortcutInfo a, ShortcutInfo b) {
|
||||
int ret = Integer.compare(a.getRank(), b.getRank());
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = Long.compare(a.getLastChangedTimestamp(), b.getLastChangedTimestamp());
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
return a.getId().compareTo(b.getId());
|
||||
}
|
||||
});
|
||||
|
||||
// In old Android versions, only 4 shortcuts are displayed but 5 maximum are supported
|
||||
// Problem: the last one added is not displayed... so stick to 4
|
||||
int maxSize = Math.min(shortcutManager.getMaxShortcutCountPerActivity(), 4);
|
||||
if (shortcuts.size() >= maxSize) {
|
||||
int toRemove = shortcuts.size() - maxSize + 1;
|
||||
ArrayList<String> toRemoveList = new ArrayList<>(toRemove);
|
||||
// Remove the lowest rank, oldest shortcut if we need it
|
||||
for(ShortcutInfo oldSi : shortcuts) {
|
||||
if (oldSi.getId().equals(id)) {
|
||||
// We will update it: no need to make space
|
||||
toRemoveList.clear();
|
||||
break;
|
||||
}
|
||||
if (toRemove > 0) {
|
||||
toRemoveList.add(oldSi.getId());
|
||||
toRemove -= 1;
|
||||
}
|
||||
}
|
||||
shortcutManager.removeDynamicShortcuts(toRemoveList);
|
||||
}
|
||||
shortcuts = new ArrayList<>(1);
|
||||
shortcuts.add(si);
|
||||
shortcutManager.addDynamicShortcuts(shortcuts);
|
||||
shortcutManager.reportShortcutUsed(id);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.R)
|
||||
private static class ShortcutCreatorR {
|
||||
public static void pushDynamicShortcut(@NonNull Context context, String id, @NonNull Intent intent, @NonNull String label, @Nullable Drawable icon, @DrawableRes int fallbackIconId) {
|
||||
Icon ic;
|
||||
if (icon != null) {
|
||||
ic = Icon.createWithBitmap(drawableToBitmap(icon));
|
||||
} else {
|
||||
ic = Icon.createWithResource(context, fallbackIconId);
|
||||
}
|
||||
ShortcutInfo si = ShortcutCreatorN_MR1.createShortcutInfo(context, id, intent, label, ic);
|
||||
|
||||
final ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
|
||||
shortcutManager.pushDynamicShortcut(si);
|
||||
// pushDynamicShortcut already reports usage
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap drawableToBitmap(@NonNull Drawable drawable) {
|
||||
// We resize to 128x128 to avoid having too big bitmaps for Binder
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
Bitmap bm = ((BitmapDrawable)drawable).getBitmap();
|
||||
bm = Bitmap.createScaledBitmap(bm, 128, 128, true);
|
||||
return bm.copy(bm.getConfig(), false);
|
||||
}
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
|
||||
// Create an immutable bitmap
|
||||
return bitmap.copy(bitmap.getConfig(), false);
|
||||
}
|
||||
}
|
||||
|
||||
static class DrawableCompat {
|
||||
public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) throws Resources.NotFoundException {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
return DrawableCompatLollipop.getDrawable(context, id);
|
||||
} else {
|
||||
return DrawableCompatOld.getDrawable(context, id);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
|
||||
private static class DrawableCompatLollipop {
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) throws Resources.NotFoundException {
|
||||
return context.getDrawable(id);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class DrawableCompatOld {
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
public static Drawable getDrawable(@NonNull Context context, @DrawableRes int id) throws Resources.NotFoundException {
|
||||
return context.getResources().getDrawable(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,860 @@
|
||||
/*
|
||||
* Copyright (C) 2008-2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.annotation.XmlRes;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
|
||||
/**
|
||||
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
|
||||
* consists of rows of keys.
|
||||
* <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
|
||||
* <pre>
|
||||
* <Keyboard
|
||||
* android:keyWidth="%10p"
|
||||
* android:keyHeight="50px"
|
||||
* android:horizontalGap="2px"
|
||||
* android:verticalGap="2px" >
|
||||
* <Row android:keyWidth="32px" >
|
||||
* <Key android:keyLabel="A" />
|
||||
* ...
|
||||
* </Row>
|
||||
* ...
|
||||
* </Keyboard>
|
||||
* </pre>
|
||||
*/
|
||||
public class CustomKeyboard {
|
||||
|
||||
static final String LOG_TAG = "CustomKeyboard";
|
||||
|
||||
// Keyboard XML Tags
|
||||
private static final String TAG_KEYBOARD = "CustomKeyboard";
|
||||
private static final String TAG_ROW = "CustomRow";
|
||||
private static final String TAG_KEY = "CustomKey";
|
||||
|
||||
public static final int EDGE_LEFT = 0x01;
|
||||
public static final int EDGE_RIGHT = 0x02;
|
||||
public static final int EDGE_TOP = 0x04;
|
||||
public static final int EDGE_BOTTOM = 0x08;
|
||||
|
||||
public static final int KEYCODE_SHIFT = -1;
|
||||
public static final int KEYCODE_MODE_CHANGE = -2;
|
||||
public static final int KEYCODE_CANCEL = -3;
|
||||
public static final int KEYCODE_DONE = -4;
|
||||
public static final int KEYCODE_DELETE = -5;
|
||||
public static final int KEYCODE_ALT = -6;
|
||||
|
||||
/** Keyboard label **/
|
||||
private CharSequence mLabel;
|
||||
|
||||
/** Horizontal gap default for all rows */
|
||||
private int mDefaultHorizontalGap;
|
||||
|
||||
/** Default key width */
|
||||
private int mDefaultWidth;
|
||||
|
||||
/** Default key height */
|
||||
private int mDefaultHeight;
|
||||
|
||||
/** Default gap between rows */
|
||||
private int mDefaultVerticalGap;
|
||||
|
||||
/** Is the keyboard in the shifted state */
|
||||
private boolean mShifted;
|
||||
|
||||
/** Key instance for the shift key, if present */
|
||||
private CustomKey[] mShiftKeys = { null, null };
|
||||
|
||||
/** Key index for the shift key, if present */
|
||||
private int[] mShiftKeyIndices = {-1, -1};
|
||||
|
||||
/** Current key width, while loading the keyboard */
|
||||
private int mKeyWidth;
|
||||
|
||||
/** Current key height, while loading the keyboard */
|
||||
private int mKeyHeight;
|
||||
|
||||
/** Total height of the keyboard, including the padding and keys */
|
||||
// @UnsupportedAppUsage
|
||||
private int mTotalHeight;
|
||||
|
||||
/**
|
||||
* Total width of the keyboard, including left side gaps and keys, but not any gaps on the
|
||||
* right side.
|
||||
*/
|
||||
// @UnsupportedAppUsage
|
||||
private int mTotalWidth;
|
||||
|
||||
/** List of keys in this keyboard */
|
||||
private List<CustomKey> mKeys;
|
||||
|
||||
/** List of modifier keys such as Shift & Alt, if any */
|
||||
// @UnsupportedAppUsage
|
||||
private List<CustomKey> mModifierKeys;
|
||||
|
||||
/** Width of the screen available to fit the keyboard */
|
||||
private int mDisplayWidth;
|
||||
|
||||
/** Height of the screen */
|
||||
private int mDisplayHeight;
|
||||
|
||||
/** Keyboard mode, or zero, if none. */
|
||||
private int mKeyboardMode;
|
||||
|
||||
// Variables for pre-computing nearest keys.
|
||||
|
||||
private static final int GRID_WIDTH = 10;
|
||||
private static final int GRID_HEIGHT = 5;
|
||||
private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
|
||||
private int mCellWidth;
|
||||
private int mCellHeight;
|
||||
private int[][] mGridNeighbors;
|
||||
private int mProximityThreshold;
|
||||
/** Number of key widths from current touch point to search for nearest keys. */
|
||||
private static float SEARCH_DISTANCE = 1.8f;
|
||||
|
||||
private ArrayList<CustomRow> rows = new ArrayList<CustomRow>();
|
||||
|
||||
/**
|
||||
* Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
|
||||
* Some of the key size defaults can be overridden per row from what the {@link CustomKeyboard}
|
||||
* defines.
|
||||
*/
|
||||
public static class CustomRow {
|
||||
/** Default width of a key in this row. */
|
||||
public int defaultWidth;
|
||||
/** Default height of a key in this row. */
|
||||
public int defaultHeight;
|
||||
/** Default horizontal gap between keys in this row. */
|
||||
public int defaultHorizontalGap;
|
||||
/** Vertical gap following this row. */
|
||||
public int verticalGap;
|
||||
|
||||
ArrayList<CustomKey> mKeys = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Edge flags for this row of keys. Possible values that can be assigned are
|
||||
* {@link CustomKeyboard#EDGE_TOP EDGE_TOP} and {@link CustomKeyboard#EDGE_BOTTOM EDGE_BOTTOM}
|
||||
*/
|
||||
public int rowEdgeFlags;
|
||||
|
||||
/** The keyboard mode for this row */
|
||||
public int mode;
|
||||
|
||||
private CustomKeyboard parent;
|
||||
|
||||
public CustomRow(CustomKeyboard parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public CustomRow(Resources res, CustomKeyboard parent, XmlResourceParser parser) {
|
||||
this.parent = parent;
|
||||
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard);
|
||||
defaultWidth = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyWidth, parent.mDisplayWidth, parent.mDefaultWidth);
|
||||
defaultHeight = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyHeight, parent.mDisplayHeight, parent.mDefaultHeight);
|
||||
defaultHorizontalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_horizontalGap, parent.mDisplayWidth, parent.mDefaultHorizontalGap);
|
||||
verticalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_verticalGap, parent.mDisplayHeight, parent.mDefaultVerticalGap);
|
||||
a.recycle();
|
||||
|
||||
a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard_CustomRow);
|
||||
rowEdgeFlags = a.getInt(R.styleable.CustomKeyboard_CustomRow_rowEdgeFlags, 0);
|
||||
mode = a.getResourceId(R.styleable.CustomKeyboard_CustomRow_keyboardMode, 0);
|
||||
a.recycle();
|
||||
}
|
||||
} // end of: static class CustomRow
|
||||
|
||||
/**
|
||||
* Class for describing the position and characteristics of a single key in the keyboard.
|
||||
*
|
||||
*/
|
||||
public static class CustomKey {
|
||||
/**
|
||||
* All the key codes (unicode or custom code) that this key could generate, zero'th
|
||||
* being the most important.
|
||||
*/
|
||||
public int[] codes;
|
||||
|
||||
/** Label to display */
|
||||
public CharSequence label;
|
||||
|
||||
/** Icon to display instead of a label. Icon takes precedence over a label */
|
||||
public Drawable icon;
|
||||
/** Preview version of the icon, for the preview popup */
|
||||
public Drawable iconPreview;
|
||||
/** Width of the key, not including the gap */
|
||||
public int width;
|
||||
/** Height of the key, not including the gap */
|
||||
public int height;
|
||||
/** The horizontal gap before this key */
|
||||
public int gap;
|
||||
/** Whether this key is sticky, i.e., a toggle key */
|
||||
public boolean sticky;
|
||||
/** X coordinate of the key in the keyboard layout */
|
||||
public int x;
|
||||
/** Y coordinate of the key in the keyboard layout */
|
||||
public int y;
|
||||
/** The current pressed state of this key */
|
||||
public boolean pressed;
|
||||
/** If this is a sticky key, is it on? */
|
||||
public boolean on;
|
||||
/** Text to output when pressed. This can be multiple characters, like ".com" */
|
||||
public CharSequence text;
|
||||
/** Popup characters */
|
||||
public CharSequence popupCharacters;
|
||||
|
||||
/**
|
||||
* Flags that specify the anchoring to edges of the keyboard for detecting touch events
|
||||
* that are just out of the boundary of the key. This is a bit mask of
|
||||
* {@link CustomKeyboard#EDGE_LEFT}, {@link CustomKeyboard#EDGE_RIGHT}, {@link CustomKeyboard#EDGE_TOP} and
|
||||
* {@link CustomKeyboard#EDGE_BOTTOM}.
|
||||
*/
|
||||
public int edgeFlags;
|
||||
/** Whether this is a modifier key, such as Shift or Alt */
|
||||
public boolean modifier;
|
||||
/** The keyboard that this key belongs to */
|
||||
private CustomKeyboard keyboard;
|
||||
/**
|
||||
* If this key pops up a mini keyboard, this is the resource id for the XML layout for that
|
||||
* keyboard.
|
||||
*/
|
||||
public int popupResId;
|
||||
/** Whether this key repeats itself when held down */
|
||||
public boolean repeatable;
|
||||
|
||||
|
||||
private final static int[] KEY_STATE_NORMAL_ON = {
|
||||
android.R.attr.state_checkable,
|
||||
android.R.attr.state_checked
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_PRESSED_ON = {
|
||||
android.R.attr.state_pressed,
|
||||
android.R.attr.state_checkable,
|
||||
android.R.attr.state_checked
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_NORMAL_OFF = {
|
||||
android.R.attr.state_checkable
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_PRESSED_OFF = {
|
||||
android.R.attr.state_pressed,
|
||||
android.R.attr.state_checkable
|
||||
};
|
||||
|
||||
private final static int[] KEY_STATE_NORMAL = { };
|
||||
|
||||
private final static int[] KEY_STATE_PRESSED = {
|
||||
android.R.attr.state_pressed
|
||||
};
|
||||
|
||||
/** Create an empty key with no attributes. */
|
||||
public CustomKey(CustomRow parent) {
|
||||
keyboard = parent.parent;
|
||||
height = parent.defaultHeight;
|
||||
width = parent.defaultWidth;
|
||||
gap = parent.defaultHorizontalGap;
|
||||
edgeFlags = parent.rowEdgeFlags;
|
||||
}
|
||||
|
||||
/** Create a key with the given top-left coordinate and extract its attributes from
|
||||
* the XML parser.
|
||||
* @param res resources associated with the caller's context
|
||||
* @param parent the row that this key belongs to. The row must already be attached to
|
||||
* a {@link CustomKeyboard}.
|
||||
* @param x the x coordinate of the top-left
|
||||
* @param y the y coordinate of the top-left
|
||||
* @param parser the XML parser containing the attributes for this key
|
||||
*/
|
||||
public CustomKey(Resources res, CustomRow parent, int x, int y, XmlResourceParser parser) {
|
||||
this(parent);
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
|
||||
// obtainAttributes (AttributeSet set, int[] attrs)
|
||||
// Retrieve a set of basic attribute values from an AttributeSet, not performing styling of them using a theme and/or style resources.
|
||||
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard);
|
||||
|
||||
width = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyWidth, keyboard.mDisplayWidth, parent.defaultWidth);
|
||||
height = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyHeight, keyboard.mDisplayHeight, parent.defaultHeight);
|
||||
gap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_horizontalGap, keyboard.mDisplayWidth, parent.defaultHorizontalGap);
|
||||
|
||||
// Log.d(LOG_TAG, "from CustomKeyboard: wid: " +width + " heigh: " + height + " gap: " + gap );
|
||||
|
||||
a.recycle();
|
||||
a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard_CustomKey);
|
||||
this.x += gap;
|
||||
TypedValue codesValue = new TypedValue();
|
||||
a.getValue(R.styleable.CustomKeyboard_CustomKey_codes, codesValue);
|
||||
if (codesValue.type == TypedValue.TYPE_INT_DEC || codesValue.type == TypedValue.TYPE_INT_HEX) {
|
||||
// Log.d(LOG_TAG, "Key codes is INT or HEX");
|
||||
codes = new int[] { codesValue.data };
|
||||
} else if (codesValue.type == TypedValue.TYPE_STRING) {
|
||||
// Log.d(LOG_TAG, "Key codes is String");
|
||||
codes = parseCSV(codesValue.string.toString());
|
||||
}
|
||||
|
||||
iconPreview = a.getDrawable(R.styleable.CustomKeyboard_CustomKey_iconPreview);
|
||||
if (iconPreview != null) {
|
||||
iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), iconPreview.getIntrinsicHeight());
|
||||
}
|
||||
popupCharacters = a.getText(R.styleable.CustomKeyboard_CustomKey_popupCharacters);
|
||||
popupResId = a.getResourceId(R.styleable.CustomKeyboard_CustomKey_popupKeyboard, 0);
|
||||
repeatable = a.getBoolean(R.styleable.CustomKeyboard_CustomKey_isRepeatable, false);
|
||||
modifier = a.getBoolean(R.styleable.CustomKeyboard_CustomKey_isModifier, false);
|
||||
sticky = a.getBoolean(R.styleable.CustomKeyboard_CustomKey_isSticky, false);
|
||||
edgeFlags = a.getInt(R.styleable.CustomKeyboard_CustomKey_keyEdgeFlags, 0);
|
||||
edgeFlags |= parent.rowEdgeFlags;
|
||||
|
||||
icon = a.getDrawable(R.styleable.CustomKeyboard_CustomKey_keyIcon);
|
||||
if (icon != null) {
|
||||
icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
|
||||
}
|
||||
label = a.getText(R.styleable.CustomKeyboard_CustomKey_keyLabel);
|
||||
// Log.d(LOG_TAG, "Key label is " + label);
|
||||
|
||||
text = a.getText(R.styleable.CustomKeyboard_CustomKey_keyOutputText);
|
||||
|
||||
if (codes == null && !TextUtils.isEmpty(label)) {
|
||||
codes = new int[] { label.charAt(0) };
|
||||
}
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the key that it has been pressed, in case it needs to change its appearance or
|
||||
* state.
|
||||
* @see #onReleased(boolean)
|
||||
*/
|
||||
public void onPressed() {
|
||||
pressed = !pressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the pressed state of the key.
|
||||
*
|
||||
* <p>Toggled state of the key will be flipped when all the following conditions are
|
||||
* fulfilled:</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>This is a sticky key, that is, {@link #sticky} is {@code true}.
|
||||
* <li>The parameter {@code inside} is {@code true}.
|
||||
* <li>{@link android.os.Build.VERSION#SDK_INT} is greater than
|
||||
* {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
|
||||
* </ul>
|
||||
*
|
||||
* @param inside whether the finger was released inside the key. Works only on Android M and
|
||||
* later. See the method document for details.
|
||||
* @see #onPressed()
|
||||
*/
|
||||
public void onReleased(boolean inside) {
|
||||
pressed = !pressed;
|
||||
if (sticky && inside) {
|
||||
on = !on;
|
||||
}
|
||||
}
|
||||
|
||||
int[] parseCSV(String value) {
|
||||
int count = 0;
|
||||
int lastIndex = 0;
|
||||
if (value.length() > 0) {
|
||||
count++;
|
||||
while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
int[] values = new int[count];
|
||||
count = 0;
|
||||
StringTokenizer st = new StringTokenizer(value, ",");
|
||||
while (st.hasMoreTokens()) {
|
||||
try {
|
||||
values[count++] = Integer.parseInt(st.nextToken());
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.e(LOG_TAG, "Error parsing keycodes " + value);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a point falls inside this key.
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return whether or not the point falls inside the key. If the key is attached to an edge,
|
||||
* it will assume that all points between the key and the edge are considered to be inside
|
||||
* the key.
|
||||
*/
|
||||
public boolean isInside(int x, int y) {
|
||||
boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
|
||||
boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
|
||||
boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
|
||||
boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
|
||||
if ((x >= this.x || (leftEdge && x <= this.x + this.width))
|
||||
&& (x < this.x + this.width || (rightEdge && x >= this.x))
|
||||
&& (y >= this.y || (topEdge && y <= this.y + this.height))
|
||||
&& (y < this.y + this.height || (bottomEdge && y >= this.y))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the square of the distance between the center of the key and the given point.
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return the square of the distance of the point from the center of the key
|
||||
*/
|
||||
public int squaredDistanceFrom(int x, int y) {
|
||||
int xDist = this.x + width / 2 - x;
|
||||
int yDist = this.y + height / 2 - y;
|
||||
return xDist * xDist + yDist * yDist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the drawable state for the key, based on the current state and type of the key.
|
||||
* @return the drawable state of the key.
|
||||
* @see android.graphics.drawable.StateListDrawable#setState(int[])
|
||||
*/
|
||||
public int[] getCurrentDrawableState() {
|
||||
int[] states = KEY_STATE_NORMAL;
|
||||
|
||||
if (on) {
|
||||
if (pressed) {
|
||||
states = KEY_STATE_PRESSED_ON;
|
||||
} else {
|
||||
states = KEY_STATE_NORMAL_ON;
|
||||
}
|
||||
} else {
|
||||
if (sticky) {
|
||||
if (pressed) {
|
||||
states = KEY_STATE_PRESSED_OFF;
|
||||
} else {
|
||||
states = KEY_STATE_NORMAL_OFF;
|
||||
}
|
||||
} else {
|
||||
if (pressed) {
|
||||
states = KEY_STATE_PRESSED;
|
||||
}
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
} // end of: static class CustomKey
|
||||
|
||||
/**
|
||||
* Creates a keyboard from the given xml key layout file.
|
||||
* @param context the application or service context
|
||||
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||
*/
|
||||
public CustomKeyboard(Context context, int xmlLayoutResId) {
|
||||
this(context, xmlLayoutResId, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||
* that have a keyboard mode defined but don't match the specified mode.
|
||||
* @param context the application or service context
|
||||
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||
* @param modeId keyboard mode identifier
|
||||
* @param width sets width of keyboard
|
||||
* @param height sets height of keyboard
|
||||
*/
|
||||
public CustomKeyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width, int height) {
|
||||
mDisplayWidth = width;
|
||||
mDisplayHeight = height;
|
||||
|
||||
mDefaultHorizontalGap = 0;
|
||||
mDefaultWidth = mDisplayWidth / 10;
|
||||
mDefaultVerticalGap = 0;
|
||||
mDefaultHeight = mDefaultWidth;
|
||||
mKeys = new ArrayList<CustomKey>();
|
||||
mModifierKeys = new ArrayList<CustomKey>();
|
||||
mKeyboardMode = modeId;
|
||||
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a keyboard from the given xml key layout file. Weeds out rows
|
||||
* that have a keyboard mode defined but don't match the specified mode.
|
||||
* @param context the application or service context
|
||||
* @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
|
||||
* @param modeId keyboard mode identifier
|
||||
*/
|
||||
public CustomKeyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) {
|
||||
DisplayMetrics dm = context.getResources().getDisplayMetrics();
|
||||
mDisplayWidth = dm.widthPixels;
|
||||
mDisplayHeight = dm.heightPixels;
|
||||
// Log.v(LOG_TAG, "keyboard's display metrics:" + dm);
|
||||
|
||||
mDefaultHorizontalGap = 0;
|
||||
mDefaultWidth = mDisplayWidth / 10;
|
||||
mDefaultVerticalGap = 0;
|
||||
mDefaultHeight = mDefaultWidth;
|
||||
mKeys = new ArrayList<CustomKey>();
|
||||
mModifierKeys = new ArrayList<CustomKey>();
|
||||
mKeyboardMode = modeId;
|
||||
// Log.v(LOG_TAG, "Resource ID is " + xmlLayoutResId + " and parser is null?" + ((context.getResources().getXml(xmlLayoutResId) == null) ? "yes" : "no"));
|
||||
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Creates a blank keyboard from the given resource file and populates it with the specified
|
||||
* characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
|
||||
* </p>
|
||||
* <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
|
||||
* possible in each row.</p>
|
||||
* @param context the application or service context
|
||||
* @param layoutTemplateResId the layout template file, containing no keys.
|
||||
* @param characters the list of characters to display on the keyboard. One key will be created
|
||||
* for each character.
|
||||
* @param columns the number of columns of keys to display. If this number is greater than the
|
||||
* number of keys that can fit in a row, it will be ignored. If this number is -1, the
|
||||
* keyboard will fit as many keys as possible in each row.
|
||||
*/
|
||||
public CustomKeyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding) {
|
||||
this(context, layoutTemplateResId);
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int column = 0;
|
||||
mTotalWidth = 0;
|
||||
|
||||
CustomRow row = new CustomRow(this);
|
||||
row.defaultHeight = mDefaultHeight;
|
||||
row.defaultWidth = mDefaultWidth;
|
||||
row.defaultHorizontalGap = mDefaultHorizontalGap;
|
||||
row.verticalGap = mDefaultVerticalGap;
|
||||
row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
|
||||
final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
|
||||
for (int i = 0; i < characters.length(); i++) {
|
||||
char c = characters.charAt(i);
|
||||
if (column >= maxColumns || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
|
||||
x = 0;
|
||||
y += mDefaultVerticalGap + mDefaultHeight;
|
||||
column = 0;
|
||||
}
|
||||
final CustomKey key = new CustomKey(row);
|
||||
key.x = x;
|
||||
key.y = y;
|
||||
key.label = String.valueOf(c);
|
||||
key.codes = new int[] { c };
|
||||
column++;
|
||||
x += key.width + key.gap;
|
||||
mKeys.add(key);
|
||||
row.mKeys.add(key);
|
||||
if (x > mTotalWidth) {
|
||||
mTotalWidth = x;
|
||||
}
|
||||
}
|
||||
mTotalHeight = y + mDefaultHeight;
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
// @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
||||
final void resize(int newWidth, int newHeight) {
|
||||
int numRows = rows.size();
|
||||
for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
|
||||
CustomRow row = rows.get(rowIndex);
|
||||
int numKeys = row.mKeys.size();
|
||||
int totalGap = 0;
|
||||
int totalWidth = 0;
|
||||
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||
CustomKey key = row.mKeys.get(keyIndex);
|
||||
if (keyIndex > 0) {
|
||||
totalGap += key.gap;
|
||||
}
|
||||
totalWidth += key.width;
|
||||
}
|
||||
if (totalGap + totalWidth > newWidth) {
|
||||
int x = 0;
|
||||
float scaleFactor = (float)(newWidth - totalGap) / totalWidth;
|
||||
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
|
||||
CustomKey key = row.mKeys.get(keyIndex);
|
||||
key.width *= scaleFactor;
|
||||
key.x = x;
|
||||
x += key.width + key.gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
mTotalWidth = newWidth;
|
||||
// TODO: This does not adjust the vertical placement according to the new size.
|
||||
// The main problem in the previous code was horizontal placement/size, but we should
|
||||
// also recalculate the vertical sizes/positions when we get this resize call.
|
||||
}
|
||||
|
||||
public List<CustomKey> getKeys() {
|
||||
return mKeys;
|
||||
}
|
||||
|
||||
public List<CustomKey> getModifierKeys() {
|
||||
return mModifierKeys;
|
||||
}
|
||||
|
||||
protected int getHorizontalGap() {
|
||||
return mDefaultHorizontalGap;
|
||||
}
|
||||
|
||||
protected void setHorizontalGap(int gap) {
|
||||
mDefaultHorizontalGap = gap;
|
||||
}
|
||||
|
||||
protected int getVerticalGap() {
|
||||
return mDefaultVerticalGap;
|
||||
}
|
||||
|
||||
protected void setVerticalGap(int gap) {
|
||||
mDefaultVerticalGap = gap;
|
||||
}
|
||||
|
||||
protected int getKeyHeight() {
|
||||
return mDefaultHeight;
|
||||
}
|
||||
|
||||
protected void setKeyHeight(int height) {
|
||||
mDefaultHeight = height;
|
||||
}
|
||||
|
||||
protected int getKeyWidth() {
|
||||
return mDefaultWidth;
|
||||
}
|
||||
|
||||
protected void setKeyWidth(int width) {
|
||||
mDefaultWidth = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total height of the keyboard
|
||||
* @return the total height of the keyboard
|
||||
*/
|
||||
public int getHeight() {
|
||||
return mTotalHeight;
|
||||
}
|
||||
|
||||
public int getMinWidth() {
|
||||
return mTotalWidth;
|
||||
}
|
||||
|
||||
public boolean setShifted(boolean shiftState) {
|
||||
for (CustomKey shiftKey : mShiftKeys) {
|
||||
if (shiftKey != null) {
|
||||
shiftKey.on = shiftState;
|
||||
}
|
||||
}
|
||||
if (mShifted != shiftState) {
|
||||
mShifted = shiftState;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isShifted() {
|
||||
return mShifted;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public int[] getShiftKeyIndices() {
|
||||
return mShiftKeyIndices;
|
||||
}
|
||||
|
||||
public int getShiftKeyIndex() {
|
||||
return mShiftKeyIndices[0];
|
||||
}
|
||||
|
||||
private void computeNearestNeighbors() {
|
||||
// Round-up so we don't have any pixels outside the grid
|
||||
mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
|
||||
mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
|
||||
mGridNeighbors = new int[GRID_SIZE][];
|
||||
int[] indices = new int[mKeys.size()];
|
||||
final int gridWidth = GRID_WIDTH * mCellWidth;
|
||||
final int gridHeight = GRID_HEIGHT * mCellHeight;
|
||||
for (int x = 0; x < gridWidth; x += mCellWidth) {
|
||||
for (int y = 0; y < gridHeight; y += mCellHeight) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < mKeys.size(); i++) {
|
||||
final CustomKey key = mKeys.get(i);
|
||||
if (key.squaredDistanceFrom(x, y) < mProximityThreshold
|
||||
|| key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold
|
||||
|| key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1) < mProximityThreshold
|
||||
|| key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) {
|
||||
indices[count++] = i;
|
||||
}
|
||||
}
|
||||
int [] cell = new int[count];
|
||||
System.arraycopy(indices, 0, cell, 0, count);
|
||||
mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the indices of the keys that are closest to the given point.
|
||||
* @param x the x-coordinate of the point
|
||||
* @param y the y-coordinate of the point
|
||||
* @return the array of integer indices for the nearest keys to the given point. If the given
|
||||
* point is out of range, then an array of size zero is returned.
|
||||
*/
|
||||
public int[] getNearestKeys(int x, int y) {
|
||||
if (mGridNeighbors == null) computeNearestNeighbors();
|
||||
if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
|
||||
int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
|
||||
if (index < GRID_SIZE) {
|
||||
return mGridNeighbors[index];
|
||||
}
|
||||
}
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
protected CustomRow createRowFromXml(Resources res, XmlResourceParser parser) {
|
||||
return new CustomRow(res, this, parser);
|
||||
}
|
||||
|
||||
protected CustomKey createKeyFromXml(Resources res, CustomRow parent, int x, int y, XmlResourceParser parser) {
|
||||
return new CustomKey(res, parent, x, y, parser);
|
||||
}
|
||||
|
||||
private void loadKeyboard(Context context, XmlResourceParser parser) {
|
||||
boolean inKey = false;
|
||||
boolean inRow = false;
|
||||
boolean leftMostKey = false;
|
||||
int row = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
CustomKey key = null;
|
||||
CustomRow currentRow = null;
|
||||
Resources res = context.getResources();
|
||||
boolean skipRow = false;
|
||||
|
||||
try {
|
||||
int event;
|
||||
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||
if (event == XmlResourceParser.START_TAG) {
|
||||
String tag = parser.getName();
|
||||
if (TAG_ROW.equals(tag)) {
|
||||
// Log.d(LOG_TAG, "TAG ROW");
|
||||
inRow = true;
|
||||
x = 0;
|
||||
currentRow = createRowFromXml(res, parser);
|
||||
rows.add(currentRow);
|
||||
skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
|
||||
if (skipRow) {
|
||||
skipToEndOfRow(parser);
|
||||
inRow = false;
|
||||
}
|
||||
} else if (TAG_KEY.equals(tag)) {
|
||||
// Log.d(LOG_TAG, "TAG KEY");
|
||||
inKey = true;
|
||||
key = createKeyFromXml(res, currentRow, x, y, parser);
|
||||
mKeys.add(key);
|
||||
if (key.codes != null) {
|
||||
if (key.codes[0] == KEYCODE_SHIFT) {
|
||||
// Find available shift key slot and put this shift key in it
|
||||
for (int i = 0; i < mShiftKeys.length; i++) {
|
||||
if (mShiftKeys[i] == null) {
|
||||
mShiftKeys[i] = key;
|
||||
mShiftKeyIndices[i] = mKeys.size()-1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mModifierKeys.add(key);
|
||||
} else if (key.codes[0] == KEYCODE_ALT) {
|
||||
mModifierKeys.add(key);
|
||||
}
|
||||
currentRow.mKeys.add(key);
|
||||
}
|
||||
} else if (TAG_KEYBOARD.equals(tag)) {
|
||||
parseKeyboardAttributes(res, parser);
|
||||
}
|
||||
} else if (event == XmlResourceParser.END_TAG) {
|
||||
if (inKey) {
|
||||
inKey = false;
|
||||
x += key.gap + key.width;
|
||||
if (x > mTotalWidth) {
|
||||
mTotalWidth = x;
|
||||
}
|
||||
} else if (inRow) {
|
||||
inRow = false;
|
||||
y += currentRow.verticalGap;
|
||||
y += currentRow.defaultHeight;
|
||||
row++;
|
||||
} else {
|
||||
// TODO: error or extend?
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "Parse error:" + e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
mTotalHeight = y - mDefaultVerticalGap;
|
||||
}
|
||||
|
||||
private void skipToEndOfRow(XmlResourceParser parser) throws XmlPullParserException, IOException {
|
||||
int event;
|
||||
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
|
||||
if (event == XmlResourceParser.END_TAG && parser.getName().equals(TAG_ROW)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
|
||||
TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.CustomKeyboard);
|
||||
|
||||
mDefaultWidth = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyWidth, mDisplayWidth, mDisplayWidth / 10);
|
||||
mDefaultHeight = getDimensionOrFraction(a, R.styleable.CustomKeyboard_keyHeight, mDisplayHeight, 50);
|
||||
mDefaultHorizontalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_horizontalGap, mDisplayWidth, 0);
|
||||
mDefaultVerticalGap = getDimensionOrFraction(a, R.styleable.CustomKeyboard_verticalGap, mDisplayHeight, 0);
|
||||
mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
|
||||
mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
|
||||
TypedValue value = a.peekValue(index);
|
||||
if (value == null)
|
||||
return defValue;
|
||||
|
||||
if (value.type == TypedValue.TYPE_DIMENSION) {
|
||||
return a.getDimensionPixelOffset(index, defValue);
|
||||
} else if (value.type == TypedValue.TYPE_FRACTION) {
|
||||
// Round it to avoid values like 47.9999 from getting truncated
|
||||
return Math.round(a.getFraction(index, base, base, defValue));
|
||||
}
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
public class EditableAccommodatingLatinIMETypeNullIssues extends SpannableStringBuilder {
|
||||
EditableAccommodatingLatinIMETypeNullIssues(CharSequence source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
//This character must be ignored by your onKey() code.
|
||||
public static final CharSequence ONE_UNPROCESSED_CHARACTER = "\\";
|
||||
|
||||
@Override
|
||||
public SpannableStringBuilder replace(final int spannableStringStart, final int spannableStringEnd, CharSequence replacementSequence, int replacementStart, int replacementEnd) {
|
||||
if (replacementEnd > replacementStart) {
|
||||
//In this case, there is something in the replacementSequence that the IME
|
||||
// is attempting to replace part of the editable with.
|
||||
//We don't really care about whatever might already be in the editable;
|
||||
// we only care about making sure that SOMETHING ends up in it,
|
||||
// so that the backspace key will continue to work.
|
||||
// So, start by zeroing out whatever is there to begin with.
|
||||
super.replace(0, length(), "", 0, 0);
|
||||
|
||||
//We DO care about preserving the new stuff that is replacing the stuff in the
|
||||
// editable, because this stuff might be sent to us as a keyDown event. So, we
|
||||
// insert the new stuff (typically, a single character) into the now-empty editable,
|
||||
// and return the result to the caller.
|
||||
return super.replace(0, 0, replacementSequence, replacementStart, replacementEnd);
|
||||
} else if (spannableStringEnd > spannableStringStart) {
|
||||
//In this case, there is NOTHING in the replacementSequence, and something is
|
||||
// being replaced in the editable.
|
||||
// This is characteristic of a DELETION.
|
||||
// So, start by zeroing out whatever is being replaced in the editable.
|
||||
super.replace(0, length(), "", 0, 0);
|
||||
|
||||
//And now, we will place our ONE_UNPROCESSED_CHARACTER into the editable buffer, and return it.
|
||||
return super.replace(0, 0, ONE_UNPROCESSED_CHARACTER, 0, 1);
|
||||
}
|
||||
|
||||
// In this case, NOTHING is being replaced in the editable. This code assumes that there
|
||||
// is already something there. This assumption is probably OK because in our
|
||||
// InputConnectionAccommodatingLatinIMETypeNullIssues.getEditable() method
|
||||
// we PLACE a ONE_UNPROCESSED_CHARACTER into the newly-created buffer. So if there
|
||||
// is nothing replacing the identified part
|
||||
// of the editable, and no part of the editable that is being replaced, then we just
|
||||
// leave whatever is in the editable ALONE,
|
||||
// and we can be confident that there will be SOMETHING there. This call to super.replace()
|
||||
// in that case will be a no-op, except
|
||||
// for the value it returns.
|
||||
return super.replace(spannableStringStart, spannableStringEnd,
|
||||
replacementSequence, replacementStart, replacementEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Selection;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
public class EditableSurfaceView extends SurfaceView {
|
||||
final Context _context;
|
||||
final boolean _allowHideSystemMousePointer;
|
||||
private boolean _mouseIsInCapturedState;
|
||||
|
||||
public EditableSurfaceView(Context context) {
|
||||
|
||||
super(context);
|
||||
_context = context;
|
||||
_mouseIsInCapturedState = false;
|
||||
_allowHideSystemMousePointer = true;
|
||||
}
|
||||
|
||||
public EditableSurfaceView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
_context = context;
|
||||
_mouseIsInCapturedState = false;
|
||||
_allowHideSystemMousePointer = true;
|
||||
}
|
||||
|
||||
public EditableSurfaceView(Context context,
|
||||
AttributeSet attrs,
|
||||
int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
_context = context;
|
||||
_mouseIsInCapturedState = false;
|
||||
_allowHideSystemMousePointer = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCheckIsTextEditor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private class MyInputConnection extends BaseInputConnection {
|
||||
// The false second argument puts the BaseInputConnection into "dummy" mode, which is also required in order for the raw key events to be sent to your view.
|
||||
// In the BaseInputConnection code, you can find several comments such as the following: "only if dummy mode, a key event is sent for the new text and the current editable buffer cleared."
|
||||
// Reference: https://stackoverflow.com/a/7386854
|
||||
public MyInputConnection() {
|
||||
super(EditableSurfaceView.this, false);
|
||||
}
|
||||
|
||||
// Bug fixes for backspace behavior in TYPE_NULL input type
|
||||
// ref: https://stackoverflow.com/a/19980975
|
||||
// This holds the Editable text buffer that the LatinIME mistakenly *thinks*
|
||||
// that it is editing, even though the views that employ this class are
|
||||
// completely driven by key events.
|
||||
Editable _myEditable = null;
|
||||
|
||||
//This method is called by the IME whenever the view that returned an
|
||||
// instance of this class to the IME from its onCreateInputConnection()
|
||||
// gains focus.
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
// Some versions of the Google Keyboard (LatinIME) were delivered with a
|
||||
// bug that causes KEYCODE_DEL to no longer be generated once the number
|
||||
// of KEYCODE_DEL taps equals the number of other characters that have
|
||||
// been typed. This bug was reported here as issue 62306.
|
||||
//
|
||||
// As of this writing (1/7/2014), it is fixed in the AOSP code, but that
|
||||
// fix has not yet been released. Even when it is released, there will
|
||||
// be many devices having versions of the Google Keyboard that include the bug
|
||||
// in the wild for the indefinite future. Therefore, a workaround is required.
|
||||
//
|
||||
// This is a workaround for that bug which just jams a single garbage character
|
||||
// into the internal buffer that the keyboard THINKS it is editing even
|
||||
// though we have specified TYPE_NULL which *should* cause LatinIME to
|
||||
// generate key events regardless of what is in that buffer. We have other
|
||||
// code that attempts to ensure as the user edites that there is always
|
||||
// one character remaining.
|
||||
//
|
||||
// The problem arises because when this unseen buffer becomes empty, the IME
|
||||
// thinks that there is nothing left to delete, and therefore stops
|
||||
// generating KEYCODE_DEL events, even though the app may still be very
|
||||
// interested in receiving them.
|
||||
//
|
||||
// So, for example, if the user taps in ABCDE and then positions the
|
||||
// (app-based) cursor to the left of A and taps the backspace key three
|
||||
// times without any evident effect on the letters (because the app's own
|
||||
// UI code knows that there are no letters to the left of the
|
||||
// app-implemented cursor), and then moves the cursor to the right of the
|
||||
// E and hits backspace five times, then, after E and D have been deleted,
|
||||
// no more KEYCODE_DEL events will be generated by the IME because the
|
||||
// unseen buffer will have become empty from five letter key taps followed
|
||||
// by five backspace key taps (as the IME is unaware of the app-based cursor
|
||||
// movements performed by the user).
|
||||
//
|
||||
// In other words, if your app is processing KEYDOWN events itself, and
|
||||
// maintaining its own cursor and so on, and not telling the IME anything
|
||||
// about the user's cursor position, this buggy processing of the hidden
|
||||
// buffer will stop KEYCODE_DEL events when your app actually needs them -
|
||||
// in whatever Android releases incorporate this LatinIME bug.
|
||||
//
|
||||
// By creating this garbage characters in the Editable that is initially
|
||||
// returned to the IME here, we make the IME think that it still has
|
||||
// something to delete, which causes it to keep generating KEYCODE_DEL
|
||||
// events in response to backspace key presses.
|
||||
//
|
||||
// A specific keyboard version that I tested this on which HAS this
|
||||
// problem but does NOT have the "KEYCODE_DEL completely gone" (issue 42904)
|
||||
// problem that is addressed by the deleteSurroundingText() override below
|
||||
// (the two problems are not both present in a single version) is
|
||||
// 2.0.19123.914326a, tested running on a Nexus7 2012 tablet.
|
||||
// There may be other versions that have issue 62306.
|
||||
//
|
||||
// A specific keyboard version that I tested this on which does NOT have
|
||||
// this problem but DOES have the "KEYCODE_DEL completely gone" (issue
|
||||
// 42904) problem that is addressed by the deleteSurroundingText()
|
||||
// override below is 1.0.1800.776638, tested running on the Nexus10
|
||||
// tablet. There may be other versions that also have issue 42904.
|
||||
//
|
||||
// The bug that this addresses was first introduced as of AOSP commit tag
|
||||
// 4.4_r0.9, and the next RELEASED Android version after that was
|
||||
// android-4.4_r1, which is the first release of Android 4.4. So, 4.4 will
|
||||
// be the first Android version that would have included, in the original
|
||||
// RELEASED version, a Google Keyboard for which this bug was present.
|
||||
//
|
||||
// Note that this bug was introduced exactly at the point that the OTHER bug
|
||||
// (the one that is addressed in deleteSurroundingText(), below) was first
|
||||
// FIXED.
|
||||
//
|
||||
// Despite the fact that the above are the RELEASES associated with the bug,
|
||||
// the fact is that any 4.x Android release could have been upgraded by the
|
||||
// user to a later version of Google Keyboard than was present when the
|
||||
// release was originally installed to the device. I have checked the
|
||||
// www.archive.org snapshots of the Google Keyboard listing page on the Google
|
||||
// Play store, and all released updates listed there (which go back to early
|
||||
// June of 2013) required Android 4.0 and up, so we can be pretty sure that
|
||||
// this bug is not present in any version earlier than 4.0 (ICS), which means
|
||||
// that we can limit this fix to API level 14 and up. And once the LatinIME
|
||||
// problem is fixed, we can limit the scope of this workaround to end as of
|
||||
// the last release that included the problem, since we can assume that
|
||||
// users will not upgrade Google Keyboard to an EARLIER version than was
|
||||
// originally included in their Android release.
|
||||
//
|
||||
// The bug that this addresses was FIXED but NOT RELEASED as of this AOSP
|
||||
// commit:
|
||||
//https://android.googlesource.com/platform/packages/inputmethods/LatinIME/+
|
||||
// /b41bea65502ce7339665859d3c2c81b4a29194e4/java/src/com/android
|
||||
// /inputmethod/latin/LatinIME.java
|
||||
// so it can be assumed to affect all of KitKat released thus far
|
||||
// (up to 4.4.2), and could even affect beyond KitKat, although I fully
|
||||
// expect it to be incorporated into the next release *after* API level 19.
|
||||
//
|
||||
// When it IS released, this method should be changed to limit it to no
|
||||
// higher than API level 19 (assuming that the fix is released before API
|
||||
// level 20), just in order to limit the scope of this fix, since poking
|
||||
// 1024 characters into the Editable object returned here is of course a
|
||||
// kluge. But right now the safest thing is just to not have an upper limit
|
||||
// on the application of this kluge, since the fix for the problem it
|
||||
// addresses has not yet been released (as of 1/7/2014).
|
||||
if(Build.VERSION.SDK_INT >= 14) {
|
||||
if (_myEditable == null) {
|
||||
_myEditable = new EditableAccommodatingLatinIMETypeNullIssues(EditableAccommodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER);
|
||||
Selection.setSelection(_myEditable, 1);
|
||||
} else {
|
||||
int _myEditableLength = _myEditable.length();
|
||||
if (_myEditableLength == 0) {
|
||||
// I actually HAVE seen this be zero on the Nexus 10 with the keyboard
|
||||
// that came with Android 4.4.2
|
||||
// On the Nexus 10 4.4.2 if I tapped away from the view and then back to it, the
|
||||
// _myEditable would come back as null and I would create a new one. This is also
|
||||
// what happens on other devices (e.g., the Nexus 6 with 4.4.2,
|
||||
// which has a slightly later version of the Google Keyboard). But for the
|
||||
// Nexus 10 4.4.2, the keyboard had a strange behavior
|
||||
// when I tapped on the rack, and then tapped Done on the keyboard to close it,
|
||||
// and then tapped on the rack AGAIN. In THAT situation,
|
||||
// the _myEditable would NOT be set to NULL but its LENGTH would be ZERO. So, I
|
||||
// just append to it in that situation.
|
||||
_myEditable.append(EditableAccommodatingLatinIMETypeNullIssues.ONE_UNPROCESSED_CHARACTER);
|
||||
Selection.setSelection(_myEditable, 1);
|
||||
}
|
||||
}
|
||||
return _myEditable;
|
||||
}
|
||||
else {
|
||||
//Default behavior for keyboards that do not require any fix
|
||||
return super.getEditable();
|
||||
}
|
||||
}
|
||||
|
||||
// This method is called INSTEAD of generating a KEYCODE_DEL event, by
|
||||
// versions of Latin IME that have the bug described in Issue 42904.
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
// If targetSdkVersion is set to anything AT or ABOVE API level 16
|
||||
// then for the GOOGLE KEYBOARD versions DELIVERED
|
||||
// with Android 4.1.x, 4.2.x or 4.3.x, NO KEYCODE_DEL EVENTS WILL BE
|
||||
// GENERATED BY THE GOOGLE KEYBOARD (LatinIME) EVEN when TYPE_NULL
|
||||
// is being returned as the InputType by your view from its
|
||||
// onCreateInputMethod() override, due to a BUG in THOSE VERSIONS.
|
||||
//
|
||||
// When TYPE_NULL is specified (as this entire class assumes is being done
|
||||
// by the views that use it, what WILL be generated INSTEAD of a KEYCODE_DEL
|
||||
// is a deleteSurroundingText(1,0) call. So, by overriding this
|
||||
// deleteSurroundingText() method, we can fire the KEYDOWN/KEYUP events
|
||||
// ourselves for KEYCODE_DEL. This provides a workaround for the bug.
|
||||
//
|
||||
// The specific AOSP RELEASES involved are 4.1.1_r1 (the very first 4.1
|
||||
// release) through 4.4_r0.8 (the release just prior to Android 4.4).
|
||||
// This means that all of KitKat should not have the bug and will not
|
||||
// need this workaround.
|
||||
//
|
||||
// Although 4.0.x (ICS) did not have this bug, it was possible to install
|
||||
// later versions of the keyboard as an app on anything running 4.0 and up,
|
||||
// so those versions are also potentially affected.
|
||||
//
|
||||
// The first version of separately-installable Google Keyboard shown on the
|
||||
// Google Play store site by www.archive.org is Version 1.0.1869.683049,
|
||||
// on June 6, 2013, and that version (and probably other, later ones)
|
||||
// already had this bug.
|
||||
//
|
||||
//Since this required at least 4.0 to install, I believe that the bug will
|
||||
// not be present on devices running versions of Android earlier than 4.0.
|
||||
//
|
||||
// AND, it should not be present on versions of Android at 4.4 and higher,
|
||||
// since users will not "upgrade" to a version of Google Keyboard that
|
||||
// is LOWER than the one they got installed with their version of Android
|
||||
// in the first place, and the bug will have been fixed as of the 4.4 release.
|
||||
//
|
||||
// The above scope of the bug is reflected in the test below, which limits
|
||||
// the application of the workaround to Android versions between 4.0.x and 4.3.x.
|
||||
//
|
||||
// UPDATE: A popular third party keyboard was found that exhibits this same issue. It
|
||||
// was not fixed at the same time as the Google Play keyboard, and so the bug in that case
|
||||
// is still in place beyond API LEVEL 19. So, even though the Google Keyboard fixed this
|
||||
// as of level 19, we cannot take out the fix based on that version number. And so I've
|
||||
// removed the test for an upper limit on the version; the fix will remain in place ad
|
||||
// infinitum - but only when TYPE_NULL is used, so it *should* be harmless even when
|
||||
// the keyboard does not have the problem...
|
||||
if ((Build.VERSION.SDK_INT >= 14) // && (Build.VERSION.SDK_INT < 19)
|
||||
&& (beforeLength == 1 && afterLength == 0)) {
|
||||
//Send Backspace key down and up events to replace the ones omitted
|
||||
// by the LatinIME keyboard.
|
||||
return super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
|
||||
&& super.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
|
||||
} else {
|
||||
//Really, I can't see how this would be invoked, given that we're using
|
||||
// TYPE_NULL, for non-buggy versions, but in order to limit the impact
|
||||
// of this change as much as possible (i.e., to versions at and above 4.0)
|
||||
// I am using the original behavior here for non-affected versions.
|
||||
return super.deleteSurroundingText(beforeLength, afterLength);
|
||||
}
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public boolean performEditorAction(int actionCode) {
|
||||
// if (actionCode == EditorInfo.IME_ACTION_DONE) {
|
||||
// InputMethodManager imm = (InputMethodManager)
|
||||
// getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
// imm.hideSoftInputFromWindow(getWindowToken(), 0);
|
||||
// }
|
||||
//
|
||||
// // Sends enter key
|
||||
// return super.performEditorAction(actionCode);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
outAttrs.initialCapsMode = 0;
|
||||
outAttrs.initialSelEnd = outAttrs.initialSelStart = -1;
|
||||
outAttrs.actionLabel = null;
|
||||
|
||||
// Per the documentation for InputType.TYPE_NULL:
|
||||
// "This should be interpreted to mean that the target input connection is not rich,
|
||||
// it can not process and show things like candidate text nor retrieve the current text,
|
||||
// so the input method will need to run in a limited 'generate key events' mode."
|
||||
// Reference: https://stackoverflow.com/a/7386854
|
||||
// We lose auto-complete, but that is ok, because we *really* want direct input key handling
|
||||
outAttrs.inputType = InputType.TYPE_NULL;
|
||||
|
||||
// IME_FLAG_NO_EXTRACT_UI used to specify that the IME does not need to show its extracted text UI. Extract UI means the fullscreen editing mode.
|
||||
// IME_ACTION_NONE Bits of IME_MASK_ACTION: there is no available action.
|
||||
// TODO should we have a IME_ACTION_DONE?
|
||||
outAttrs.imeOptions = (EditorInfo.IME_ACTION_NONE |
|
||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||
|
||||
return new MyInputConnection();
|
||||
}
|
||||
|
||||
public void showSystemMouseCursor(boolean show) {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::showSystemMouseCursor2 " + show);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Android N (Nougat) is Android 7.0
|
||||
//SurfaceView main_surface = findViewById(R.id.main_surface);
|
||||
int type = show ? PointerIcon.TYPE_ARROW : PointerIcon.TYPE_NULL;
|
||||
// https://stackoverflow.com/a/55482761
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::showSystemMouseCursor3a");
|
||||
setPointerIcon(PointerIcon.getSystemIcon(_context, type));
|
||||
} else {
|
||||
/* Currently hiding the system mouse cursor is only
|
||||
supported on OUYA. If other systems provide similar
|
||||
intents, please add them here as well */
|
||||
Intent intent =
|
||||
new Intent(show?
|
||||
"tv.ouya.controller.action.SHOW_CURSOR" :
|
||||
"tv.ouya.controller.action.HIDE_CURSOR");
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::showSystemMouseCursor3b");
|
||||
_context.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
// This re-inforces the code for hiding the system mouse.
|
||||
// We already had code for this in ScummVMActivity (see showSystemMouseCursor())
|
||||
// so this might be redundant
|
||||
//
|
||||
// It applies on devices running Android 7 and above
|
||||
// https://stackoverflow.com/a/55482761
|
||||
// https://developer.android.com/reference/android/view/PointerIcon.html
|
||||
//
|
||||
@TargetApi(24)
|
||||
@Override
|
||||
public PointerIcon onResolvePointerIcon(MotionEvent me, int pointerIndex) {
|
||||
if (_allowHideSystemMousePointer) {
|
||||
if (_mouseIsInCapturedState) {
|
||||
return PointerIcon.getSystemIcon(_context, PointerIcon.TYPE_NULL);
|
||||
} else {
|
||||
return PointerIcon.getSystemIcon(_context, PointerIcon.TYPE_ARROW);
|
||||
}
|
||||
} else {
|
||||
return PointerIcon.getSystemIcon(_context, PointerIcon.TYPE_ARROW);
|
||||
}
|
||||
}
|
||||
|
||||
public void captureMouse(final boolean capture) {
|
||||
|
||||
if ((!_mouseIsInCapturedState && ((ScummVMActivity)_context).isKeyboardOverlayShown() && capture)) {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::returned - keyboard is shown");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((capture && _mouseIsInCapturedState) ||
|
||||
(!capture && ! _mouseIsInCapturedState)) {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::returned - nothing to do");
|
||||
return;
|
||||
}
|
||||
|
||||
if (capture) {
|
||||
// setFocusableInTouchMode(true);
|
||||
// setFocusable(true);
|
||||
// requestFocus();
|
||||
_mouseIsInCapturedState = true;
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::_mouseIsInCapturedState");
|
||||
} else {
|
||||
//Log.d(ScummVM.LOG_TAG, "captureMouse::no _mouseIsInCapturedState");
|
||||
_mouseIsInCapturedState = false;
|
||||
}
|
||||
|
||||
showSystemMouseCursor(!capture);
|
||||
|
||||
// trying capturing the pointer resulted in firing TrackBallEvent instead of HoverEvent for our SurfaceView
|
||||
// also the behavior was inconsistent when resuming to the app from a pause
|
||||
// if (_allowHideSystemMousePointer) {
|
||||
// // for API 26 and above
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// Log.d(ScummVM.LOG_TAG, "captureMouse::CODES 0");
|
||||
// if (capture) {
|
||||
// postDelayed( new Runnable() {
|
||||
// public void run()
|
||||
// {
|
||||
// Log.v(ScummVM.LOG_TAG, "captureMouse::requestPointerCapture() delayed");
|
||||
//// if (!hasPointerCapture()) {
|
||||
// requestPointerCapture();
|
||||
//// showSystemMouseCursor(!capture);
|
||||
//// }
|
||||
// }
|
||||
// }, 50 );
|
||||
// } else {
|
||||
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
// postDelayed(new Runnable() {
|
||||
// public void run() {
|
||||
// Log.v(ScummVM.LOG_TAG, "captureMouse::releasePointerCapture()");
|
||||
//// if (hasPointerCapture()) {
|
||||
// releasePointerCapture();
|
||||
//// showSystemMouseCursor(!capture);
|
||||
//// }
|
||||
// }
|
||||
// }, 50);
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// Log.d(ScummVM.LOG_TAG, "captureMouse::NO CODES 0 showSystemMouseCursor " + !capture);
|
||||
// showSystemMouseCursor(!capture);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Scanner;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
* Contains helper methods to get list of available media
|
||||
*/
|
||||
public class ExternalStorage {
|
||||
public static final String SD_CARD = "sdCard";
|
||||
public static final String EXTERNAL_SD_CARD = "externalSdCard";
|
||||
|
||||
// User data is an system wide folder (typically, but maybe not always, "/data/") where apps can store data in their own subfolder.
|
||||
// While this folder does exists in newer Android OS versions it is most likely not *directly* usable.
|
||||
public static final String DATA_DIRECTORY = "User data (System Wide)";
|
||||
|
||||
// Internal App Data folder is a folder that is guaranteed to always be available for access.
|
||||
// Only the app (here ScummVM) can write and read under this folder.
|
||||
// It is used to store configuration file(s), log file(s), the default saved games folder, the default icons folder, and distribution data files.
|
||||
// The folder's contents are kept kept upon upgrading to a compatible newer version of the app.
|
||||
// It is wiped when downgrading, uninstalling or explicitly cleaning the application's data from the Android System Apps menu options.
|
||||
// The storage for this folder is assigned from internal device storage (ie. not external physical SD Card).
|
||||
public static final String DATA_DIRECTORY_INT = "ScummVM data (Internal)";
|
||||
|
||||
// External App Data folder is a folder that is NOT guaranteed to always be available for access.
|
||||
// Only the app (here ScummVM) can write under this folder, but other apps have read access to the folder's contents.
|
||||
// The folder's contents are kept upon upgrading to a compatible newer version of the app.
|
||||
// It is wiped when downgrading, uninstalling or explicitly cleaning the application's data from the Android System Apps menu options.
|
||||
public static final String DATA_DIRECTORY_EXT = "ScummVM data (External)";
|
||||
|
||||
// Find candidate removable sd card paths
|
||||
// Code reference: https://stackoverflow.com/a/54411385
|
||||
private static final String ANDROID_DIR = File.separator + "Android";
|
||||
|
||||
private static String ancestor(File dir) {
|
||||
// getExternalFilesDir() and getExternalStorageDirectory()
|
||||
// may return something app-specific like:
|
||||
// /storage/sdcard1/Android/data/com.mybackuparchives.android/files
|
||||
// so we want the great-great-grandparent folder.
|
||||
if (dir == null) {
|
||||
return null;
|
||||
} else {
|
||||
String path = dir.getAbsolutePath();
|
||||
int i = path.indexOf(ANDROID_DIR);
|
||||
if (i == -1) {
|
||||
return path;
|
||||
} else {
|
||||
return path.substring(0, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern that SD card device should match
|
||||
private static final Pattern
|
||||
devicePattern = Pattern.compile("/dev/(block/.*vold.*|fuse)|/mnt/.*");
|
||||
// Pattern that SD card mount path should match
|
||||
private static final Pattern
|
||||
pathPattern = Pattern.compile("/(mnt|storage|external_sd|extsd|_ExternalSD|Removable|.*MicroSD).*", Pattern.CASE_INSENSITIVE);
|
||||
// Pattern that the mount path should not match.
|
||||
//' emulated' indicates an internal storage location, so skip it.
|
||||
// 'asec' is an encrypted package file, decrypted and mounted as a directory.
|
||||
private static final Pattern
|
||||
pathAntiPattern = Pattern.compile(".*(/secure|/asec|/emulated).*");
|
||||
// These are expected fs types, including vfat. tmpfs is not OK.
|
||||
// fuse can be removable SD card (as on Moto E or Asus ZenPad), or can be internal (Huawei G610).
|
||||
private static final Pattern
|
||||
fsTypePattern = Pattern.compile(".*(fat|msdos|ntfs|ext[34]|fuse|sdcard|esdfs).*");
|
||||
|
||||
/** Common paths for microSD card. **/
|
||||
private static final String[] commonPaths = {
|
||||
// Some of these taken from
|
||||
// https://stackoverflow.com/questions/13976982/removable-storage-external-sdcard-path-by-manufacturers
|
||||
// These are roughly in order such that the earlier ones, if they exist, are more sure
|
||||
// to be removable storage than the later ones.
|
||||
"/mnt/Removable/MicroSD",
|
||||
"/storage/removable/" + SD_CARD + "1", // !< Sony Xperia Z1
|
||||
"/Removable/MicroSD", // Asus ZenPad C
|
||||
"/removable/microsd",
|
||||
"/external_sd", // Samsung
|
||||
"/_ExternalSD", // some LGs
|
||||
"/storage/extSdCard", // later Samsung
|
||||
"/storage/extsdcard", // Main filesystem is case-sensitive; FAT isn't.
|
||||
"/mnt/extsd", // some Chinese tablets, e.g. Zeki
|
||||
"/storage/" + SD_CARD + "1", // If this exists it's more likely than sdcard0 to be removable.
|
||||
"/mnt/extSdCard",
|
||||
"/mnt/" + SD_CARD + "/external_sd",
|
||||
"/mnt/external_sd",
|
||||
"/storage/external_SD",
|
||||
"/storage/ext_sd", // HTC One Max
|
||||
"/mnt/" + SD_CARD + "/_ExternalSD",
|
||||
"/mnt/" + SD_CARD + "-ext",
|
||||
|
||||
"/" + SD_CARD + "2", // HTC One M8s
|
||||
"/" + SD_CARD + "1", // Sony Xperia Z
|
||||
"/mnt/media_rw/" + SD_CARD + "1", // 4.4.2 on CyanogenMod S3
|
||||
"/mnt/" + SD_CARD, // This can be built-in storage (non-removable).
|
||||
"/" + SD_CARD,
|
||||
"/storage/" + SD_CARD +"0",
|
||||
"/emmc",
|
||||
"/mnt/emmc",
|
||||
"/" + SD_CARD + "/sd",
|
||||
"/mnt/" + SD_CARD + "/bpemmctest",
|
||||
"/mnt/external1",
|
||||
"/data/sdext4",
|
||||
"/data/sdext3",
|
||||
"/data/sdext2",
|
||||
"/data/sdext",
|
||||
"/storage/microsd" //ASUS ZenFone 2
|
||||
|
||||
// If we ever decide to support USB OTG storage, the following paths could be helpful:
|
||||
// An LG Nexus 5 apparently uses usb://1002/UsbStorage/ as a URI to access an SD
|
||||
// card over OTG cable. Other models, like Galaxy S5, use /storage/UsbDriveA
|
||||
// "/mnt/usb_storage",
|
||||
// "/mnt/UsbDriveA",
|
||||
// "/mnt/UsbDriveB",
|
||||
};
|
||||
|
||||
/** Find path to removable SD card. */
|
||||
public static LinkedHashSet<File> findSdCardPath() {
|
||||
String[] mountFields;
|
||||
BufferedReader bufferedReader = null;
|
||||
String lineRead;
|
||||
|
||||
// Possible SD card paths
|
||||
LinkedHashSet<File> candidatePaths = new LinkedHashSet<>();
|
||||
|
||||
// Build a list of candidate paths, roughly in order of preference. That way if
|
||||
// we can't definitively detect removable storage, we at least can pick a more likely
|
||||
// candidate.
|
||||
|
||||
// Could do: use getExternalStorageState(File path), with and without an argument, when
|
||||
// available. With an argument is available since API level 21.
|
||||
// This may not be necessary, since we also check whether a directory exists and has contents,
|
||||
// which would fail if the external storage state is neither MOUNTED nor MOUNTED_READ_ONLY.
|
||||
|
||||
// I moved hard-coded paths toward the end, but we need to make sure we put the ones in
|
||||
// backwards order that are returned by the OS. And make sure the iterators respect
|
||||
// the order!
|
||||
// This is because when multiple "external" storage paths are returned, it's always (in
|
||||
// experience, but not guaranteed by documentation) with internal/emulated storage
|
||||
// first, removable storage second.
|
||||
|
||||
// Add value of environment variables as candidates, if set:
|
||||
// EXTERNAL_STORAGE, SECONDARY_STORAGE, EXTERNAL_SDCARD_STORAGE
|
||||
// But note they are *not* necessarily *removable* storage! Especially EXTERNAL_STORAGE.
|
||||
// And they are not documented (API) features. Typically useful only for old versions of Android.
|
||||
|
||||
String val = System.getenv("SECONDARY_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
val = System.getenv("EXTERNAL_SDCARD_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
// Get listing of mounted devices with their properties.
|
||||
ArrayList<File> mountedPaths = new ArrayList<>();
|
||||
try {
|
||||
// Note: Despite restricting some access to /proc (https://stackoverflow.com/a/38728738/423105),
|
||||
// Android 7.0 does *not* block access to /proc/mounts, according to our test on George's Alcatel A30 GSM.
|
||||
bufferedReader = new BufferedReader(new FileReader("/proc/mounts"));
|
||||
|
||||
// Iterate over each line of the mounts listing.
|
||||
while ((lineRead = bufferedReader.readLine()) != null) {
|
||||
// Log.d(ScummVM.LOG_TAG, "\nMounts line: " + lineRead);
|
||||
mountFields = lineRead.split(" ");
|
||||
|
||||
// columns: device, mountpoint, fs type, options... Example:
|
||||
// /dev/block/vold/179:97 /storage/sdcard1 vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0002,dmask=0002,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
|
||||
String device = mountFields[0], path = mountFields[1], fsType = mountFields[2];
|
||||
|
||||
// The device, path, and fs type must conform to expected patterns.
|
||||
// mtdblock is internal, I'm told.
|
||||
// Check for disqualifying patterns in the path.
|
||||
// If this mounts line fails our tests, skip it.
|
||||
if (!(devicePattern.matcher(device).matches()
|
||||
&& pathPattern.matcher(path).matches()
|
||||
&& fsTypePattern.matcher(fsType).matches())
|
||||
|| device.contains("mtdblock")
|
||||
|| pathAntiPattern.matcher(path).matches()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO maybe: check options to make sure it's mounted RW?
|
||||
// The answer at https://stackoverflow.com/a/13648873/423105 does.
|
||||
// But it hasn't seemed to be necessary so far in my testing.
|
||||
|
||||
// This line met the criteria so far, so add it to candidate list.
|
||||
addPath(path, mountedPaths);
|
||||
}
|
||||
} catch (IOException ignored) { }
|
||||
finally {
|
||||
if (bufferedReader != null) {
|
||||
try {
|
||||
bufferedReader.close();
|
||||
} catch (IOException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
// Append the paths from mount table to candidate list, in reverse order.
|
||||
if (!mountedPaths.isEmpty()) {
|
||||
// See https://stackoverflow.com/a/5374346/423105 on why the following is necessary.
|
||||
// Basically, .toArray() needs its parameter to know what type of array to return.
|
||||
File[] mountedPathsArray = mountedPaths.toArray(new File[0]);
|
||||
addAncestors(mountedPathsArray, candidatePaths);
|
||||
}
|
||||
|
||||
// Add hard-coded known common paths to candidate list:
|
||||
addStrings(commonPaths, candidatePaths);
|
||||
|
||||
// If the above doesn't work we could try the following other options, but in my experience they
|
||||
// haven't added anything helpful yet.
|
||||
|
||||
// getExternalFilesDir() and getExternalStorageDirectory() typically something app-specific like
|
||||
// /storage/sdcard1/Android/data/com.mybackuparchives.android/files
|
||||
// so we want the great-great-grandparent folder.
|
||||
|
||||
// TODO Note, This method was deprecated in API level 29.
|
||||
// To improve user privacy, direct access to shared/external storage devices is deprecated.
|
||||
// When an app targets Build.VERSION_CODES.Q, the path returned from this method is no longer directly accessible to apps.
|
||||
// Apps can continue to access content stored on shared/external storage by migrating to
|
||||
// alternatives such as Context#getExternalFilesDir(String), MediaStore, or Intent#ACTION_OPEN_DOCUMENT.
|
||||
//
|
||||
// This may be non-removable.
|
||||
Log.d(ScummVM.LOG_TAG, "Environment.getExternalStorageDirectory():");
|
||||
addPath(ancestor(Environment.getExternalStorageDirectory()), candidatePaths);
|
||||
|
||||
// TODO maybe use getExternalStorageState(File path), with and without an argument,
|
||||
// when available. With an argument is available since API level 21.
|
||||
// This may not be necessary, since we also check whether a directory exists,
|
||||
// which would fail if the external storage state is neither MOUNTED nor MOUNTED_READ_ONLY.
|
||||
|
||||
// A "public" external storage directory. But in my experience it doesn't add anything helpful.
|
||||
// Note that you can't pass null, or you'll get an NPE.
|
||||
final File publicDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC);
|
||||
// Take the parent, because we tend to get a path like /pathTo/sdCard/Music.
|
||||
if (publicDirectory.getParentFile() != null) {
|
||||
addPath(publicDirectory.getParentFile().getAbsolutePath(), candidatePaths);
|
||||
}
|
||||
|
||||
// EXTERNAL_STORAGE: may not be removable.
|
||||
val = System.getenv("EXTERNAL_STORAGE");
|
||||
if (!TextUtils.isEmpty(val)) {
|
||||
addPath(val, candidatePaths);
|
||||
}
|
||||
|
||||
if (candidatePaths.isEmpty()) {
|
||||
Log.w(ScummVM.LOG_TAG, "No removable microSD card found.");
|
||||
return candidatePaths;
|
||||
} else {
|
||||
Log.i(ScummVM.LOG_TAG, "\nFound potential removable storage locations: " + candidatePaths);
|
||||
}
|
||||
|
||||
// Accept or eliminate candidate paths if we can determine whether they're removable storage.
|
||||
// In Lollipop and later, we can check isExternalStorageRemovable() status on each candidate.
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
Iterator<File> itf = candidatePaths.iterator();
|
||||
while (itf.hasNext()) {
|
||||
File dir = itf.next();
|
||||
// handle illegalArgumentException if the path is not a valid storage device.
|
||||
try {
|
||||
if (Environment.isExternalStorageRemovable(dir)) {
|
||||
Log.i(ScummVM.LOG_TAG, dir.getPath() + " is removable external storage");
|
||||
addPath(dir.getAbsolutePath(), candidatePaths);
|
||||
} else if (Environment.isExternalStorageEmulated(dir)) {
|
||||
Log.d(ScummVM.LOG_TAG, "Removing emulated external storage dir " + dir);
|
||||
itf.remove();
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.d(ScummVM.LOG_TAG, "isRemovable(" + dir.getPath() + "): not a valid storage device.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue trying to accept or eliminate candidate paths based on whether they're removable storage.
|
||||
// On pre-Lollipop, we only have singular externalStorage. Check whether it's removable.
|
||||
if (Build.VERSION.SDK_INT >= 9) {
|
||||
File externalStorage = Environment.getExternalStorageDirectory();
|
||||
Log.d(ScummVM.LOG_TAG, String.format(Locale.ROOT, "findSDCardPath: getExternalStorageDirectory = %s", externalStorage.getPath()));
|
||||
if (Environment.isExternalStorageRemovable()) {
|
||||
// Make sure this is a candidate.
|
||||
// TODO: Does this contains() work? Should we be canonicalizing paths before comparing?
|
||||
if (candidatePaths.contains(externalStorage)) {
|
||||
Log.d(ScummVM.LOG_TAG, "Using externalStorage dir " + externalStorage);
|
||||
// return externalStorage;
|
||||
addPath(externalStorage.getAbsolutePath(), candidatePaths);
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 11 && Environment.isExternalStorageEmulated()) {
|
||||
Log.d(ScummVM.LOG_TAG, "Removing emulated external storage dir " + externalStorage);
|
||||
candidatePaths.remove(externalStorage);
|
||||
}
|
||||
}
|
||||
|
||||
return candidatePaths;
|
||||
}
|
||||
|
||||
|
||||
/** Add each path to the collection. */
|
||||
private static void addStrings(String[] newPaths, LinkedHashSet<File> candidatePaths) {
|
||||
for (String path : newPaths) {
|
||||
addPath(path, candidatePaths);
|
||||
}
|
||||
}
|
||||
|
||||
/** Add ancestor of each File to the collection. */
|
||||
private static void addAncestors(File[] files, LinkedHashSet<File> candidatePaths) {
|
||||
for (int i = files.length - 1; i >= 0; i--) {
|
||||
addPath(ancestor(files[i]), candidatePaths);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new candidate directory path to our list, if it's not obviously wrong.
|
||||
* Supply path as either String or File object.
|
||||
* @param strNew path of directory to add
|
||||
*/
|
||||
private static void addPath(String strNew, Collection<File> paths) {
|
||||
// If one of the arguments is null, fill it in from the other.
|
||||
if (!TextUtils.isEmpty(strNew)) {
|
||||
File fileNew = new File(strNew);
|
||||
|
||||
if (!paths.contains(fileNew) &&
|
||||
// Check for paths known not to be removable SD card.
|
||||
// The antipattern check can be redundant, depending on where this is called from.
|
||||
!pathAntiPattern.matcher(strNew).matches()) {
|
||||
|
||||
// Eliminate candidate if not a directory or not fully accessible.
|
||||
if (fileNew.exists() && fileNew.isDirectory() && fileNew.canExecute()) {
|
||||
Log.d(ScummVM.LOG_TAG, " Adding candidate path " + strNew);
|
||||
paths.add(fileNew);
|
||||
} else {
|
||||
Log.d(ScummVM.LOG_TAG, String.format(Locale.ROOT, " Invalid path %s: exists: %b isDir: %b canExec: %b canRead: %b",
|
||||
strNew, fileNew.exists(), fileNew.isDirectory(), fileNew.canExecute(), fileNew.canRead()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return True if the external storage is available. False otherwise.
|
||||
*/
|
||||
public static boolean isAvailable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
|
||||
}
|
||||
|
||||
public static String getSdCardPath() {
|
||||
return Environment.getExternalStorageDirectory().getPath() + "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the external storage is writable. False otherwise.
|
||||
*/
|
||||
public static boolean isWritable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
return Environment.MEDIA_MOUNTED.equals(state);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list of locations available. Odd elements are names, even are paths
|
||||
*/
|
||||
public static List<String> getAllStorageLocations(Context ctx) {
|
||||
List<String> map = new ArrayList<>(20);
|
||||
|
||||
List<String> mMounts = new ArrayList<>(10);
|
||||
List<String> mVold = new ArrayList<>(10);
|
||||
mMounts.add("/mnt/sdcard");
|
||||
mVold.add("/mnt/sdcard");
|
||||
|
||||
try {
|
||||
File mountFile = new File("/proc/mounts");
|
||||
if (mountFile.exists()) {
|
||||
Scanner scanner = new Scanner(mountFile);
|
||||
while (scanner.hasNext()) {
|
||||
String line = scanner.nextLine();
|
||||
if (line.startsWith("/dev/block/vold/")) {
|
||||
String[] lineElements = line.split(" ");
|
||||
String element = lineElements[1];
|
||||
|
||||
// don't add the default mount path
|
||||
// it's already in the list.
|
||||
if (!element.equals("/mnt/sdcard"))
|
||||
mMounts.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
File voldFile = new File("/system/etc/vold.fstab");
|
||||
if (voldFile.exists()){
|
||||
Scanner scanner = new Scanner(voldFile);
|
||||
while (scanner.hasNext()) {
|
||||
String line = scanner.nextLine();
|
||||
if (line.startsWith("dev_mount")) {
|
||||
String[] lineElements = line.split(" ");
|
||||
String element = lineElements[2];
|
||||
|
||||
if (element.contains(":"))
|
||||
element = element.substring(0, element.indexOf(":"));
|
||||
if (!element.equals("/mnt/sdcard"))
|
||||
mVold.add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < mMounts.size(); i++) {
|
||||
String mount = mMounts.get(i);
|
||||
if (!mVold.contains(mount))
|
||||
mMounts.remove(i--);
|
||||
}
|
||||
mVold.clear();
|
||||
|
||||
List<String> mountHash = new ArrayList<>(10);
|
||||
|
||||
for (String mount : mMounts) {
|
||||
File root = new File(mount);
|
||||
if (root.exists() && root.isDirectory() && root.canRead()) {
|
||||
File[] list = root.listFiles();
|
||||
StringBuilder hash = new StringBuilder("[");
|
||||
if (list != null) {
|
||||
for (File f : list) {
|
||||
hash.append(f.getName().hashCode()).append(":").append(f.length()).append(", ");
|
||||
}
|
||||
}
|
||||
hash.append("]");
|
||||
if (!mountHash.contains(hash.toString())) {
|
||||
String key = SD_CARD + "_" + (map.size() / 2);
|
||||
if (map.size() == 0) {
|
||||
key = SD_CARD;
|
||||
} else if (map.size() == 2) {
|
||||
key = EXTERNAL_SD_CARD;
|
||||
}
|
||||
mountHash.add(hash.toString());
|
||||
map.add(key);
|
||||
map.add(root.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mMounts.clear();
|
||||
|
||||
if (Environment.getDataDirectory() != null
|
||||
&& !TextUtils.isEmpty(Environment.getDataDirectory().getAbsolutePath())) {
|
||||
File dataFilePath = new File(Environment.getDataDirectory().getAbsolutePath());
|
||||
if (dataFilePath.exists() && dataFilePath.isDirectory()) {
|
||||
map.add(DATA_DIRECTORY);
|
||||
map.add(Environment.getDataDirectory().getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
map.add(DATA_DIRECTORY_INT);
|
||||
map.add(ctx.getFilesDir().getPath());
|
||||
|
||||
if (ctx.getExternalFilesDir(null) != null) {
|
||||
map.add(DATA_DIRECTORY_EXT);
|
||||
map.add(ctx.getExternalFilesDir(null).getPath());
|
||||
}
|
||||
|
||||
// Now go through the external storage
|
||||
if (isAvailable()) { // we can read the External Storage...
|
||||
// Retrieve the primary External Storage:
|
||||
File primaryExternalStorage = Environment.getExternalStorageDirectory();
|
||||
|
||||
// Retrieve the External Storages root directory:
|
||||
String externalStorageRootDir;
|
||||
if ((externalStorageRootDir = primaryExternalStorage.getParent()) == null) { // no parent...
|
||||
String key = primaryExternalStorage.getAbsolutePath();
|
||||
if (!map.contains(key)) {
|
||||
map.add(key); // Make name as directory
|
||||
map.add(key);
|
||||
}
|
||||
} else {
|
||||
File externalStorageRoot = new File(externalStorageRootDir);
|
||||
File[] files = externalStorageRoot.listFiles();
|
||||
|
||||
if (files != null) {
|
||||
for (final File file : files) {
|
||||
// Check if it is a real directory (not a USB drive)...
|
||||
if (file.isDirectory()
|
||||
&& file.canRead()
|
||||
&& file.listFiles() != null
|
||||
&& (file.listFiles().length > 0)) {
|
||||
String key = file.getAbsolutePath();
|
||||
if (!map.contains(key)) {
|
||||
map.add(key); // Make name as directory
|
||||
map.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get candidates for removable external storage
|
||||
LinkedHashSet<File> candidateRemovableSdCardPaths = findSdCardPath();
|
||||
for (final File file : candidateRemovableSdCardPaths) {
|
||||
String key = file.getAbsolutePath();
|
||||
if (!map.contains(key)) {
|
||||
map.add(key); // Make name as directory
|
||||
map.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
346
backends/platform/android/org/scummvm/scummvm/INIParser.java
Normal file
346
backends/platform/android/org/scummvm/scummvm/INIParser.java
Normal file
@@ -0,0 +1,346 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* INI file parser modeled after config manager one in C++ side
|
||||
*/
|
||||
public class INIParser {
|
||||
private final static String LOG_TAG = "INIParser";
|
||||
|
||||
// This class can not be instantiated
|
||||
private INIParser() {
|
||||
}
|
||||
|
||||
public static Map<String, Map<String, String>> parse(Reader reader) throws IOException {
|
||||
Map<String, Map<String, String>> ret = new LinkedHashMap<>();
|
||||
BufferedReader lineReader = new BufferedReader(reader);
|
||||
Map<String, String> domain = null;
|
||||
int lineno = 0;
|
||||
String line;
|
||||
|
||||
while ((line = lineReader.readLine()) != null) {
|
||||
lineno++;
|
||||
|
||||
if (lineno == 1 && line.startsWith("\357\273\277")) {
|
||||
line = line.substring(3);
|
||||
}
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final char firstChar = line.charAt(0);
|
||||
|
||||
/* Unlike C++ parser, we ignore comments for simplicity */
|
||||
if (firstChar == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstChar == '[') {
|
||||
int i;
|
||||
for(i = 1; i < line.length(); i++) {
|
||||
final char c = line.charAt(i);
|
||||
if (c > 127) {
|
||||
break;
|
||||
}
|
||||
if (!Character.isLetterOrDigit(c) && c != '-' && c != '_') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == line.length()) {
|
||||
return null;
|
||||
}
|
||||
if (line.charAt(i) != ']') {
|
||||
return null;
|
||||
}
|
||||
|
||||
String domainName = line.substring(1, i);
|
||||
domain = new LinkedHashMap<>();
|
||||
ret.put(domainName, domain);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
int i;
|
||||
for (i = 0; i < line.length(); i++) {
|
||||
final char c = line.charAt(i);
|
||||
if (!isSpace(c)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == line.length()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int equal = line.indexOf('=');
|
||||
if (equal == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = line.substring(i, equal);
|
||||
String value = line.substring(equal + 1);
|
||||
|
||||
key = trim(key);
|
||||
value = trim(value);
|
||||
|
||||
domain.put(key, value);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static String get(Map<String, Map<String, String>> ini, String section, String key, String defaultValue) {
|
||||
if (ini == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
Map<String, String> s = ini.get(section);
|
||||
if (s == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
String value = s.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static File getPath(Map<String, Map<String, String>> ini, String section, String key, File defaultValue) {
|
||||
if (ini == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
Map<String, String> s = ini.get(section);
|
||||
if (s == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
String value = s.get(key);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
// Path components are escaped and puny encoded, undo this
|
||||
File path = new File(""); // Create an abstract empty path
|
||||
for(String component : value.split("/")) {
|
||||
component = decodePathComponent(component);
|
||||
path = new File(path, component);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String decodePathComponent(String component) {
|
||||
if (!component.startsWith("xn--")) {
|
||||
return component;
|
||||
}
|
||||
|
||||
String decoded = punycodeDecode(component);
|
||||
if (component == decoded) {
|
||||
return component;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder(decoded);
|
||||
int i = result.indexOf("\u0081");
|
||||
while(i != -1 && i + 1 < result.length()) {
|
||||
char c = decoded.charAt(i + 1);
|
||||
if (c != 0x79) {
|
||||
result.setCharAt(i, (char)(c - 0x80));
|
||||
}
|
||||
result.deleteCharAt(i + 1);
|
||||
i = result.indexOf("\u0081", i + 1);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/* Java isWhitespace is more inclusive than C one */
|
||||
private static boolean isSpace(char c) {
|
||||
return (c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\013');
|
||||
}
|
||||
|
||||
/* Java trim is more strict than C one */
|
||||
private static String trim(String s) {
|
||||
int begin, end;
|
||||
for(begin = 0; begin < s.length(); begin++) {
|
||||
if (!isSpace(s.charAt(begin))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for(end = s.length() - 1; end > begin; end--) {
|
||||
if (!isSpace(s.charAt(end))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return s.substring(begin, end + 1);
|
||||
}
|
||||
|
||||
// punycode parameters, see https://datatracker.ietf.org/doc/html/rfc3492#section-5
|
||||
private static final int BASE = 36;
|
||||
private static final int TMIN = 1;
|
||||
private static final int TMAX = 26;
|
||||
private static final int SKEW = 38;
|
||||
private static final int DAMP = 700;
|
||||
private static final int INITIAL_N = 0x80;
|
||||
private static final int INITIAL_BIAS = 72;
|
||||
private static final int SMAX = 2147483647; // maximum Unicode code point
|
||||
|
||||
private static String punycodeDecode(String src) {
|
||||
// Check for prefix
|
||||
if (!src.startsWith("xn--")) {
|
||||
return src;
|
||||
}
|
||||
|
||||
// Check if it is ASCII
|
||||
for (int i = 0; i < src.length(); i++) {
|
||||
int c = src.charAt(i);
|
||||
if (c > 0x7F) {
|
||||
return src;
|
||||
}
|
||||
}
|
||||
|
||||
src = src.substring(4);
|
||||
|
||||
int tail = src.length();
|
||||
int startInsert = src.lastIndexOf('-', tail) + 1;
|
||||
while(true) {
|
||||
// Check the insertions string and chop off invalid characters
|
||||
int i;
|
||||
for(i = startInsert; i < tail; i++) {
|
||||
char c = src.charAt(i);
|
||||
if (!((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z'))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (i == tail) {
|
||||
// All good
|
||||
break;
|
||||
}
|
||||
if (src.charAt(i) == '.') {
|
||||
// Assume it's an extension, stop there
|
||||
tail = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (startInsert == 0) {
|
||||
// That was all invalid
|
||||
return src;
|
||||
}
|
||||
|
||||
// Look for previous dash
|
||||
tail = startInsert;
|
||||
startInsert = src.lastIndexOf('-', tail) + 1;
|
||||
// Check again
|
||||
}
|
||||
|
||||
// Do punycode work
|
||||
StringBuilder dest = new StringBuilder(src.substring(0, startInsert > 0 ? startInsert - 1 : 0));
|
||||
|
||||
int di = dest.length();
|
||||
int i = 0;
|
||||
int n = INITIAL_N;
|
||||
int bias = INITIAL_BIAS;
|
||||
|
||||
for (int si = startInsert; si < tail; di++) {
|
||||
int org_i = i;
|
||||
|
||||
for (int w = 1, k = BASE; true; k += BASE) {
|
||||
if (si >= tail) {
|
||||
Log.w(LOG_TAG, "punycode_decode: incorrect digit for string: " + src);
|
||||
return src;
|
||||
}
|
||||
|
||||
int digit = decodeDigit(src.charAt(si));
|
||||
si++;
|
||||
|
||||
if (digit == SMAX) {
|
||||
Log.w(LOG_TAG, "punycode_decode: incorrect digit2 for string: " + src);
|
||||
return src;
|
||||
}
|
||||
|
||||
if (digit > (SMAX - i) / w) {
|
||||
// OVERFLOW
|
||||
Log.w(LOG_TAG, "punycode_decode: overflow1 for string: " + src);
|
||||
return src;
|
||||
}
|
||||
|
||||
i += digit * w;
|
||||
int t;
|
||||
|
||||
if (k <= bias) {
|
||||
t = TMIN;
|
||||
} else if (k >= bias + TMAX) {
|
||||
t = TMAX;
|
||||
} else {
|
||||
t = k - bias;
|
||||
}
|
||||
|
||||
if (digit < t) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (w > SMAX / (BASE - t)) {
|
||||
// OVERFLOW
|
||||
Log.w(LOG_TAG, "punycode_decode: overflow2 for string: "+ src);
|
||||
return src;
|
||||
}
|
||||
|
||||
w *= BASE - t;
|
||||
}
|
||||
|
||||
bias = adaptBias(i - org_i, di + 1, org_i == 0);
|
||||
|
||||
if (i / (di + 1) > SMAX - n) {
|
||||
// OVERFLOW
|
||||
Log.w(LOG_TAG, "punycode_decode: overflow3 for string: " + src);
|
||||
return src;
|
||||
}
|
||||
|
||||
n += i / (di + 1);
|
||||
i %= (di + 1);
|
||||
|
||||
dest.insert(i, Character.toChars(n));
|
||||
i++;
|
||||
}
|
||||
|
||||
// Re-add tail
|
||||
dest.append(src.substring(tail));
|
||||
return dest.toString();
|
||||
}
|
||||
|
||||
private static int decodeDigit(char c) {
|
||||
if (c >= '0' && c <= '9') {
|
||||
return 26 + c - '0';
|
||||
} else if (c >= 'a' && c <= 'z') {
|
||||
return c - 'a';
|
||||
} else if (c >= 'A' && c <= 'Z') {
|
||||
return c - 'A';
|
||||
} else {
|
||||
return SMAX;
|
||||
}
|
||||
}
|
||||
|
||||
private static int adaptBias(int delta, int nPoints, boolean isFirst) {
|
||||
int k;
|
||||
|
||||
delta /= isFirst ? DAMP : 2;
|
||||
delta += delta / nPoints;
|
||||
|
||||
// while delta > 455: delta /= 35
|
||||
for (k = 0; delta > ((BASE - TMIN) * TMAX) / 2; k += BASE) {
|
||||
delta /= (BASE - TMIN);
|
||||
}
|
||||
|
||||
return k + (((BASE - TMIN + 1) * delta) / (delta + SKEW));
|
||||
}
|
||||
}
|
||||
168
backends/platform/android/org/scummvm/scummvm/LedView.java
Normal file
168
backends/platform/android/org/scummvm/scummvm/LedView.java
Normal file
@@ -0,0 +1,168 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
public class LedView extends View {
|
||||
public static final int DEFAULT_LED_COLOR = 0xffff0000;
|
||||
private static final int BLINK_TIME = 30; // ms
|
||||
|
||||
private boolean _state;
|
||||
private Runnable _blink;
|
||||
private Paint _painter;
|
||||
private int _radius;
|
||||
private int _centerX;
|
||||
private int _centerY;
|
||||
|
||||
public LedView(Context context) {
|
||||
this(context, true, DEFAULT_LED_COLOR);
|
||||
}
|
||||
|
||||
public LedView(Context context, boolean state) {
|
||||
this(context, state, DEFAULT_LED_COLOR);
|
||||
}
|
||||
|
||||
public LedView(Context context, boolean state, int color) {
|
||||
super(context);
|
||||
_state = state;
|
||||
init(color);
|
||||
}
|
||||
|
||||
public LedView(Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public LedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
|
||||
public LedView(
|
||||
Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(
|
||||
attrs,
|
||||
R.styleable.LedView,
|
||||
defStyleAttr, defStyleRes);
|
||||
|
||||
try {
|
||||
_state = a.getBoolean(R.styleable.LedView_state, true);
|
||||
int color = a.getColor(R.styleable.LedView_color, DEFAULT_LED_COLOR);
|
||||
init(color);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private void init(int color) {
|
||||
_painter = new Paint();
|
||||
_painter.setStyle(Paint.Style.FILL);
|
||||
if (isInEditMode()) {
|
||||
_painter.setStrokeWidth(2);
|
||||
_painter.setStyle(_state ? Paint.Style.FILL : Paint.Style.STROKE);
|
||||
}
|
||||
_painter.setColor(color);
|
||||
_painter.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
|
||||
int w = resolveSizeAndState(minw, widthMeasureSpec, 0);
|
||||
|
||||
int minh = MeasureSpec.getSize(w) - getPaddingLeft() - getPaddingRight() +
|
||||
getPaddingBottom() + getPaddingTop();
|
||||
int h = resolveSizeAndState(minh, heightMeasureSpec, 0);
|
||||
|
||||
setMeasuredDimension(w, h);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
int xpad = (getPaddingLeft() + getPaddingRight());
|
||||
int ypad = (getPaddingTop() + getPaddingBottom());
|
||||
|
||||
int ww = w - xpad;
|
||||
int hh = h - ypad;
|
||||
|
||||
_radius = Math.min(ww, hh) / 2 - 2;
|
||||
_centerX = w / 2;
|
||||
_centerY = h / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (!_state && !isInEditMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.drawCircle(_centerX, _centerY, _radius, _painter);
|
||||
}
|
||||
|
||||
public void on() {
|
||||
setState(true);
|
||||
}
|
||||
|
||||
public void off() {
|
||||
setState(false);
|
||||
}
|
||||
|
||||
public void setState(boolean state) {
|
||||
if (_blink != null) {
|
||||
removeCallbacks(_blink);
|
||||
_blink = null;
|
||||
}
|
||||
|
||||
if (_state == state) {
|
||||
return;
|
||||
}
|
||||
_state = state;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void blinkOnce() {
|
||||
if (_blink != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean oldState = _state;
|
||||
_state = !oldState;
|
||||
invalidate();
|
||||
|
||||
_blink = new Runnable() {
|
||||
private boolean _ran;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (_ran) {
|
||||
_blink = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_ran = true;
|
||||
_state = oldState;
|
||||
invalidate();
|
||||
|
||||
postDelayed(this, BLINK_TIME);
|
||||
}
|
||||
};
|
||||
postDelayed(_blink, BLINK_TIME);
|
||||
}
|
||||
}
|
||||
250
backends/platform/android/org/scummvm/scummvm/MouseHelper.java
Normal file
250
backends/platform/android/org/scummvm/scummvm/MouseHelper.java
Normal file
@@ -0,0 +1,250 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Contains helper methods for mouse/hover events that were introduced in Android 4.0.
|
||||
*/
|
||||
public class MouseHelper implements View.OnHoverListener {
|
||||
//private final View.OnHoverListener _listener;
|
||||
private final ScummVM _scummvm;
|
||||
private boolean _rmbPressed;
|
||||
private boolean _lmbPressed;
|
||||
private boolean _mmbPressed;
|
||||
private boolean _bmbPressed;
|
||||
private boolean _fmbPressed;
|
||||
private boolean _srmbPressed;
|
||||
private boolean _smmbPressed;
|
||||
|
||||
//
|
||||
// Class initialization fails when this throws an exception.
|
||||
// Checking hover availability is done on static class initialization for Android 1.6 compatibility.
|
||||
//
|
||||
static {
|
||||
try {
|
||||
Class.forName("android.view.View$OnHoverListener");
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling this forces class initialization
|
||||
*/
|
||||
public static void checkHoverAvailable() {}
|
||||
|
||||
public MouseHelper(ScummVM scummvm) {
|
||||
_scummvm = scummvm;
|
||||
//_listener = createListener();
|
||||
}
|
||||
|
||||
// private View.OnHoverListener createListener() {
|
||||
// return new View.OnHoverListener() {
|
||||
// @Override
|
||||
// public boolean onHover(View view, MotionEvent e) {
|
||||
// Log.d(ScummVM.LOG_TAG, "onHover mouseEvent");
|
||||
// return onMouseEvent(e, true);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
@Override
|
||||
public boolean onHover(View view, MotionEvent motionEvent) {
|
||||
//Log.d(ScummVM.LOG_TAG, "onHover mouseEvent");
|
||||
return onMouseEvent(motionEvent, true);
|
||||
// return false;
|
||||
}
|
||||
|
||||
// public void attach(SurfaceView main_surface) {
|
||||
// main_surface.setOnHoverListener(_listener);
|
||||
// }
|
||||
|
||||
// isTrackball is a subcase of isMouse (meaning isMouse will also return true)
|
||||
public static boolean isTrackball(KeyEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int source = e.getSource();
|
||||
return ((source & InputDevice.SOURCE_TRACKBALL) == InputDevice.SOURCE_TRACKBALL) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ((source & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE));
|
||||
|
||||
|
||||
}
|
||||
|
||||
// isTrackball is a subcase of isMouse (meaning isMouse will also return true)
|
||||
public static boolean isTrackball(MotionEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
//int source = e.getSource();
|
||||
|
||||
InputDevice device = e.getDevice();
|
||||
|
||||
if (device == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sources = device.getSources();
|
||||
|
||||
return ((sources & InputDevice.SOURCE_TRACKBALL) == InputDevice.SOURCE_TRACKBALL) ||
|
||||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && ((sources & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE));
|
||||
|
||||
}
|
||||
|
||||
// "Checking against SOURCE_STYLUS only indicates "an input device is capable of obtaining input
|
||||
// from a stylus. To determine whether a given touch event was produced by a stylus, examine
|
||||
// the tool type returned by MotionEvent#getToolType(int) for each individual pointer."
|
||||
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_STYLUS
|
||||
public static boolean isStylus(MotionEvent e){
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(int idx = 0; idx < e.getPointerCount(); idx++) {
|
||||
if (e.getToolType(idx) == MotionEvent.TOOL_TYPE_STYLUS)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isMouse(KeyEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int source = e.getSource();
|
||||
|
||||
//Log.d(ScummVM.LOG_TAG, "isMouse keyEvent source: " + source);
|
||||
|
||||
// SOURCE_MOUSE_RELATIVE is sent when mouse is detected as trackball
|
||||
// TODO: why does this happen? Do we need to also check for SOURCE_TRACKBALL here?
|
||||
return ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE)
|
||||
|| ((source & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS)
|
||||
|| ((source & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD)
|
||||
|| isTrackball(e);
|
||||
}
|
||||
|
||||
public static boolean isMouse(MotionEvent e) {
|
||||
if (e == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InputDevice device = e.getDevice();
|
||||
|
||||
if (device == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int sources = device.getSources();
|
||||
|
||||
// SOURCE_MOUSE_RELATIVE is sent when mouse is detected as trackball
|
||||
// TODO: why does this happen? Do we need to also check for SOURCE_TRACKBALL here?
|
||||
// TODO: should these all be checks against TOOL_TYPEs instead of SOURCEs?
|
||||
return ((sources & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE)
|
||||
|| ((sources & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD)
|
||||
|| isStylus(e)
|
||||
|| isTrackball(e);
|
||||
}
|
||||
|
||||
private boolean handleButton(MotionEvent e, boolean mbPressed, int mask, int downEvent, int upEvent) {
|
||||
boolean mbDown = (e.getButtonState() & mask) == mask;
|
||||
if ((e.getSource() & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE
|
||||
&& (e.getButtonState() & MotionEvent.BUTTON_BACK) == MotionEvent.BUTTON_BACK) {
|
||||
mbDown = (mask == MotionEvent.BUTTON_SECONDARY);
|
||||
}
|
||||
|
||||
if (mbDown) {
|
||||
if (!mbPressed) {
|
||||
// mouse button was pressed just now
|
||||
//Log.d(ScummVM.LOG_TAG, "handleButton mbDown, not mbPressed, mask = " + mask);
|
||||
_scummvm.pushEvent(downEvent, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (mbPressed) {
|
||||
//Log.d(ScummVM.LOG_TAG, "handleButton not mbDown, mbPressed, mask = " + mask);
|
||||
// mouse button was released just now
|
||||
_scummvm.pushEvent(upEvent, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public boolean onMouseEvent(MotionEvent e, boolean hover) {
|
||||
|
||||
_scummvm.pushEvent(ScummVMEvents.JE_MOUSE_MOVE,
|
||||
(int) e.getX(),
|
||||
(int) e.getY(),
|
||||
0,
|
||||
0, 0, 0);
|
||||
|
||||
if (e.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||
// The call is coming from ScummVMEvents, from a GenericMotionEvent (scroll wheel movement)
|
||||
// TODO Do we want the JE_MOUSE_MOVE event too in this case?
|
||||
int eventJEWheelUpDown = ScummVMEvents.JE_MOUSE_WHEEL_UP;
|
||||
if (e.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) {
|
||||
eventJEWheelUpDown = ScummVMEvents.JE_MOUSE_WHEEL_DOWN;
|
||||
}
|
||||
//Log.d(ScummVM.LOG_TAG, "onMouseEvent Wheel Up/Down = " + eventJEWheelUpDown);
|
||||
_scummvm.pushEvent(eventJEWheelUpDown,
|
||||
(int) e.getX(),
|
||||
(int) e.getY(),
|
||||
0,
|
||||
0, 0, 0);
|
||||
} else {
|
||||
|
||||
int buttonState = e.getButtonState();
|
||||
|
||||
//Log.d(ScummVM.LOG_TAG, "onMouseEvent buttonState = " + buttonState);
|
||||
|
||||
boolean lmbDown = (buttonState & MotionEvent.BUTTON_PRIMARY) == MotionEvent.BUTTON_PRIMARY;
|
||||
|
||||
if (!hover && e.getActionMasked() != MotionEvent.ACTION_UP && buttonState == 0) {
|
||||
// On some device types, ButtonState is 0 even when tapping on the touch-pad or using the stylus on the screen etc.
|
||||
lmbDown = true;
|
||||
}
|
||||
|
||||
if (lmbDown) {
|
||||
if (!_lmbPressed) {
|
||||
// left mouse button was pressed just now
|
||||
_scummvm.pushEvent(ScummVMEvents.JE_LMB_DOWN, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
_lmbPressed = true;
|
||||
} else {
|
||||
if (_lmbPressed) {
|
||||
// left mouse button was released just now
|
||||
_scummvm.pushEvent(ScummVMEvents.JE_LMB_UP, (int)e.getX(), (int)e.getY(), e.getButtonState(), 0, 0, 0);
|
||||
}
|
||||
|
||||
_lmbPressed = false;
|
||||
}
|
||||
|
||||
_rmbPressed = handleButton(e, _rmbPressed, MotionEvent.BUTTON_SECONDARY, ScummVMEvents.JE_RMB_DOWN, ScummVMEvents.JE_RMB_UP);
|
||||
_mmbPressed = handleButton(e, _mmbPressed, MotionEvent.BUTTON_TERTIARY, ScummVMEvents.JE_MMB_DOWN, ScummVMEvents.JE_MMB_UP);
|
||||
_bmbPressed = handleButton(e, _bmbPressed, MotionEvent.BUTTON_BACK, ScummVMEvents.JE_BMB_DOWN, ScummVMEvents.JE_BMB_UP);
|
||||
_fmbPressed = handleButton(e, _fmbPressed, MotionEvent.BUTTON_FORWARD, ScummVMEvents.JE_FMB_DOWN, ScummVMEvents.JE_FMB_UP);
|
||||
// Lint warning for BUTTON_STYLUS... "
|
||||
// Field requires API level 23 (current min is 16): android.view.MotionEvent#BUTTON_STYLUS_PRIMARY"
|
||||
// Field requires API level 23 (current min is 16): android.view.MotionEvent#BUTTON_STYLUS_SECONDARY"
|
||||
// We suppress it:
|
||||
//
|
||||
// https://stackoverflow.com/a/48588149
|
||||
_srmbPressed = handleButton(e, _srmbPressed, MotionEvent.BUTTON_STYLUS_PRIMARY, ScummVMEvents.JE_RMB_DOWN, ScummVMEvents.JE_RMB_UP);
|
||||
_smmbPressed = handleButton(e, _smmbPressed, MotionEvent.BUTTON_STYLUS_SECONDARY, ScummVMEvents.JE_MMB_DOWN, ScummVMEvents.JE_MMB_UP);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import static org.scummvm.scummvm.ScummVMEvents.JE_MOUSE_WHEEL_DOWN;
|
||||
import static org.scummvm.scummvm.ScummVMEvents.JE_MOUSE_WHEEL_UP;
|
||||
import static org.scummvm.scummvm.ScummVMEvents.JE_MULTI;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class MultitouchHelper {
|
||||
private final ScummVM _scummvm;
|
||||
|
||||
private boolean _candidateStartOfMultitouchSession;
|
||||
// a flag indicating whether we are in multitouch mode (more than one fingers down)
|
||||
private boolean _multitouchMode;
|
||||
|
||||
// within a multitouch session (until a cancel event or no more multiple fingers down) the IDs for each finger-pointer is persisted and is consistent across events
|
||||
// we can use the ids to track and utilize the movement of a specific finger (while ignoring the rest)
|
||||
// Currently, we are using the last finger down, as the finger that moves the cursor
|
||||
private int _firstPointerId;
|
||||
private int _secondPointerId;
|
||||
private int _thirdPointerId;
|
||||
|
||||
private int _cachedActionEventOnPointer2DownX;
|
||||
private int _cachedActionEventOnPointer2DownY;
|
||||
|
||||
//
|
||||
private int _cachedActionEventOnPointer1DownX;
|
||||
private int _cachedActionEventOnPointer1DownY;
|
||||
|
||||
// The "level" of multitouch that is detected.
|
||||
// We do not support downgrading a level, ie. if a three finger multitouch is detected,
|
||||
// then raising one finger will void the multitouch session,
|
||||
// rather than revert to two fingers multitouch.
|
||||
// Similarly we do not support upgrading a level, ie. if we are already handling a two finger multitouch,
|
||||
// then putting down another finger will void the session,
|
||||
// rather than upgrade it to three fingers multitouch.
|
||||
// INFO for this purpose we need to allow some limited time limit (delay _kLevelDecisionDelayMs) before deciding
|
||||
// if the user did a two finger multitouch or intents to do a three finger multitouch
|
||||
// Valid values for _multitouchLevel: MULTITOUCH_UNDECIDED, MULTITOUCH_TWO_FINGERS, MULTITOUCH_THREE_FINGERS
|
||||
private final int MULTITOUCH_UNDECIDED = 0;
|
||||
private final int MULTITOUCH_TWO_FINGERS = 2;
|
||||
private final int MULTITOUCH_THREE_FINGERS = 3;
|
||||
private int _multitouchLevel;
|
||||
|
||||
private final int _kLevelDecisionDelayMs = 400; // in milliseconds
|
||||
private final int _kTouchMouseWheelDecisionDelayMs = 260; // in milliseconds - NOTE: Keep it significantly lower than _kLevelDecisionDelayMs
|
||||
|
||||
// messages for MultitouchHelperHandler
|
||||
final static int MSG_MT_DECIDE_MULTITOUCH_SESSION_TIMEDOUT = 1;
|
||||
final static int MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT = 2;
|
||||
final static int MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT = 3;
|
||||
|
||||
final private MultitouchHelper.MultitouchHelperHandler _multiTouchLevelUpgradeHandler = new MultitouchHelper.MultitouchHelperHandler(this);
|
||||
|
||||
// Scroll handling variables (calling it "Mouse Wheel" as SCROLL event on Android touch interface refers to moving around a finger on the touch surfaces)
|
||||
private final int TOUCH_MOUSE_WHEEL_UNDECIDED = 0;
|
||||
private final int TOUCH_MOUSE_WHEEL_ACTIVE = 1;
|
||||
private final int TOUCH_MOUSE_WHEEL_NOT_HAPPENING = 3;
|
||||
|
||||
// Both fingers need to have moved up or down to enter the scroll (mouse wheel) mode
|
||||
// The difference from the original positions (for both fingers respectively) is averaged and compared to the threshold below
|
||||
// The decision to enter scroll mode happens as long as there are two fingers down but we're still undecided,
|
||||
// (ie. undecided about whether this is a two finger event OR a third finger will follow OR it is a scroll event)
|
||||
private final int MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION = 20;
|
||||
|
||||
private final int MOVE_THRESHOLD_FOR_SEND_TOUCH_MOUSE_WHEEL_EVENT = 30;
|
||||
|
||||
// 0: Undecided for scrolling (mouse wheel)
|
||||
// 1: "scrolling" mode active
|
||||
// 2: no "scrolling" (mouse wheel) (decided)
|
||||
// Scrolling (mouse wheel) mode is mutually exclusive with the rest of the multi-touch modes,
|
||||
// we can either send mouse wheel events or mouse click events in a multitouch session.
|
||||
private int _touchMouseWheelDecisionLevel = 0;
|
||||
|
||||
// constructor
|
||||
public MultitouchHelper(ScummVM scummvm) {
|
||||
_scummvm = scummvm;
|
||||
|
||||
_multitouchMode = false;
|
||||
_multitouchLevel = MULTITOUCH_UNDECIDED;
|
||||
_candidateStartOfMultitouchSession = false;
|
||||
|
||||
_touchMouseWheelDecisionLevel = TOUCH_MOUSE_WHEEL_UNDECIDED;
|
||||
resetPointers();
|
||||
}
|
||||
|
||||
public void resetPointers() {
|
||||
_firstPointerId = -1;
|
||||
_secondPointerId = -1;
|
||||
_thirdPointerId = -1;
|
||||
_cachedActionEventOnPointer1DownX = -1;
|
||||
_cachedActionEventOnPointer1DownY = -1;
|
||||
_cachedActionEventOnPointer2DownX = -1;
|
||||
_cachedActionEventOnPointer2DownY = -1;
|
||||
}
|
||||
|
||||
public boolean isMultitouchMode() {
|
||||
return _multitouchMode;
|
||||
}
|
||||
public int getMultitouchLevel() {
|
||||
return _multitouchLevel;
|
||||
}
|
||||
|
||||
public int getTouchMouseWheelDecisionLevel() {
|
||||
return _touchMouseWheelDecisionLevel;
|
||||
}
|
||||
public boolean isTouchMouseWheel() { return getTouchMouseWheelDecisionLevel() == TOUCH_MOUSE_WHEEL_ACTIVE; }
|
||||
|
||||
public void setMultitouchMode(boolean enabledFlg) {
|
||||
_multitouchMode = enabledFlg;
|
||||
}
|
||||
public void setMultitouchLevel(int mtlevel) {
|
||||
_multitouchLevel = mtlevel;
|
||||
}
|
||||
|
||||
public void setTouchMouseWheelDecisionLevel(int scrlevel) {
|
||||
_touchMouseWheelDecisionLevel = scrlevel;
|
||||
}
|
||||
|
||||
// TODO Maybe for consistency purposes, maybe sent all (important) UP events that were not sent, when ending a multitouch session?
|
||||
public boolean handleMotionEvent(final MotionEvent event) {
|
||||
|
||||
// constants from APIv5:
|
||||
// (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT
|
||||
//final int pointer = (action & 0xff00) >> 8;
|
||||
|
||||
final int maskedAction = event.getActionMasked();
|
||||
|
||||
int pointerIndex = -1;
|
||||
int actionEventX;
|
||||
int actionEventY;
|
||||
|
||||
if (maskedAction == MotionEvent.ACTION_DOWN) {
|
||||
// start of a multitouch session! one finger down -- this is sent for the first pointer who touches the screen
|
||||
resetPointers();
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_UNDECIDED);
|
||||
setMultitouchMode(false);
|
||||
_candidateStartOfMultitouchSession = true;
|
||||
_multiTouchLevelUpgradeHandler.clear();
|
||||
|
||||
pointerIndex = 0;
|
||||
_firstPointerId = event.getPointerId(pointerIndex);
|
||||
_cachedActionEventOnPointer1DownX = (int) event.getX(pointerIndex);;
|
||||
_cachedActionEventOnPointer1DownY = (int) event.getY(pointerIndex);;
|
||||
return false;
|
||||
|
||||
} else if (maskedAction == MotionEvent.ACTION_CANCEL) {
|
||||
resetPointers();
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_UNDECIDED);
|
||||
setMultitouchMode(false);
|
||||
_multiTouchLevelUpgradeHandler.clear();
|
||||
return true;
|
||||
|
||||
} else if (maskedAction == MotionEvent.ACTION_OUTSIDE) {
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
if (event.getPointerCount() > 1 && event.getPointerCount() < 4) {
|
||||
// a multi-touch event
|
||||
if (_candidateStartOfMultitouchSession && event.getPointerCount() > 1) {
|
||||
_candidateStartOfMultitouchSession = false; // reset this flag
|
||||
setMultitouchMode(true);
|
||||
}
|
||||
|
||||
if (isMultitouchMode()) {
|
||||
if (maskedAction == MotionEvent.ACTION_POINTER_DOWN) {
|
||||
pointerIndex = event.getActionIndex();
|
||||
if (event.getPointerCount() == 2) {
|
||||
_secondPointerId = event.getPointerId(pointerIndex);
|
||||
|
||||
if (getMultitouchLevel() == MULTITOUCH_UNDECIDED) {
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
if (pointerIndex != -1) {
|
||||
_cachedActionEventOnPointer2DownX = (int) event.getX(pointerIndex);
|
||||
_cachedActionEventOnPointer2DownY = (int) event.getY(pointerIndex);
|
||||
} else {
|
||||
_cachedActionEventOnPointer2DownX = -1;
|
||||
_cachedActionEventOnPointer2DownY = -1;
|
||||
}
|
||||
// Allow for some time before deciding a two finger touch event, since the user might be going for a three finger touch event
|
||||
_multiTouchLevelUpgradeHandler.sendMessageDelayed(_multiTouchLevelUpgradeHandler.obtainMessage(MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT), _kLevelDecisionDelayMs);
|
||||
// Also allow for (less) time to check if this is a two-finger "mouse-wheel" event
|
||||
_multiTouchLevelUpgradeHandler.sendMessageDelayed(_multiTouchLevelUpgradeHandler.obtainMessage(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT), _kTouchMouseWheelDecisionDelayMs);
|
||||
// Return as event "handled" here
|
||||
// while we wait for the decision to be made for the level of multitouch (two or three)
|
||||
//
|
||||
return true;
|
||||
}
|
||||
// Don't return here
|
||||
// We want to handle the case whereby we were in multitouch level 2, and we got a new pointer down event with 2 pointers count
|
||||
// This is the case where the user keeps one finger down and taps the second finger
|
||||
// This behavior should count as multiple right clicks (one for each new "tap" (ACTION_POINTER_DOWN event))
|
||||
// for user friendliness / control intuitiveness
|
||||
|
||||
} else if (event.getPointerCount() == 3) {
|
||||
_thirdPointerId = event.getPointerId(pointerIndex);
|
||||
if (getMultitouchLevel() == MULTITOUCH_UNDECIDED) {
|
||||
setMultitouchLevel(MULTITOUCH_THREE_FINGERS);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_NOT_HAPPENING);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (event.getPointerCount() == 2) {
|
||||
// we prioritize the second pointer/ finger
|
||||
pointerIndex = event.findPointerIndex(_secondPointerId);
|
||||
if (pointerIndex != -1) {
|
||||
actionEventX = (int)event.getX(pointerIndex);
|
||||
actionEventY = (int)event.getY(pointerIndex);
|
||||
} else {
|
||||
actionEventX = -1;
|
||||
actionEventY = -1;
|
||||
}
|
||||
|
||||
if (getMultitouchLevel() == MULTITOUCH_UNDECIDED) {
|
||||
// Fast trigger an ACTION_POINTER_DOWN if:
|
||||
// - we were not yet decided on which level to use
|
||||
// AND either:
|
||||
// - a finger got up (from 3 to 2, shouldn't happen) or
|
||||
// - our main finger (second finger down) moved from cached position
|
||||
if (maskedAction == MotionEvent.ACTION_POINTER_UP
|
||||
|| (maskedAction == MotionEvent.ACTION_MOVE
|
||||
&& (actionEventX != _cachedActionEventOnPointer2DownX
|
||||
|| actionEventY != _cachedActionEventOnPointer2DownY))) {
|
||||
|
||||
setMultitouchLevel(MULTITOUCH_TWO_FINGERS);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT);
|
||||
|
||||
// Checking if we can decide on a touch mouse wheel mode session
|
||||
int firstPointerIndex = event.findPointerIndex(_firstPointerId);
|
||||
int actionEventFirstPointerCoordY = -1;
|
||||
if (firstPointerIndex != -1) {
|
||||
actionEventFirstPointerCoordY = (int) event.getY(firstPointerIndex);
|
||||
}
|
||||
|
||||
if (maskedAction == MotionEvent.ACTION_MOVE) {
|
||||
// Decide Scroll (touch mouse wheel) if:
|
||||
// - we were not yet decided on which level to use
|
||||
// - and two fingers are down (but not because we went from 3 to 2)
|
||||
// - and it's a move event
|
||||
// - and the movement distance of both fingers on y axis is around >= MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION
|
||||
// (plus some other qualifying checks to determine significant and similar movement on both fingers)
|
||||
// NOTE the movementOfFinger2onY and movementOfFinger1onY gets higher (on subsequent events)
|
||||
// if the user keeps moving their fingers (in the same direction),
|
||||
// since it's in reference to the starting points for the fingers
|
||||
int movementOfFinger2onY = actionEventY - _cachedActionEventOnPointer2DownY;
|
||||
int movementOfFinger1onY = actionEventFirstPointerCoordY - _cachedActionEventOnPointer1DownY;
|
||||
int absMovementOfFinger2onY = Math.abs(movementOfFinger2onY);
|
||||
int absMovementOfFinger1onY = Math.abs(movementOfFinger1onY);
|
||||
int absDiffOfMovementOfFingersOnY = Math.abs(movementOfFinger2onY - movementOfFinger1onY);
|
||||
|
||||
if (getTouchMouseWheelDecisionLevel() == TOUCH_MOUSE_WHEEL_UNDECIDED
|
||||
&& (movementOfFinger2onY > 0 && movementOfFinger1onY > 0) || (movementOfFinger2onY < 0 && movementOfFinger1onY < 0)
|
||||
&& absDiffOfMovementOfFingersOnY < MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION ) {
|
||||
|
||||
if ((absMovementOfFinger2onY + absMovementOfFinger1onY) / 2 >= MOVE_THRESHOLD_FOR_TOUCH_MOUSE_WHEEL_DECISION) {
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_ACTIVE);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
} else {
|
||||
// ignore this move event but don't forward it (return true as "event handled")
|
||||
// there's still potential to be a scroll (touch mouse wheel) event, with accumulated movement
|
||||
//
|
||||
// Also downgrade the multitouch level to undecided to re-enter this code segment next time
|
||||
// (the "countdown" for three-finger touch decision is not resumed)
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
setMultitouchLevel(MULTITOUCH_UNDECIDED);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_NOT_HAPPENING);
|
||||
_multiTouchLevelUpgradeHandler.removeMessages(MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT);
|
||||
}
|
||||
// End of: Checking if we can decide on a touch mouse wheel mode session
|
||||
|
||||
if (getTouchMouseWheelDecisionLevel() != TOUCH_MOUSE_WHEEL_ACTIVE) {
|
||||
// send the missing pointer down event first, before sending the actual current move event below
|
||||
_scummvm.pushEvent(JE_MULTI,
|
||||
event.getPointerCount(),
|
||||
MotionEvent.ACTION_POINTER_DOWN,
|
||||
actionEventX,
|
||||
actionEventY,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.getPointerCount() == 3) {
|
||||
// we prioritize the third pointer/ finger
|
||||
pointerIndex = event.findPointerIndex(_thirdPointerId);
|
||||
}
|
||||
}
|
||||
|
||||
// if (pointerIndex == -1) {
|
||||
// Log.d(ScummVM.LOG_TAG,"Warning: pointerIndex == -1 and getPointerCount = " + event.getPointerCount());
|
||||
// }
|
||||
|
||||
if (pointerIndex != -1) {
|
||||
actionEventX = (int)event.getX(pointerIndex);
|
||||
actionEventY = (int)event.getY(pointerIndex);
|
||||
} else {
|
||||
actionEventX = -1;
|
||||
actionEventY = -1;
|
||||
}
|
||||
|
||||
// we are only concerned for events with fingers down equal to the decided level of multitouch session
|
||||
if (getMultitouchLevel() == event.getPointerCount()) {
|
||||
if ((isTouchMouseWheel()) ) {
|
||||
if (maskedAction == MotionEvent.ACTION_MOVE && pointerIndex == event.findPointerIndex(_secondPointerId)) {
|
||||
// The co-ordinates sent with the event are the original touch co-ordinates of the first finger
|
||||
// The mouse cursor should not move around while touch-mouse-wheel scrolling
|
||||
// Also for simplification we only use the move events for the second finger
|
||||
// and skip non-significant movements.
|
||||
int movementOfFinger2onY = actionEventY - _cachedActionEventOnPointer2DownY;
|
||||
if (Math.abs(movementOfFinger2onY) > MOVE_THRESHOLD_FOR_SEND_TOUCH_MOUSE_WHEEL_EVENT) {
|
||||
_scummvm.pushEvent((movementOfFinger2onY > 0 ) ? JE_MOUSE_WHEEL_UP : JE_MOUSE_WHEEL_DOWN,
|
||||
_cachedActionEventOnPointer1DownX,
|
||||
_cachedActionEventOnPointer1DownY,
|
||||
1, // This will indicate to the event handling code in native that it comes from touch interface
|
||||
0,
|
||||
0, 0);
|
||||
_cachedActionEventOnPointer2DownY = actionEventY;
|
||||
}
|
||||
} // otherwise don't push an event in this case and return true
|
||||
} else {
|
||||
// arg1 will be the number of fingers down in the MULTI event we send to events.cpp
|
||||
// arg2 is the event action
|
||||
// arg3 and arg4 are the X,Y coordinates for the "active pointer" index, which is the last finger down
|
||||
// (the second in two-fingers touch, or the third in a three-fingers touch mode)
|
||||
_scummvm.pushEvent(JE_MULTI,
|
||||
event.getPointerCount(),
|
||||
event.getAction(),
|
||||
actionEventX,
|
||||
actionEventY,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (event.getPointerCount() >= 4) {
|
||||
// ignore if more than 3 fingers down. Mark as multitouch "handled" (return true)
|
||||
return true;
|
||||
|
||||
} else if (event.getPointerCount() == 1 && isMultitouchMode() ) {
|
||||
// We were already in a Multitouch session, but we're left with one finger down now
|
||||
// Keep ignoring events for single pointer until we exit multitouch mode "session"
|
||||
// this is to catch the case of being left with only one finger still touching the surface
|
||||
// after lifting the rest of the fingers that were touching the surface
|
||||
return true;
|
||||
} else {
|
||||
// one finger, no active multitouch mode "session". Mark as unhandled.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom handler code (to avoid mem leaks, see warning "This Handler Class Should Be Static Or Leaks Might Occur”) based on:
|
||||
// https://stackoverflow.com/a/27826094
|
||||
public static class MultitouchHelperHandler extends Handler {
|
||||
|
||||
private final WeakReference<MultitouchHelper> mListenerReference;
|
||||
|
||||
public MultitouchHelperHandler(MultitouchHelper listener) {
|
||||
super(Looper.getMainLooper());
|
||||
mListenerReference = new WeakReference<>(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void handleMessage(@NonNull Message msg) {
|
||||
MultitouchHelper listener = mListenerReference.get();
|
||||
if(listener != null) {
|
||||
listener.handle_MTHH_Message(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle_MTHH_Message(final Message msg) {
|
||||
if ((msg.what == MSG_MT_UPGRADE_TO_LEVEL_3_TIMEDOUT && getMultitouchLevel() == MULTITOUCH_UNDECIDED)
|
||||
|| (msg.what == MSG_MT_CHECK_FOR_TOUCH_MOUSE_WHEEL_TIMEDOUT
|
||||
&& (getMultitouchLevel() == MULTITOUCH_UNDECIDED
|
||||
|| (getMultitouchLevel() == 2 && getTouchMouseWheelDecisionLevel() == TOUCH_MOUSE_WHEEL_UNDECIDED)))) {
|
||||
// Either:
|
||||
// - window of allowing upgrade to level 3 (three fingers) timed out
|
||||
// - or window of allowing time for checking for scroll (touch mouse wheel) timed out
|
||||
// decide level 2 (two fingers).
|
||||
setMultitouchLevel(MULTITOUCH_TWO_FINGERS);
|
||||
setTouchMouseWheelDecisionLevel(TOUCH_MOUSE_WHEEL_NOT_HAPPENING);
|
||||
|
||||
// send the delayed pointer down event
|
||||
_scummvm.pushEvent(JE_MULTI,
|
||||
2,
|
||||
MotionEvent.ACTION_POINTER_DOWN,
|
||||
_cachedActionEventOnPointer2DownX,
|
||||
_cachedActionEventOnPointer2DownY,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearEventHandler() {
|
||||
_multiTouchLevelUpgradeHandler.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
public interface OnKeyboardVisibilityListener {
|
||||
void onVisibilityChanged(boolean visible);
|
||||
}
|
||||
797
backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
Normal file
797
backends/platform/android/org/scummvm/scummvm/SAFFSTree.java
Normal file
@@ -0,0 +1,797 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.UriPermission;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* SAF primitives for C++ FSNode
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public class SAFFSTree {
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public interface IOBusyListener {
|
||||
void onIOBusy(float ratio);
|
||||
}
|
||||
|
||||
private static class IOTime {
|
||||
long start;
|
||||
long end;
|
||||
long duration;
|
||||
}
|
||||
// Declare us as busy when I/O waits took more than 90% in 2 secs
|
||||
private static final long IO_BUSINESS_TIMESPAN = 2000;
|
||||
private static final long IO_BUSINESS_THRESHOLD = 1800;
|
||||
|
||||
private static ConcurrentLinkedQueue<IOTime> _lastIOs;
|
||||
private static IOBusyListener _listener;
|
||||
|
||||
private static HashMap<String, SAFFSTree> _trees;
|
||||
|
||||
// This map will store the references of all our objects used
|
||||
// by the native side.
|
||||
// This avoids overflowing JNI will a pile of global references
|
||||
private static ConcurrentHashMap<Long, SAFFSNode> _nodes;
|
||||
// This atomic variable will generate unique identifiers for our objects
|
||||
private static AtomicLong _idCounter;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public static void setIOBusyListener(IOBusyListener l) {
|
||||
if (_lastIOs == null) {
|
||||
_lastIOs = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
_listener = l;
|
||||
}
|
||||
|
||||
private static void reportIO(long start, long end) {
|
||||
if (_listener == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register this new query
|
||||
IOTime entry = new IOTime();
|
||||
entry.start = start;
|
||||
entry.end = end;
|
||||
entry.duration = end - start;
|
||||
_lastIOs.add(entry);
|
||||
|
||||
long deadline = end - IO_BUSINESS_TIMESPAN;
|
||||
long duration = 0;
|
||||
|
||||
// Remove outdated entries and compute the time spent in I/Os
|
||||
Iterator<IOTime> it = _lastIOs.iterator();
|
||||
while (it.hasNext()) {
|
||||
entry = it.next();
|
||||
//Log.d(ScummVM.LOG_TAG, "ENTRY <" + Long.toString(entry.start) + " " + Long.toString(entry.end) + " " + Long.toString(entry.duration) + ">");
|
||||
if (entry.end <= deadline) {
|
||||
// entry is too old
|
||||
it.remove();
|
||||
} else if (entry.start < deadline) {
|
||||
// This entry crossed the deadline
|
||||
duration += entry.end - deadline;
|
||||
} else {
|
||||
duration += entry.duration;
|
||||
}
|
||||
}
|
||||
//Log.d(ScummVM.LOG_TAG, "SUM: " + Long.toString(duration) + " DEADLINE WAS: " + Long.toString(deadline));
|
||||
|
||||
if (duration >= IO_BUSINESS_THRESHOLD && _listener != null) {
|
||||
_listener.onIOBusy((float)duration / IO_BUSINESS_TIMESPAN);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadSAFTrees(Context context) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
|
||||
// As this function is called before starting to emit nodes,
|
||||
// we can take the opportunity to setup the reference related stuff here
|
||||
if (_nodes == null) {
|
||||
_nodes = new ConcurrentHashMap<>();
|
||||
_idCounter = new AtomicLong();
|
||||
}
|
||||
|
||||
_trees = new HashMap<>();
|
||||
for (UriPermission permission : resolver.getPersistedUriPermissions()) {
|
||||
final Uri uri = permission.getUri();
|
||||
if (!DocumentsContract.isTreeUri(uri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SAFFSTree tree = new SAFFSTree(context, uri);
|
||||
_trees.put(tree.getTreeId(), tree);
|
||||
}
|
||||
}
|
||||
|
||||
public static SAFFSTree newTree(Context context, Uri uri) {
|
||||
if (_trees == null) {
|
||||
loadSAFTrees(context);
|
||||
}
|
||||
SAFFSTree tree = new SAFFSTree(context, uri);
|
||||
_trees.put(tree.getTreeId(), tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
public static SAFFSTree[] getTrees(Context context) {
|
||||
if (_trees == null) {
|
||||
loadSAFTrees(context);
|
||||
}
|
||||
return _trees.values().toArray(new SAFFSTree[0]);
|
||||
}
|
||||
|
||||
public static SAFFSTree findTree(Context context, String name) {
|
||||
if (_trees == null) {
|
||||
loadSAFTrees(context);
|
||||
}
|
||||
return _trees.get(name);
|
||||
}
|
||||
|
||||
public static class PathResult {
|
||||
public final SAFFSTree tree;
|
||||
public final SAFFSNode node;
|
||||
|
||||
PathResult(SAFFSTree tree, SAFFSNode node) {
|
||||
this.tree = tree;
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a ScummVM virtual path to SAF objects if it's in the SAF domain.
|
||||
* Returns null otherwise and throws a FileNotFoundException if the SAF path doesn't exist.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public static PathResult fullPathToNode(Context context, String path, boolean createDirIfNotExists) throws FileNotFoundException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N ||
|
||||
!path.startsWith("/saf/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
int slash = path.indexOf('/', 5);
|
||||
if (slash == -1) {
|
||||
slash = path.length();
|
||||
}
|
||||
String treeName = path.substring(5, slash);
|
||||
String innerPath = path.substring(slash);
|
||||
|
||||
SAFFSTree tree = SAFFSTree.findTree(context, treeName);
|
||||
if (tree == null) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
SAFFSNode node = tree.pathToNode(innerPath, createDirIfNotExists);
|
||||
if (node == null) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
return new PathResult(tree, node);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.BASE)
|
||||
public static void clearCaches() {
|
||||
if (_trees == null) {
|
||||
return;
|
||||
}
|
||||
for (SAFFSTree tree : _trees.values()) {
|
||||
tree.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public static void addNodeRef(long nodeId) {
|
||||
assert(nodeId != 0);
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
long newId = node.addRef();
|
||||
assert(newId == nodeId);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public static void decNodeRef(long nodeId) {
|
||||
assert(nodeId != 0);
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
node.decRef();
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public static SAFFSNode refToNode(long nodeId) {
|
||||
assert(nodeId != 0);
|
||||
return _nodes.get(nodeId);
|
||||
}
|
||||
|
||||
public static class SAFFSNode implements Comparable<SAFFSNode> {
|
||||
public static final int DIRECTORY = 0x01;
|
||||
public static final int WRITABLE = 0x02;
|
||||
public static final int READABLE = 0x04;
|
||||
public static final int DELETABLE = 0x08;
|
||||
public static final int REMOVABLE = 0x10;
|
||||
|
||||
public SAFFSNode _parent;
|
||||
public String _path;
|
||||
public String _documentId;
|
||||
public int _flags;
|
||||
|
||||
private HashMap<String, SoftReference<SAFFSNode>> _children;
|
||||
private boolean _dirty;
|
||||
private int _refCnt; // Reference counter for the native side
|
||||
private long _id; // Identifier for the native side
|
||||
|
||||
private SAFFSNode reset(SAFFSNode parent, String path, String documentId, int flags) {
|
||||
_parent = parent;
|
||||
_path = path;
|
||||
_documentId = documentId;
|
||||
_flags = flags;
|
||||
|
||||
_children = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
private static int computeFlags(String mimeType, int flags) {
|
||||
int ourFlags = 0;
|
||||
if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
ourFlags |= SAFFSNode.DIRECTORY;
|
||||
}
|
||||
if ((flags & (DocumentsContract.Document.FLAG_SUPPORTS_WRITE | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE)) != 0) {
|
||||
ourFlags |= SAFFSNode.WRITABLE;
|
||||
}
|
||||
if ((flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) == 0) {
|
||||
ourFlags |= SAFFSNode.READABLE;
|
||||
}
|
||||
if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0) {
|
||||
ourFlags |= SAFFSNode.DELETABLE;
|
||||
}
|
||||
if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_REMOVE) != 0) {
|
||||
ourFlags |= SAFFSNode.REMOVABLE;
|
||||
}
|
||||
return ourFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(SAFFSNode o) {
|
||||
if (o == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
return _path.compareTo(o._path);
|
||||
}
|
||||
|
||||
public synchronized long addRef() {
|
||||
_refCnt += 1;
|
||||
if (_refCnt > 1) {
|
||||
return _id;
|
||||
}
|
||||
assert(_refCnt == 1);
|
||||
|
||||
if (_id == 0) {
|
||||
_id = _idCounter.incrementAndGet();
|
||||
}
|
||||
_nodes.put(_id, this);
|
||||
|
||||
return _id;
|
||||
}
|
||||
|
||||
public synchronized void decRef() {
|
||||
if (_refCnt == 1) {
|
||||
SAFFSNode tmp = _nodes.remove(_id);
|
||||
assert(tmp == this);
|
||||
}
|
||||
_refCnt -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
private final Context _context;
|
||||
private final Uri _treeUri;
|
||||
|
||||
private final SAFFSNode _root;
|
||||
private final String _treeName;
|
||||
|
||||
public SAFFSTree(Context context, Uri treeUri) {
|
||||
_context = context;
|
||||
_treeUri = treeUri;
|
||||
|
||||
_root = new SAFFSNode().reset(null, "", DocumentsContract.getTreeDocumentId(treeUri), 0);
|
||||
// Update flags and get name
|
||||
String treeName = stat(_root);
|
||||
if (treeName == null) {
|
||||
// The tree likely got deleted
|
||||
// Use the document ID instead as this will let the user do some cleanup
|
||||
treeName = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
}
|
||||
_treeName = treeName;
|
||||
}
|
||||
|
||||
public String getTreeId() {
|
||||
return Uri.encode(DocumentsContract.getTreeDocumentId(_treeUri));
|
||||
}
|
||||
public String getTreeName() {
|
||||
return _treeName;
|
||||
}
|
||||
public Uri getTreeDocumentUri() {
|
||||
return DocumentsContract.buildDocumentUriUsingTree(_treeUri, _root._documentId);
|
||||
}
|
||||
|
||||
private void clearCache() {
|
||||
ArrayDeque<SAFFSNode> stack = new ArrayDeque<>();
|
||||
stack.push(_root);
|
||||
while (!stack.isEmpty()) {
|
||||
SAFFSNode node = stack.pop();
|
||||
node._dirty = true;
|
||||
if (node._children == null) {
|
||||
continue;
|
||||
}
|
||||
for (SoftReference<SAFFSNode> ref : node._children.values()) {
|
||||
node = ref.get();
|
||||
if (node != null) {
|
||||
stack.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SAFFSNode pathToNode(String path, boolean createDirIfNotExists) {
|
||||
String[] components = path.split("/");
|
||||
|
||||
SAFFSNode node = _root;
|
||||
for (String component : components) {
|
||||
if (component.isEmpty() || ".".equals(component)) {
|
||||
continue;
|
||||
}
|
||||
if ("..".equals(component)) {
|
||||
if (node._parent != null) {
|
||||
node = node._parent;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
SAFFSNode newNode = getChild(node, component);
|
||||
if (newNode == null && createDirIfNotExists) {
|
||||
newNode = createDirectory(node, component);
|
||||
}
|
||||
if (newNode == null) {
|
||||
return null;
|
||||
}
|
||||
node = newNode;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public SAFFSNode[] getChildren(SAFFSNode node) {
|
||||
Collection<SAFFSNode> results = null;
|
||||
if (node._children != null && !node._dirty) {
|
||||
results = new ArrayDeque<>();
|
||||
for (SoftReference<SAFFSNode> ref : node._children.values()) {
|
||||
if (ref == null) {
|
||||
continue;
|
||||
}
|
||||
SAFFSNode newnode = ref.get();
|
||||
if (newnode == null) {
|
||||
// Some reference went stale: refresh
|
||||
results = null;
|
||||
break;
|
||||
}
|
||||
results.add(newnode);
|
||||
}
|
||||
}
|
||||
|
||||
if (results == null) {
|
||||
try {
|
||||
results = fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return results.toArray(new SAFFSNode[0]);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode[] getChildren(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return getChildren(node);
|
||||
}
|
||||
|
||||
public Collection<SAFFSNode> fetchChildren(SAFFSNode node) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri searchUri = DocumentsContract.buildChildDocumentsUriUsingTree(_treeUri, node._documentId);
|
||||
final ArrayDeque<SAFFSNode> results = new ArrayDeque<>();
|
||||
|
||||
// Keep the old children around to reuse them: this will help to keep one SAFFSNode instance for each node
|
||||
HashMap<String, SoftReference<SAFFSNode>> oldChildren = node._children;
|
||||
node._children = null;
|
||||
|
||||
// When _children will be set, it will be clean
|
||||
node._dirty = false;
|
||||
HashMap<String, SoftReference<SAFFSNode>> newChildren = new HashMap<>();
|
||||
|
||||
Cursor c = null;
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
c = resolver.query(searchUri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
|
||||
if (c == null) {
|
||||
return results;
|
||||
}
|
||||
while (c.moveToNext()) {
|
||||
final String displayName = c.getString(0);
|
||||
final String documentId = c.getString(1);
|
||||
final String mimeType = c.getString(2);
|
||||
final int flags = c.getInt(3);
|
||||
|
||||
final int ourFlags = SAFFSNode.computeFlags(mimeType, flags);
|
||||
|
||||
SAFFSNode newnode = null;
|
||||
SoftReference<SAFFSNode> oldnodeRef;
|
||||
if (oldChildren != null) {
|
||||
oldnodeRef = oldChildren.remove(displayName);
|
||||
if (oldnodeRef != null) {
|
||||
newnode = oldnodeRef.get();
|
||||
}
|
||||
}
|
||||
if (newnode == null) {
|
||||
newnode = new SAFFSNode();
|
||||
}
|
||||
|
||||
newnode.reset(node, node._path + "/" + displayName, documentId, ourFlags);
|
||||
newChildren.put(displayName, new SoftReference<>(newnode));
|
||||
|
||||
results.add(newnode);
|
||||
}
|
||||
|
||||
// Success: store the cache
|
||||
node._children = newChildren;
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public SAFFSNode getChild(SAFFSNode node, String name) {
|
||||
//This variable is used to hold a strong reference on every children nodes
|
||||
//noinspection unused
|
||||
Collection<SAFFSNode> children;
|
||||
|
||||
if (node._children == null || node._dirty) {
|
||||
try {
|
||||
//noinspection UnusedAssignment
|
||||
children = fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
SoftReference<SAFFSNode> ref = node._children.get(name);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SAFFSNode newnode = ref.get();
|
||||
if (newnode != null) {
|
||||
return newnode;
|
||||
}
|
||||
|
||||
// Node reference was stale, force a refresh
|
||||
try {
|
||||
//noinspection UnusedAssignment
|
||||
children = fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
ref = node._children.get(name);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
newnode = ref.get();
|
||||
if (newnode == null) {
|
||||
Log.e(ScummVM.LOG_TAG, "Failed to keep a reference on object");
|
||||
}
|
||||
|
||||
return newnode;
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode getChild(long nodeId, String name) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return getChild(node, name);
|
||||
}
|
||||
|
||||
public SAFFSNode createDirectory(SAFFSNode node, String name) {
|
||||
return createDocument(node, name, DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode createDirectory(long nodeId, String name) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createDirectory(node, name);
|
||||
}
|
||||
|
||||
public SAFFSNode createFile(SAFFSNode node, String name) {
|
||||
return createDocument(node, name, "application/octet-stream");
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public SAFFSNode createFile(long nodeId, String name) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createFile(node, name);
|
||||
}
|
||||
|
||||
public int createReadStream(SAFFSNode node) {
|
||||
return createStream(node, "r");
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public int createReadStream(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createReadStream(node);
|
||||
}
|
||||
|
||||
public int createWriteStream(SAFFSNode node) {
|
||||
return createStream(node, "wt");
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public int createWriteStream(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return createWriteStream(node);
|
||||
}
|
||||
|
||||
public int removeNode(SAFFSNode node) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
|
||||
if ((node._flags & SAFFSNode.REMOVABLE) != 0) {
|
||||
final Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._parent._documentId);
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
if (!DocumentsContract.removeDocument(resolver, uri, parentUri)) {
|
||||
return OsConstants.EIO;
|
||||
}
|
||||
} catch(FileNotFoundException e) {
|
||||
return OsConstants.ENOENT;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
} else if ((node._flags & SAFFSNode.DELETABLE) != 0) {
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
if (!DocumentsContract.deleteDocument(resolver, uri)) {
|
||||
return OsConstants.EIO;
|
||||
}
|
||||
} catch(FileNotFoundException e) {
|
||||
return OsConstants.ENOENT;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
} else {
|
||||
return OsConstants.EPERM;
|
||||
}
|
||||
|
||||
// Cleanup node
|
||||
node._parent._dirty = true;
|
||||
node.reset(null, null, null, 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public int removeNode(long nodeId) {
|
||||
SAFFSNode node = _nodes.get(nodeId);
|
||||
assert(node != null);
|
||||
|
||||
return removeNode(node);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* This version is used by the C++ side
|
||||
*/
|
||||
public void removeTree() {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
|
||||
String treeId = getTreeId();
|
||||
|
||||
resolver.releasePersistableUriPermission(_treeUri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
|
||||
if (_trees == null || _trees.remove(treeId) == null) {
|
||||
loadSAFTrees(_context);
|
||||
}
|
||||
}
|
||||
|
||||
private SAFFSNode createDocument(SAFFSNode node, String name, String mimeType) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
Uri newDocUri;
|
||||
|
||||
// Make sure _children is OK
|
||||
if (node._children == null || node._dirty) {
|
||||
try {
|
||||
fetchChildren(node);
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed to get children: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
newDocUri = DocumentsContract.createDocument(resolver, parentUri, mimeType, name);
|
||||
} catch(FileNotFoundException e) {
|
||||
return null;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
if (newDocUri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String documentId = DocumentsContract.getDocumentId(newDocUri);
|
||||
|
||||
SAFFSNode newnode = null;
|
||||
|
||||
SoftReference<SAFFSNode> oldnodeRef = node._children.remove(name);
|
||||
if (oldnodeRef != null) {
|
||||
newnode = oldnodeRef.get();
|
||||
}
|
||||
if (newnode == null) {
|
||||
newnode = new SAFFSNode();
|
||||
}
|
||||
|
||||
newnode.reset(node, node._path + "/" + name, documentId, 0);
|
||||
// Update flags
|
||||
final String realName = stat(newnode);
|
||||
if (realName == null) {
|
||||
return null;
|
||||
}
|
||||
// Unlikely but...
|
||||
if (!realName.equals(name)) {
|
||||
node._children.remove(realName);
|
||||
newnode._path = node._path + "/" + realName;
|
||||
}
|
||||
|
||||
node._children.put(realName, new SoftReference<>(newnode));
|
||||
|
||||
return newnode;
|
||||
}
|
||||
|
||||
public ParcelFileDescriptor createFileDescriptor(SAFFSNode node, String mode) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
|
||||
ParcelFileDescriptor pfd;
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
pfd = resolver.openFileDescriptor(uri, mode);
|
||||
} catch(FileNotFoundException e) {
|
||||
return null;
|
||||
} finally {
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
|
||||
return pfd;
|
||||
}
|
||||
|
||||
private int createStream(SAFFSNode node, String mode) {
|
||||
ParcelFileDescriptor pfd = createFileDescriptor(node, mode);
|
||||
if (pfd == null) {
|
||||
return -1;
|
||||
}
|
||||
return pfd.detachFd();
|
||||
}
|
||||
|
||||
private String stat(SAFFSNode node) {
|
||||
final ContentResolver resolver = _context.getContentResolver();
|
||||
final Uri uri = DocumentsContract.buildDocumentUriUsingTree(_treeUri, node._documentId);
|
||||
|
||||
Cursor c = null;
|
||||
|
||||
long startIO = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
c = resolver.query(uri, new String[] { DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_FLAGS }, null, null, null);
|
||||
if (c == null) {
|
||||
return null;
|
||||
}
|
||||
if (c.moveToNext()) {
|
||||
final String displayName = c.getString(0);
|
||||
final String mimeType = c.getString(1);
|
||||
final int flags = c.getInt(2);
|
||||
|
||||
node._flags = SAFFSNode.computeFlags(mimeType, flags);
|
||||
|
||||
return displayName;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(ScummVM.LOG_TAG, "Failed query: " + e);
|
||||
} finally {
|
||||
if (c != null) {
|
||||
try {
|
||||
c.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
long endIO = System.currentTimeMillis();
|
||||
reportIO(startIO, endIO);
|
||||
}
|
||||
// We should never end up here
|
||||
// If we do, a tree or a file got likely removed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
573
backends/platform/android/org/scummvm/scummvm/ScummVM.java
Normal file
573
backends/platform/android/org/scummvm/scummvm/ScummVM.java
Normal file
@@ -0,0 +1,573 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.util.Log;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Scanner;
|
||||
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
import javax.microedition.khronos.egl.EGLSurface;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
public abstract class ScummVM implements SurfaceHolder.Callback,
|
||||
CompatHelpers.SystemInsets.SystemInsetsListener, Runnable {
|
||||
public static final int SHOW_ON_SCREEN_MENU = 1;
|
||||
public static final int SHOW_ON_SCREEN_INPUT_MODE = 2;
|
||||
|
||||
final protected static String LOG_TAG = "ScummVM";
|
||||
final private AssetManager _asset_manager;
|
||||
final private Object _sem_surface;
|
||||
final private MyScummVMDestroyedCallback _svm_destroyed_callback;
|
||||
|
||||
private EGL10 _egl;
|
||||
private EGLDisplay _egl_display = EGL10.EGL_NO_DISPLAY;
|
||||
private EGLConfig _egl_config;
|
||||
private EGLContext _egl_context = EGL10.EGL_NO_CONTEXT;
|
||||
private EGLSurface _egl_surface = EGL10.EGL_NO_SURFACE;
|
||||
|
||||
private SurfaceHolder _surface_holder;
|
||||
private int bitsPerPixel;
|
||||
private AudioTrack _audio_track;
|
||||
private int _sample_rate = 0;
|
||||
private int _buffer_size = 0;
|
||||
|
||||
private boolean _assetsUpdated;
|
||||
private String[] _args;
|
||||
|
||||
private native void create(AssetManager asset_manager,
|
||||
EGL10 egl,
|
||||
EGLDisplay egl_display,
|
||||
AudioTrack audio_track,
|
||||
int sample_rate,
|
||||
int buffer_size,
|
||||
boolean assetsUpdated);
|
||||
private native void destroy();
|
||||
private native void setSurface(int width, int height, int bpp);
|
||||
private native int main(String[] args);
|
||||
|
||||
// pause the engine and all native threads
|
||||
final public native void setPause(boolean pause);
|
||||
// Feed an event to ScummVM. Safe to call from other threads.
|
||||
final public native void pushEvent(int type, int arg1, int arg2, int arg3,
|
||||
int arg4, int arg5, int arg6);
|
||||
// Update the 3D touch controls
|
||||
final public native void setupTouchMode(int oldValue, int newValue);
|
||||
final public native void updateTouch(int action, int ptr, int x, int y);
|
||||
|
||||
final public native void syncVirtkeyboardState(boolean newState);
|
||||
|
||||
final public native String getNativeVersionInfo();
|
||||
|
||||
// CompatHelpers.WindowInsets.SystemInsetsListener interface
|
||||
@Override
|
||||
final public native void systemInsetsUpdated(int[] gestureInsets, int[] systemInsets, int[] cutoutInsets);
|
||||
|
||||
// Callbacks from C++ peer instance
|
||||
abstract protected void getDPI(float[] values);
|
||||
abstract protected void displayMessageOnOSD(String msg);
|
||||
abstract protected void openUrl(String url);
|
||||
abstract protected boolean hasTextInClipboard();
|
||||
abstract protected String getTextFromClipboard();
|
||||
abstract protected boolean setTextInClipboard(String text);
|
||||
abstract protected boolean isConnectionLimited();
|
||||
abstract protected void setWindowCaption(String caption);
|
||||
abstract protected void showVirtualKeyboard(boolean enable);
|
||||
abstract protected void showOnScreenControls(int enableMask);
|
||||
abstract protected void setTouchMode(int touchMode);
|
||||
abstract protected int getTouchMode();
|
||||
abstract protected void setOrientation(int orientation);
|
||||
abstract protected String getScummVMBasePath();
|
||||
abstract protected String getScummVMConfigPath();
|
||||
abstract protected String getScummVMLogPath();
|
||||
abstract protected void setCurrentGame(String target);
|
||||
abstract protected String[] getSysArchives();
|
||||
abstract protected String[] getAllStorageLocations();
|
||||
abstract protected String[] getAllStorageLocationsNoPermissionRequest();
|
||||
abstract protected SAFFSTree getNewSAFTree(boolean write, String initialURI, String prompt);
|
||||
abstract protected SAFFSTree[] getSAFTrees();
|
||||
abstract protected SAFFSTree findSAFTree(String name);
|
||||
abstract protected int exportBackup(String prompt);
|
||||
abstract protected int importBackup(String prompt, String path);
|
||||
|
||||
public ScummVM(AssetManager asset_manager, SurfaceHolder holder, final MyScummVMDestroyedCallback scummVMDestroyedCallback) {
|
||||
_asset_manager = asset_manager;
|
||||
_sem_surface = new Object();
|
||||
_svm_destroyed_callback = scummVMDestroyedCallback;
|
||||
holder.addCallback(this);
|
||||
}
|
||||
|
||||
final public String getInstallingScummVMVersionInfo() {
|
||||
return getNativeVersionInfo();
|
||||
}
|
||||
|
||||
// SurfaceHolder callback
|
||||
final public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.d(LOG_TAG, "surfaceCreated");
|
||||
|
||||
// no need to do anything, surfaceChanged() will be called in any case
|
||||
}
|
||||
|
||||
// SurfaceHolder callback
|
||||
final public void surfaceChanged(SurfaceHolder holder, int format,
|
||||
int width, int height) {
|
||||
|
||||
PixelFormat pixelFormat = new PixelFormat();
|
||||
PixelFormat.getPixelFormatInfo(format, pixelFormat);
|
||||
bitsPerPixel = pixelFormat.bitsPerPixel;
|
||||
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "surfaceChanged: %dx%d (%d: %dbpp)",
|
||||
width, height, format, bitsPerPixel));
|
||||
|
||||
// store values for the native code
|
||||
// make sure to do it before notifying the lock
|
||||
// as it leads to a race condition otherwise
|
||||
setSurface(width, height, bitsPerPixel);
|
||||
|
||||
synchronized(_sem_surface) {
|
||||
_surface_holder = holder;
|
||||
_sem_surface.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
// SurfaceHolder callback
|
||||
final public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.d(LOG_TAG, "surfaceDestroyed");
|
||||
|
||||
synchronized(_sem_surface) {
|
||||
_surface_holder = null;
|
||||
_sem_surface.notifyAll();
|
||||
}
|
||||
|
||||
// Don't call when EGL is not init:
|
||||
// this avoids polluting the static variables with obsolete values
|
||||
if (_egl != null) {
|
||||
// clear values for the native code
|
||||
setSurface(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
final public void setAssetsUpdated(boolean assetsUpdated) {
|
||||
_assetsUpdated = assetsUpdated;
|
||||
}
|
||||
|
||||
final public void setArgs(String[] args) {
|
||||
_args = args;
|
||||
}
|
||||
|
||||
final public void run() {
|
||||
try {
|
||||
// wait for the surfaceChanged callback
|
||||
synchronized(_sem_surface) {
|
||||
while (_surface_holder == null)
|
||||
_sem_surface.wait();
|
||||
}
|
||||
|
||||
initAudio();
|
||||
initEGL();
|
||||
} catch (Exception e) {
|
||||
deinitEGL();
|
||||
deinitAudio();
|
||||
|
||||
throw new RuntimeException("Error preparing the ScummVM thread", e);
|
||||
}
|
||||
|
||||
create(_asset_manager, _egl, _egl_display,
|
||||
_audio_track, _sample_rate, _buffer_size,
|
||||
_assetsUpdated);
|
||||
|
||||
int res = main(_args);
|
||||
|
||||
destroy();
|
||||
|
||||
deinitEGL();
|
||||
deinitAudio();
|
||||
|
||||
// Don't exit force-ably here!
|
||||
if (_svm_destroyed_callback != null) {
|
||||
_svm_destroyed_callback.handle(res);
|
||||
}
|
||||
}
|
||||
|
||||
private void initEGL() throws Exception {
|
||||
_egl = (EGL10)EGLContext.getEGL();
|
||||
_egl_display = _egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
|
||||
|
||||
int[] version = new int[2];
|
||||
_egl.eglInitialize(_egl_display, version);
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "EGL version %d.%d initialized", version[0], version[1]));
|
||||
|
||||
int[] num_config = new int[1];
|
||||
_egl.eglGetConfigs(_egl_display, null, 0, num_config);
|
||||
|
||||
final int numConfigs = num_config[0];
|
||||
|
||||
if (numConfigs <= 0)
|
||||
throw new IllegalArgumentException("No EGL configs");
|
||||
|
||||
EGLConfig[] configs = new EGLConfig[numConfigs];
|
||||
_egl.eglGetConfigs(_egl_display, configs, numConfigs, num_config);
|
||||
|
||||
// Android's eglChooseConfig is busted in several versions and
|
||||
// devices so we have to filter/rank the configs ourselves.
|
||||
_egl_config = chooseEglConfig(configs, version);
|
||||
|
||||
int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||
int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL10.EGL_NONE };
|
||||
_egl_context = _egl.eglCreateContext(_egl_display, _egl_config,
|
||||
EGL10.EGL_NO_CONTEXT, attrib_list);
|
||||
|
||||
if (_egl_context == EGL10.EGL_NO_CONTEXT)
|
||||
throw new Exception(String.format(Locale.ROOT, "Failed to create context: 0x%x",
|
||||
_egl.eglGetError()));
|
||||
}
|
||||
|
||||
// Callback from C++ peer instance
|
||||
final protected EGLSurface initSurface() throws Exception {
|
||||
_egl_surface = _egl.eglCreateWindowSurface(_egl_display, _egl_config,
|
||||
_surface_holder, null);
|
||||
|
||||
if (_egl_surface == EGL10.EGL_NO_SURFACE)
|
||||
throw new Exception(String.format(Locale.ROOT,
|
||||
"eglCreateWindowSurface failed: 0x%x", _egl.eglGetError()));
|
||||
|
||||
_egl.eglMakeCurrent(_egl_display, _egl_surface, _egl_surface,
|
||||
_egl_context);
|
||||
|
||||
GL10 gl = (GL10)_egl_context.getGL();
|
||||
|
||||
Log.i(LOG_TAG, String.format(Locale.ROOT, "Using EGL %s (%s); GL %s/%s (%s)",
|
||||
_egl.eglQueryString(_egl_display, EGL10.EGL_VERSION),
|
||||
_egl.eglQueryString(_egl_display, EGL10.EGL_VENDOR),
|
||||
gl.glGetString(GL10.GL_VERSION),
|
||||
gl.glGetString(GL10.GL_RENDERER),
|
||||
gl.glGetString(GL10.GL_VENDOR)));
|
||||
|
||||
return _egl_surface;
|
||||
}
|
||||
|
||||
// Callback from C++ peer instance
|
||||
final protected void deinitSurface() {
|
||||
if (_egl_display != EGL10.EGL_NO_DISPLAY) {
|
||||
_egl.eglMakeCurrent(_egl_display, EGL10.EGL_NO_SURFACE,
|
||||
EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
|
||||
|
||||
if (_egl_surface != EGL10.EGL_NO_SURFACE)
|
||||
_egl.eglDestroySurface(_egl_display, _egl_surface);
|
||||
}
|
||||
|
||||
_egl_surface = EGL10.EGL_NO_SURFACE;
|
||||
}
|
||||
|
||||
// Callback from C++ peer instance
|
||||
final protected int eglVersion() {
|
||||
String version = _egl.eglQueryString(_egl_display, EGL10.EGL_VERSION);
|
||||
if (version == null) {
|
||||
// 1.0
|
||||
return 0x00010000;
|
||||
}
|
||||
|
||||
Scanner versionScan = new Scanner(version).useLocale(Locale.ROOT).useDelimiter("[ .]");
|
||||
int versionInt = versionScan.nextInt() << 16;
|
||||
versionInt |= versionScan.nextInt() & 0xffff;
|
||||
return versionInt;
|
||||
}
|
||||
|
||||
private void deinitEGL() {
|
||||
if (_egl_display != EGL10.EGL_NO_DISPLAY) {
|
||||
_egl.eglMakeCurrent(_egl_display, EGL10.EGL_NO_SURFACE,
|
||||
EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
|
||||
|
||||
if (_egl_surface != EGL10.EGL_NO_SURFACE)
|
||||
_egl.eglDestroySurface(_egl_display, _egl_surface);
|
||||
|
||||
if (_egl_context != EGL10.EGL_NO_CONTEXT)
|
||||
_egl.eglDestroyContext(_egl_display, _egl_context);
|
||||
|
||||
_egl.eglTerminate(_egl_display);
|
||||
}
|
||||
|
||||
_egl_surface = EGL10.EGL_NO_SURFACE;
|
||||
_egl_context = EGL10.EGL_NO_CONTEXT;
|
||||
_egl_config = null;
|
||||
_egl_display = EGL10.EGL_NO_DISPLAY;
|
||||
_egl = null;
|
||||
}
|
||||
|
||||
private void initAudio() throws Exception {
|
||||
_sample_rate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);
|
||||
_buffer_size = AudioTrack.getMinBufferSize(_sample_rate,
|
||||
AudioFormat.CHANNEL_OUT_STEREO,
|
||||
AudioFormat.ENCODING_PCM_16BIT);
|
||||
|
||||
// ~50ms
|
||||
int buffer_size_want = (_sample_rate * 2 * 2 / 20) & ~1023;
|
||||
|
||||
if (_buffer_size < buffer_size_want) {
|
||||
Log.w(LOG_TAG, String.format(Locale.ROOT,
|
||||
"adjusting audio buffer size (was: %d)", _buffer_size));
|
||||
|
||||
_buffer_size = buffer_size_want;
|
||||
}
|
||||
|
||||
Log.i(LOG_TAG, String.format(Locale.ROOT, "Using %d bytes buffer for %dHz audio",
|
||||
_buffer_size, _sample_rate));
|
||||
|
||||
CompatHelpers.AudioTrackCompat.AudioTrackCompatReturn audioTrackRet =
|
||||
CompatHelpers.AudioTrackCompat.make(_sample_rate, _buffer_size);
|
||||
_audio_track = audioTrackRet.audioTrack;
|
||||
_buffer_size = audioTrackRet.bufferSize;
|
||||
|
||||
if (_audio_track.getState() != AudioTrack.STATE_INITIALIZED)
|
||||
throw new Exception(
|
||||
String.format(Locale.ROOT, "Error initializing AudioTrack: %d",
|
||||
_audio_track.getState()));
|
||||
}
|
||||
|
||||
private void deinitAudio() {
|
||||
if (_audio_track != null)
|
||||
_audio_track.release();
|
||||
|
||||
_audio_track = null;
|
||||
_buffer_size = 0;
|
||||
_sample_rate = 0;
|
||||
}
|
||||
|
||||
private static final int[] s_eglAttribs = {
|
||||
EGL10.EGL_CONFIG_ID,
|
||||
EGL10.EGL_BUFFER_SIZE,
|
||||
EGL10.EGL_RED_SIZE,
|
||||
EGL10.EGL_GREEN_SIZE,
|
||||
EGL10.EGL_BLUE_SIZE,
|
||||
EGL10.EGL_ALPHA_SIZE,
|
||||
EGL10.EGL_CONFIG_CAVEAT,
|
||||
EGL10.EGL_DEPTH_SIZE,
|
||||
EGL10.EGL_LEVEL,
|
||||
EGL10.EGL_MAX_PBUFFER_WIDTH,
|
||||
EGL10.EGL_MAX_PBUFFER_HEIGHT,
|
||||
EGL10.EGL_MAX_PBUFFER_PIXELS,
|
||||
EGL10.EGL_NATIVE_RENDERABLE,
|
||||
EGL10.EGL_NATIVE_VISUAL_ID,
|
||||
EGL10.EGL_NATIVE_VISUAL_TYPE,
|
||||
EGL10.EGL_SAMPLE_BUFFERS,
|
||||
EGL10.EGL_SAMPLES,
|
||||
EGL10.EGL_STENCIL_SIZE,
|
||||
EGL10.EGL_SURFACE_TYPE,
|
||||
EGL10.EGL_TRANSPARENT_TYPE,
|
||||
EGL10.EGL_TRANSPARENT_RED_VALUE,
|
||||
EGL10.EGL_TRANSPARENT_GREEN_VALUE,
|
||||
EGL10.EGL_TRANSPARENT_BLUE_VALUE,
|
||||
EGL10.EGL_RENDERABLE_TYPE
|
||||
};
|
||||
final private static int EGL_OPENGL_ES_BIT = 1;
|
||||
final private static int EGL_OPENGL_ES2_BIT = 4;
|
||||
|
||||
final private class EglAttribs {
|
||||
|
||||
LinkedHashMap<Integer, Integer> _lhm;
|
||||
|
||||
public EglAttribs(EGLConfig config) {
|
||||
_lhm = new LinkedHashMap<>(s_eglAttribs.length);
|
||||
|
||||
int[] value = new int[1];
|
||||
|
||||
// prevent throwing IllegalArgumentException
|
||||
if (_egl_display == null || config == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i : s_eglAttribs) {
|
||||
_egl.eglGetConfigAttrib(_egl_display, config, i, value);
|
||||
|
||||
_lhm.put(i, value[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private int weightBits(int attr, int size) {
|
||||
final int value = get(attr);
|
||||
|
||||
int score = 0;
|
||||
|
||||
if (value == size || (size > 0 && value > size))
|
||||
score += 10;
|
||||
|
||||
// penalize for wasted bits
|
||||
if (value > size)
|
||||
score -= value - size;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
public int weight() {
|
||||
int score = 10000;
|
||||
|
||||
if (get(EGL10.EGL_CONFIG_CAVEAT) != EGL10.EGL_NONE)
|
||||
score -= 1000;
|
||||
|
||||
// If there is a config with EGL_OPENGL_ES2_BIT it must be favored
|
||||
// This attribute can only be checked with EGL 1.3 but it may be present on older versions
|
||||
if ((get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES2_BIT) > 0)
|
||||
score += 5000;
|
||||
|
||||
// less MSAA is better
|
||||
score -= get(EGL10.EGL_SAMPLES) * 100;
|
||||
|
||||
// Must be at least 565, but then smaller is better
|
||||
score += weightBits(EGL10.EGL_RED_SIZE, 5);
|
||||
score += weightBits(EGL10.EGL_GREEN_SIZE, 6);
|
||||
score += weightBits(EGL10.EGL_BLUE_SIZE, 5);
|
||||
score += weightBits(EGL10.EGL_ALPHA_SIZE, 0);
|
||||
// Prefer 24 bits depth
|
||||
score += weightBits(EGL10.EGL_DEPTH_SIZE, 24);
|
||||
score += weightBits(EGL10.EGL_STENCIL_SIZE, 8);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String toString() {
|
||||
String s;
|
||||
|
||||
if (get(EGL10.EGL_ALPHA_SIZE) > 0)
|
||||
s = String.format(Locale.ROOT, "[%d] RGBA%d%d%d%d",
|
||||
get(EGL10.EGL_CONFIG_ID),
|
||||
get(EGL10.EGL_RED_SIZE),
|
||||
get(EGL10.EGL_GREEN_SIZE),
|
||||
get(EGL10.EGL_BLUE_SIZE),
|
||||
get(EGL10.EGL_ALPHA_SIZE));
|
||||
else
|
||||
s = String.format(Locale.ROOT, "[%d] RGB%d%d%d",
|
||||
get(EGL10.EGL_CONFIG_ID),
|
||||
get(EGL10.EGL_RED_SIZE),
|
||||
get(EGL10.EGL_GREEN_SIZE),
|
||||
get(EGL10.EGL_BLUE_SIZE));
|
||||
|
||||
if (get(EGL10.EGL_DEPTH_SIZE) > 0)
|
||||
s += String.format(Locale.ROOT, " D%d", get(EGL10.EGL_DEPTH_SIZE));
|
||||
|
||||
if (get(EGL10.EGL_STENCIL_SIZE) > 0)
|
||||
s += String.format(Locale.ROOT, " S%d", get(EGL10.EGL_STENCIL_SIZE));
|
||||
|
||||
if (get(EGL10.EGL_SAMPLES) > 0)
|
||||
s += String.format(Locale.ROOT, " MSAAx%d", get(EGL10.EGL_SAMPLES));
|
||||
|
||||
if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_WINDOW_BIT) > 0)
|
||||
s += " W";
|
||||
if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_PBUFFER_BIT) > 0)
|
||||
s += " P";
|
||||
if ((get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_PIXMAP_BIT) > 0)
|
||||
s += " X";
|
||||
|
||||
if ((get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES_BIT) > 0)
|
||||
s += " ES";
|
||||
if ((get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES2_BIT) > 0)
|
||||
s += " ES2";
|
||||
|
||||
|
||||
switch (get(EGL10.EGL_CONFIG_CAVEAT)) {
|
||||
case EGL10.EGL_NONE:
|
||||
break;
|
||||
|
||||
case EGL10.EGL_SLOW_CONFIG:
|
||||
s += " SLOW";
|
||||
break;
|
||||
|
||||
case EGL10.EGL_NON_CONFORMANT_CONFIG:
|
||||
s += " NON_CONFORMANT";
|
||||
|
||||
default:
|
||||
s += String.format(Locale.ROOT, " unknown CAVEAT 0x%x",
|
||||
get(EGL10.EGL_CONFIG_CAVEAT));
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
public Integer get(Integer key) {
|
||||
if (_lhm.containsKey(key) && _lhm.get(key) != null) {
|
||||
return _lhm.get(key);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private EGLConfig chooseEglConfig(EGLConfig[] configs, int[] version) {
|
||||
EGLConfig res = configs[0];
|
||||
int bestScore = -1;
|
||||
|
||||
Log.d(LOG_TAG, "EGL configs:");
|
||||
|
||||
for (EGLConfig config : configs) {
|
||||
if (config != null) {
|
||||
boolean good = true;
|
||||
|
||||
EglAttribs attr = new EglAttribs(config);
|
||||
|
||||
// must have
|
||||
if ((attr.get(EGL10.EGL_SURFACE_TYPE) & EGL10.EGL_WINDOW_BIT) == 0)
|
||||
good = false;
|
||||
|
||||
if (version[0] >= 2 ||
|
||||
(version[0] == 1 && version[1] >= 3)) {
|
||||
// EGL_OPENGL_ES2_BIT is only supported since EGL 1.3
|
||||
if ((attr.get(EGL10.EGL_RENDERABLE_TYPE) & EGL_OPENGL_ES2_BIT) == 0)
|
||||
good = false;
|
||||
}
|
||||
if (attr.get(EGL10.EGL_BUFFER_SIZE) < bitsPerPixel)
|
||||
good = false;
|
||||
|
||||
// Force a config with a depth buffer and a stencil buffer when rendering directly on backbuffer
|
||||
if ((attr.get(EGL10.EGL_DEPTH_SIZE) == 0) || (attr.get(EGL10.EGL_STENCIL_SIZE) == 0))
|
||||
good = false;
|
||||
|
||||
int score = attr.weight();
|
||||
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "%s (%d, %s)", attr.toString(), score, good ? "OK" : "NOK"));
|
||||
|
||||
if (!good) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
res = config;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestScore < 0)
|
||||
Log.e(LOG_TAG,
|
||||
"Unable to find an acceptable EGL config, expect badness.");
|
||||
|
||||
Log.d(LOG_TAG, String.format(Locale.ROOT, "Chosen EGL config: %s",
|
||||
new EglAttribs(res).toString()));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static {
|
||||
// // For grabbing with gdb...
|
||||
// final boolean sleep_for_debugger = false;
|
||||
// if (sleep_for_debugger) {
|
||||
// try {
|
||||
// Thread.sleep(20 * 1000);
|
||||
// } catch (InterruptedException ignored) {
|
||||
// }
|
||||
// }
|
||||
|
||||
System.loadLibrary("scummvm");
|
||||
}
|
||||
}
|
||||
2404
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
Normal file
2404
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
Normal file
File diff suppressed because it is too large
Load Diff
1035
backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java
Normal file
1035
backends/platform/android/org/scummvm/scummvm/ScummVMEvents.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,488 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.scummvm.scummvm.zip.ZipEntry;
|
||||
import org.scummvm.scummvm.zip.ZipFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class ShortcutCreatorActivity extends Activity implements CompatHelpers.SystemInsets.SystemInsetsListener {
|
||||
final protected static String LOG_TAG = "ShortcutCreatorActivity";
|
||||
|
||||
private IconsCache _cache;
|
||||
|
||||
static void pushShortcut(Context context, String gameId, Intent intent) {
|
||||
Map<String, Map<String, String>> parsedIniMap;
|
||||
try (FileReader reader = new FileReader(new File(context.getFilesDir(), "scummvm.ini"))) {
|
||||
parsedIniMap = INIParser.parse(reader);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
parsedIniMap = null;
|
||||
} catch(IOException ignored) {
|
||||
parsedIniMap = null;
|
||||
}
|
||||
|
||||
if (parsedIniMap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Game game = Game.loadGame(parsedIniMap, gameId);
|
||||
if (game == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileInputStream defaultStream = openFile(new File(context.getFilesDir(), "gui-icons.dat"));
|
||||
|
||||
File iconsPath = INIParser.getPath(parsedIniMap, "scummvm", "iconspath",
|
||||
new File(context.getFilesDir(), "icons"));
|
||||
FileInputStream[] packsStream = openFiles(context, iconsPath, "gui-icons.*\\.dat");
|
||||
|
||||
IconsCache cache = new IconsCache(context, defaultStream, packsStream);
|
||||
final Drawable icon = cache.getGameIcon(game);
|
||||
|
||||
CompatHelpers.ShortcutCreator.pushDynamicShortcut(context, game.getTarget(), intent, game.getDescription(), icon, R.drawable.ic_no_game_icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.shortcut_creator_activity);
|
||||
|
||||
CompatHelpers.SystemInsets.registerSystemInsetsListener(findViewById(R.id.shortcut_creator_root), this);
|
||||
|
||||
// We are only here to create a shortcut
|
||||
if (!Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
List<Game> games;
|
||||
Map<String, Map<String, String>> parsedIniMap;
|
||||
try (FileReader reader = new FileReader(new File(getFilesDir(), "scummvm.ini"))) {
|
||||
parsedIniMap = INIParser.parse(reader);
|
||||
} catch(FileNotFoundException ignored) {
|
||||
parsedIniMap = null;
|
||||
} catch(IOException ignored) {
|
||||
parsedIniMap = null;
|
||||
}
|
||||
|
||||
if (parsedIniMap == null) {
|
||||
Toast.makeText(this, R.string.ini_parsing_error, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
games = Game.loadGames(parsedIniMap);
|
||||
|
||||
FileInputStream defaultStream = openFile(new File(getFilesDir(), "gui-icons.dat"));
|
||||
|
||||
File iconsPath = INIParser.getPath(parsedIniMap, "scummvm", "iconspath",
|
||||
new File(getFilesDir(), "icons"));
|
||||
FileInputStream[] packsStream = openFiles(this, iconsPath, "gui-icons.*\\.dat");
|
||||
|
||||
_cache = new IconsCache(this, defaultStream, packsStream);
|
||||
|
||||
final GameAdapter listAdapter = new GameAdapter(this, games, _cache);
|
||||
|
||||
ListView listView = findViewById(R.id.shortcut_creator_games_list);
|
||||
listView.setAdapter(listAdapter);
|
||||
listView.setEmptyView(findViewById(R.id.shortcut_creator_games_list_empty));
|
||||
listView.setOnItemClickListener(_gameClicked);
|
||||
|
||||
EditText searchEdit = findViewById(R.id.shortcut_creator_search_edit);
|
||||
searchEdit.addTextChangedListener(new TextWatcher() {
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) {
|
||||
listAdapter.getFilter().filter(cs.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
|
||||
int arg3) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable arg0) {
|
||||
}
|
||||
});
|
||||
if (games.isEmpty()) {
|
||||
searchEdit.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void systemInsetsUpdated(int[] gestureInsets, int[] systemInsets, int[] cutoutInsets) {
|
||||
LinearLayout root = findViewById(R.id.shortcut_creator_root);
|
||||
// Ignore bottom as we have our list which can overflow
|
||||
root.setPadding(
|
||||
Math.max(systemInsets[0], cutoutInsets[0]),
|
||||
Math.max(systemInsets[1], cutoutInsets[1]),
|
||||
Math.max(systemInsets[2], cutoutInsets[2]), 0);
|
||||
}
|
||||
|
||||
static private FileInputStream openFile(File path) {
|
||||
try {
|
||||
return new FileInputStream(path);
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static private FileInputStream[] openFiles(Context context, File basePath, String regex) {
|
||||
SAFFSTree.PathResult pr;
|
||||
try {
|
||||
pr = SAFFSTree.fullPathToNode(context, basePath.getPath(), false);
|
||||
} catch (FileNotFoundException e) {
|
||||
return new FileInputStream[0];
|
||||
}
|
||||
|
||||
// This version check is only to make Android Studio linter happy
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// This is a standard filesystem path
|
||||
File[] children = basePath.listFiles((dir, name) -> name.matches(regex));
|
||||
if (children == null) {
|
||||
return new FileInputStream[0];
|
||||
}
|
||||
Arrays.sort(children);
|
||||
FileInputStream[] ret = new FileInputStream[children.length];
|
||||
int i = 0;
|
||||
for (File f: children) {
|
||||
ret[i] = openFile(f);
|
||||
i += 1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// This is a SAF fake mount point
|
||||
SAFFSTree.SAFFSNode[] children = pr.tree.getChildren(pr.node);
|
||||
if (children == null) {
|
||||
return new FileInputStream[0];
|
||||
}
|
||||
Arrays.sort(children);
|
||||
|
||||
ArrayList<FileInputStream> ret = new ArrayList<>();
|
||||
for (SAFFSTree.SAFFSNode child : children) {
|
||||
if ((child._flags & SAFFSTree.SAFFSNode.DIRECTORY) != 0) {
|
||||
continue;
|
||||
}
|
||||
String component = child._path.substring(child._path.lastIndexOf('/') + 1);
|
||||
if (!component.matches(regex)) {
|
||||
continue;
|
||||
}
|
||||
ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(child, "r");
|
||||
if (pfd == null) {
|
||||
continue;
|
||||
}
|
||||
ret.add(new ParcelFileDescriptor.AutoCloseInputStream(pfd));
|
||||
}
|
||||
return ret.toArray(new FileInputStream[0]);
|
||||
}
|
||||
|
||||
private final OnItemClickListener _gameClicked = new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> a, View v, int position, long id) {
|
||||
Game game = (Game)a.getItemAtPosition(position);
|
||||
final Drawable icon = _cache.getGameIcon(game);
|
||||
|
||||
// Display a customization dialog to let the user change (shorten?) the title
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(ShortcutCreatorActivity.this);
|
||||
builder.setTitle("Title");
|
||||
View fragment = LayoutInflater.from(ShortcutCreatorActivity.this).inflate(R.layout.shortcut_creator_customize, null);
|
||||
final EditText desc = fragment.findViewById(R.id.shortcut_creator_customize_game_description);
|
||||
desc.setText(game.getDescription());
|
||||
final ImageView iconView = fragment.findViewById(R.id.shortcut_creator_customize_game_icon);
|
||||
Drawable displayedIcon = icon;
|
||||
if (displayedIcon == null) {
|
||||
displayedIcon = CompatHelpers.DrawableCompat.getDrawable(ShortcutCreatorActivity.this, R.drawable.ic_no_game_icon);
|
||||
}
|
||||
iconView.setImageDrawable(displayedIcon);
|
||||
builder.setView(fragment);
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
String label = desc.getText().toString();
|
||||
// Generate an id which depends on the user description
|
||||
// Without this, if the user changes the description but already has the same shortcut (also in the dynamic ones), the other label will be reused
|
||||
String shortcutId = game.getTarget() + String.format("-%08x", label.hashCode());
|
||||
|
||||
Intent shortcut = new Intent(Intent.ACTION_MAIN, Uri.fromParts("scummvm", game.getTarget(), null),
|
||||
ShortcutCreatorActivity.this, SplashActivity.class);
|
||||
Intent result = CompatHelpers.ShortcutCreator.createShortcutResultIntent(ShortcutCreatorActivity.this, shortcutId, shortcut,
|
||||
desc.getText().toString(), icon, R.drawable.ic_no_game_icon);
|
||||
setResult(RESULT_OK, result);
|
||||
|
||||
finish();
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) ->
|
||||
dialog.cancel());
|
||||
|
||||
final AlertDialog dialog = builder.create();
|
||||
desc.setOnEditorActionListener((TextView tv, int actionId, KeyEvent event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
};
|
||||
|
||||
private static class Game {
|
||||
@NonNull
|
||||
private final String _target;
|
||||
private final String _engineid;
|
||||
private final String _gameid;
|
||||
@NonNull
|
||||
private final String _description;
|
||||
|
||||
private Game(@NonNull String target, String engineid, String gameid, @NonNull String description) {
|
||||
_target = target;
|
||||
_engineid = engineid;
|
||||
_gameid = gameid;
|
||||
_description = description;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getTarget() {
|
||||
return _target;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getDescription() {
|
||||
return _description;
|
||||
}
|
||||
|
||||
public Collection<String> getIconCandidates() {
|
||||
if (_engineid == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
ArrayList<String> ret = new ArrayList<>();
|
||||
if (_gameid != null) {
|
||||
ret.add(String.format("icons/%s-%s.png", _engineid, _gameid).toLowerCase());
|
||||
}
|
||||
ret.add(String.format("icons/%s.png", _engineid).toLowerCase());
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static Game loadGame(@NonNull Map<String, Map<String, String>> parsedIniMap, String target) {
|
||||
Map<String, String> domain = parsedIniMap.get(target);
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
String engineid = domain.get("engineid");
|
||||
String gameid = domain.get("gameid");
|
||||
String description = domain.get("description");
|
||||
if (description == null) {
|
||||
return null;
|
||||
}
|
||||
return new Game(target, engineid, gameid, description);
|
||||
}
|
||||
|
||||
public static List<Game> loadGames(@NonNull Map<String, Map<String, String>> parsedIniMap) {
|
||||
List<Game> games = new ArrayList<>();
|
||||
for (Map.Entry<String, Map<String, String>> entry : parsedIniMap.entrySet()) {
|
||||
final String domain = entry.getKey();
|
||||
if (domain == null ||
|
||||
"scummvm".equals(domain) ||
|
||||
"cloud".equals(domain) ||
|
||||
"keymapper".equals(domain)) {
|
||||
continue;
|
||||
}
|
||||
String engineid = entry.getValue().get("engineid");
|
||||
String gameid = entry.getValue().get("gameid");
|
||||
String description = entry.getValue().get("description");
|
||||
if (description == null) {
|
||||
continue;
|
||||
}
|
||||
games.add(new Game(domain, engineid, gameid, description));
|
||||
}
|
||||
return games;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return _description;
|
||||
}
|
||||
}
|
||||
|
||||
private static class IconsCache {
|
||||
/**
|
||||
* This kind of mimics Common::generateZipSet
|
||||
*/
|
||||
private final Context _context;
|
||||
private final Map<String, byte[]> _icons = new LinkedHashMap<String, byte[]>(16,0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
|
||||
return size() > 128;
|
||||
}
|
||||
};
|
||||
private static final byte[] _noIconSentinel = new byte[0];
|
||||
|
||||
private final List<ZipFile> _zipFiles = new ArrayList<>();
|
||||
|
||||
public IconsCache(Context context,
|
||||
FileInputStream defaultStream,
|
||||
FileInputStream[] packsStream) {
|
||||
_context = context;
|
||||
|
||||
for (int i = packsStream.length - 1; i >= 0; i--) {
|
||||
final FileInputStream packStream = packsStream[i];
|
||||
if (packStream == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ZipFile zf = new ZipFile(packStream);
|
||||
_zipFiles.add(zf);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error while loading pack ZipFile: " + i, e);
|
||||
}
|
||||
}
|
||||
if (defaultStream != null) {
|
||||
try {
|
||||
ZipFile zf = new ZipFile(defaultStream);
|
||||
_zipFiles.add(zf);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error while loading default ZipFile", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Drawable getGameIcon(Game game) {
|
||||
for (String name : game.getIconCandidates()) {
|
||||
byte[] data = _icons.get(name);
|
||||
if (data == null) {
|
||||
data = loadIcon(name);
|
||||
}
|
||||
if (data == _noIconSentinel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
if (bitmap == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new BitmapDrawable(_context.getResources(), bitmap);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private byte[] loadIcon(String name) {
|
||||
int zfi = 0;
|
||||
for(ZipFile zf : _zipFiles) {
|
||||
final ZipEntry ze = zf.getEntry(name);
|
||||
if (ze == null) {
|
||||
zfi++;
|
||||
continue;
|
||||
}
|
||||
|
||||
int sz = (int) ze.getSize();
|
||||
byte[] buffer = new byte[sz];
|
||||
|
||||
try (InputStream is = zf.getInputStream(ze)) {
|
||||
if (is.read(buffer) != buffer.length) {
|
||||
throw new IOException();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Error while uncompressing: " + name + " from zip file " + zfi, e);
|
||||
zfi++;
|
||||
continue;
|
||||
}
|
||||
|
||||
_icons.put(name, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// Register failure
|
||||
_icons.put(name, _noIconSentinel);
|
||||
return _noIconSentinel;
|
||||
}
|
||||
}
|
||||
|
||||
private static class GameAdapter extends ArrayAdapter<Game> {
|
||||
private final IconsCache _cache;
|
||||
|
||||
public GameAdapter(Context context,
|
||||
List<Game> items,
|
||||
IconsCache cache) {
|
||||
super(context, 0, items);
|
||||
Collections.sort(items, (lhs, rhs) -> lhs.getDescription().compareToIgnoreCase(rhs.getDescription()));
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent)
|
||||
{
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.shortcut_creator_game_list_item, parent, false);
|
||||
}
|
||||
final Game game = getItem(position);
|
||||
assert game != null;
|
||||
|
||||
final TextView desc = convertView.findViewById(R.id.shortcut_creator_game_item_description);
|
||||
desc.setText(game.getDescription());
|
||||
final ImageView iconView = convertView.findViewById(R.id.shortcut_creator_game_item_icon);
|
||||
Drawable icon = _cache.getGameIcon(game);
|
||||
if (icon == null) {
|
||||
icon = CompatHelpers.DrawableCompat.getDrawable(getContext(), R.drawable.ic_no_game_icon);
|
||||
}
|
||||
iconView.setImageDrawable(icon);
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SplashActivity extends Activity {
|
||||
|
||||
/**
|
||||
* Ids to identify an external storage read (and write) request.
|
||||
* They are app-defined int constants. The callback method gets the result of the request.
|
||||
* Ie. returned in the Activity's onRequestPermissionsResult()
|
||||
*/
|
||||
private static final int MY_PERMISSION_ALL = 110;
|
||||
|
||||
private static final String[] MY_PERMISSIONS_STR_LIST = {
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
};
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
&& (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
|
||||
|| checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)
|
||||
) {
|
||||
// In Tiramisu (API 33) and above, READ and WRITE external storage permissions have no effect,
|
||||
// and they are automatically denied -- onRequestPermissionsResult() will be called without user's input
|
||||
requestPermissions(MY_PERMISSIONS_STR_LIST, MY_PERMISSION_ALL);
|
||||
} else {
|
||||
Intent next = new Intent(this, ScummVMActivity.class);
|
||||
next.fillIn(getIntent(), Intent.FILL_IN_ACTION | Intent.FILL_IN_DATA);
|
||||
startActivity(next);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
CompatHelpers.HideSystemStatusBar.hide(getWindow());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == MY_PERMISSION_ALL) {
|
||||
int numOfReqPermsGranted = 0;
|
||||
// If request is canceled, the result arrays are empty.
|
||||
for (int i = 0; i < grantResults.length; ++i) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i(ScummVM.LOG_TAG, permissions[i] + " permission was granted at Runtime");
|
||||
++numOfReqPermsGranted;
|
||||
} else {
|
||||
Log.i(ScummVM.LOG_TAG, permissions[i] + " permission was denied at Runtime");
|
||||
}
|
||||
}
|
||||
|
||||
if (numOfReqPermsGranted != grantResults.length) {
|
||||
// permission denied! We won't be able to make use of functionality depending on this permission.
|
||||
Toast.makeText(this, "Until permission is granted, some storage locations may be inaccessible for r/w!", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
startActivity(new Intent(this, ScummVMActivity.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus) {
|
||||
CompatHelpers.HideSystemStatusBar.hide(getWindow());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
}
|
||||
|
||||
}
|
||||
74
backends/platform/android/org/scummvm/scummvm/Version.java
Normal file
74
backends/platform/android/org/scummvm/scummvm/Version.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package org.scummvm.scummvm;
|
||||
|
||||
// Based on code from: https://stackoverflow.com/a/11024200
|
||||
public class Version implements Comparable<Version> {
|
||||
|
||||
private final String versionOnlyDigits;
|
||||
private final String versionDescription;
|
||||
|
||||
public final String getDescription() {
|
||||
return this.versionDescription;
|
||||
}
|
||||
|
||||
public final String get() {
|
||||
return this.versionOnlyDigits;
|
||||
}
|
||||
|
||||
public Version(String version) {
|
||||
if(version == null) {
|
||||
this.versionOnlyDigits = "0";
|
||||
this.versionDescription = "0";
|
||||
} else {
|
||||
this.versionDescription = version;
|
||||
// cleanup from any non-digit characters in the version string
|
||||
final String strippedVersion = version.replaceAll("[^\\d.]", "");
|
||||
if (!strippedVersion.matches("[0-9]+(\\.[0-9]+)*")) {
|
||||
this.versionOnlyDigits = "0";
|
||||
} else {
|
||||
this.versionOnlyDigits = strippedVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here a version is considered "dirty" if it contains other characters in the description string than the expected digits (and dots) of a "clean" proper version
|
||||
// eg. 2.3.0pre or 2.3.0git or 2.3.0git9272-gc71ac4748b are dirty
|
||||
// 2.3.0 is NOT dirty
|
||||
public boolean isDirty() {
|
||||
return (versionOnlyDigits.compareTo(versionDescription) != 0);
|
||||
}
|
||||
|
||||
|
||||
@Override public int compareTo(Version that) {
|
||||
if(that == null)
|
||||
return 1;
|
||||
String[] thisParts = this.get().split("\\.");
|
||||
String[] thatParts = that.get().split("\\.");
|
||||
int length = Math.max(thisParts.length, thatParts.length);
|
||||
try {
|
||||
for (int i = 0; i < length; i++) {
|
||||
int thisPart = i < thisParts.length ?
|
||||
Integer.parseInt(thisParts[i]) : 0;
|
||||
int thatPart = i < thatParts.length ?
|
||||
Integer.parseInt(thatParts[i]) : 0;
|
||||
if (thisPart < thatPart)
|
||||
return -1;
|
||||
if (thisPart > thatPart)
|
||||
return 1;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object that) {
|
||||
if(this == that)
|
||||
return true;
|
||||
if(that == null)
|
||||
return false;
|
||||
if(this.getClass() != that.getClass())
|
||||
return false;
|
||||
return this.compareTo((Version) that) == 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class HTTPManager {
|
||||
protected ExecutorService _executor;
|
||||
protected ArrayBlockingQueue<Runnable> _queue;
|
||||
protected boolean _empty;
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI (main ScummVM thread)
|
||||
*/
|
||||
public HTTPManager() {
|
||||
TLSSocketFactory.init();
|
||||
|
||||
_executor = Executors.newCachedThreadPool();
|
||||
// Use a capacity to make sure the queue is checked on a regular basis
|
||||
_queue = new ArrayBlockingQueue<>(50);
|
||||
_empty = true;
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI (main ScummVM thread)
|
||||
*/
|
||||
public void startRequest(HTTPRequest request) {
|
||||
request._manager = this;
|
||||
_executor.execute(request);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI (main ScummVM thread)
|
||||
*/
|
||||
public void poll() {
|
||||
Runnable r;
|
||||
while((r = _queue.poll()) != null) {
|
||||
r.run();
|
||||
}
|
||||
// The read is never synchronized but at least we ensure here that we don't miss any event
|
||||
synchronized(this) {
|
||||
_empty = _queue.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// Called from workers
|
||||
void enqueue(Runnable r) {
|
||||
while(true) {
|
||||
try {
|
||||
_queue.put(r);
|
||||
synchronized(this) {
|
||||
_empty = false;
|
||||
}
|
||||
return;
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import org.scummvm.scummvm.SAFFSTree;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.SequenceInputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class HTTPRequest implements Runnable {
|
||||
final static String LOG_TAG = "ScummVM.HTTP";
|
||||
private static final int DEFAULT_BUFFER_SIZE = 16384;
|
||||
|
||||
HTTPManager _manager;
|
||||
|
||||
protected String _method;
|
||||
protected AtomicReference<String> _url;
|
||||
protected TreeMap<String, String> _requestHeaders;
|
||||
protected InputStream _uploadBuffer;
|
||||
protected int _uploadBufferLength;
|
||||
|
||||
protected long _nativePointer;
|
||||
|
||||
protected AtomicBoolean _cancelled;
|
||||
|
||||
protected native void gotHeaders(long nativePointer, String[] headers);
|
||||
protected native void gotData(long nativePointer, byte[] data, int size, int totalSize);
|
||||
protected native void finished(long nativePointer, int errorCode, String errorMsg);
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI
|
||||
*/
|
||||
public HTTPRequest(long nativePointer, String url, String[] requestHeaders, byte[] uploadBuffer, boolean uploading, boolean usingPatch, boolean post) {
|
||||
init(nativePointer, url);
|
||||
setupUploadBuffer(uploadBuffer, uploading, usingPatch, post);
|
||||
setupHeaders(requestHeaders);
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI
|
||||
*/
|
||||
public HTTPRequest(long nativePointer, String url, String[] requestHeaders, String[] formFields, String[] formFiles) {
|
||||
init(nativePointer, url);
|
||||
setupMultipartForm(formFields, formFiles);
|
||||
setupHeaders(requestHeaders);
|
||||
}
|
||||
|
||||
private void init(long nativePointer, String url) {
|
||||
_nativePointer = nativePointer;
|
||||
_url = new AtomicReference<>(url);
|
||||
_requestHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
_cancelled = new AtomicBoolean(false);
|
||||
_method = "GET";
|
||||
}
|
||||
|
||||
private void setupUploadBuffer(byte[] uploadBuffer, boolean uploading, boolean usingPatch, boolean post) {
|
||||
_uploadBuffer = null;
|
||||
_uploadBufferLength = 0;
|
||||
if (uploading) {
|
||||
if (uploadBuffer == null) {
|
||||
uploadBuffer = new byte[0];
|
||||
}
|
||||
_uploadBuffer = new ByteArrayInputStream(uploadBuffer);
|
||||
_uploadBufferLength = uploadBuffer.length;
|
||||
_method = "PUT";
|
||||
return;
|
||||
}
|
||||
|
||||
if (usingPatch) {
|
||||
_method = "PATCH";
|
||||
return;
|
||||
}
|
||||
|
||||
if (uploadBuffer != null && uploadBuffer.length > 0) {
|
||||
_uploadBuffer = new ByteArrayInputStream(uploadBuffer);
|
||||
_uploadBufferLength = uploadBuffer.length;
|
||||
post = true;
|
||||
}
|
||||
|
||||
if (post) {
|
||||
_method = "POST";
|
||||
_requestHeaders.put("content-type", "application/x-www-form-urlencoded");
|
||||
}
|
||||
}
|
||||
|
||||
private void setupMultipartForm(String[] formFields, String[] formFiles) {
|
||||
if ((formFields.length & 1) != 0) {
|
||||
throw new IllegalArgumentException("formFields has odd length");
|
||||
}
|
||||
if ((formFiles.length & 1) != 0) {
|
||||
throw new IllegalArgumentException("formFiles has odd length");
|
||||
}
|
||||
|
||||
SecureRandom rnd = new SecureRandom();
|
||||
String boundary = "ScummVM-Boundary-" + (new BigInteger(128, rnd)).toString(10);
|
||||
|
||||
int contentLength = 0;
|
||||
Vector<InputStream> bodyParts = new Vector<>(formFiles.length * 2 + 2);
|
||||
|
||||
_method = "POST";
|
||||
_requestHeaders.put("content-type", String.format("multipart/form-data; boundary=%s", boundary));
|
||||
|
||||
StringBuilder formFieldsContent = new StringBuilder();
|
||||
for (int i = 0; i < formFields.length; i += 2) {
|
||||
formFieldsContent.append(String.format("\r\n--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n", boundary, formFields[i]));
|
||||
formFieldsContent.append(formFields[i+1]);
|
||||
}
|
||||
for (int i = 0; i < formFiles.length; i += 2) {
|
||||
File file = new File(formFiles[i+1]);
|
||||
formFieldsContent.append(String.format("\r\n--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n\r\n", boundary, formFiles[i], file.getName()));
|
||||
|
||||
byte[] textContent = formFieldsContent.toString().getBytes(Charset.defaultCharset());
|
||||
bodyParts.add(new ByteArrayInputStream(textContent));
|
||||
if (contentLength >= 0) {
|
||||
try {
|
||||
contentLength = Math.addExact(contentLength, textContent.length);
|
||||
} catch (ArithmeticException e) {
|
||||
contentLength = -1;
|
||||
}
|
||||
}
|
||||
formFieldsContent = new StringBuilder();
|
||||
|
||||
try {
|
||||
SAFFSTree.PathResult pr = SAFFSTree.fullPathToNode(null, file.getPath(), false);
|
||||
if (pr == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
bodyParts.add(new FileInputStream(file));
|
||||
long length = file.length();
|
||||
if (!file.isFile() || length >= Integer.MAX_VALUE) {
|
||||
contentLength = -1;
|
||||
} else if (contentLength >= 0) {
|
||||
try {
|
||||
contentLength = Math.addExact(contentLength, (int)length);
|
||||
} catch (ArithmeticException e) {
|
||||
contentLength = -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ParcelFileDescriptor pfd = pr.tree.createFileDescriptor(pr.node, "r");
|
||||
bodyParts.add(new ParcelFileDescriptor.AutoCloseInputStream(pfd));
|
||||
contentLength = -1;
|
||||
}
|
||||
} catch (FileNotFoundException ignored) {
|
||||
// We can't trigger an error now: we will make sure we call finished later with an error
|
||||
bodyParts.add(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Now we only have to close the multipart with an ending boundary
|
||||
formFieldsContent.append(String.format("\r\n--%s--\r\n", boundary));
|
||||
byte[] textContent = formFieldsContent.toString().getBytes(Charset.defaultCharset());
|
||||
bodyParts.add(new ByteArrayInputStream(textContent));
|
||||
if (contentLength >= 0) {
|
||||
try {
|
||||
contentLength = Math.addExact(contentLength, textContent.length);
|
||||
} catch (ArithmeticException e) {
|
||||
contentLength = -1;
|
||||
}
|
||||
}
|
||||
_uploadBuffer = new SequenceInputStream(bodyParts.elements());
|
||||
_uploadBufferLength = contentLength;
|
||||
}
|
||||
|
||||
private void setupHeaders(String[] requestHeaders) {
|
||||
if ((requestHeaders.length & 1) != 0) {
|
||||
throw new IllegalArgumentException("requestHeaders has odd length");
|
||||
}
|
||||
for(int i = 0; i < requestHeaders.length; i += 2) {
|
||||
if (requestHeaders[i] == null) {
|
||||
// If there were invalid headers passed in native code
|
||||
// we end up with null entries at the end of the array
|
||||
return;
|
||||
}
|
||||
_requestHeaders.put(requestHeaders[i], requestHeaders[i+1]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @noinspection unused
|
||||
* Called from JNI
|
||||
*/
|
||||
public void cancel() {
|
||||
_cancelled.set(true);
|
||||
// Don't notify the native object if we got cancelled: it may have been reused
|
||||
_nativePointer = 0;
|
||||
}
|
||||
|
||||
public String getURL() {
|
||||
return _url.get();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (_uploadBuffer != null) {
|
||||
try {
|
||||
_uploadBuffer.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on HTTPManager thread pool
|
||||
@Override
|
||||
public void run() {
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
URL url;
|
||||
HttpURLConnection urlConnection;
|
||||
try {
|
||||
url = new URL(_url.get());
|
||||
Log.d(LOG_TAG, String.format("Will make HTTP request to %s with method %s", url, _method));
|
||||
urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod(_method);
|
||||
} catch (IOException e) {
|
||||
final String errorMsg = e.getMessage();
|
||||
_manager.enqueue(() -> finished(_nativePointer, -1, errorMsg));
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
urlConnection.setInstanceFollowRedirects(true);
|
||||
for (Map.Entry<String, String> e : _requestHeaders.entrySet()) {
|
||||
urlConnection.addRequestProperty(e.getKey(), e.getValue());
|
||||
}
|
||||
if (_uploadBuffer != null) {
|
||||
urlConnection.setDoOutput(true);
|
||||
if (_uploadBufferLength != -1) {
|
||||
urlConnection.setFixedLengthStreamingMode(_uploadBufferLength);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
urlConnection.connect();
|
||||
if (_cancelled.get()) {
|
||||
urlConnection.disconnect();
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_uploadBuffer != null) {
|
||||
try (OutputStream out = urlConnection.getOutputStream()) {
|
||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int read;
|
||||
while ((read = _uploadBuffer.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
// We failed to open some file when building the buffer
|
||||
_manager.enqueue(() -> finished(_nativePointer, -1, "Can't open file"));
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_cancelled.get()) {
|
||||
urlConnection.disconnect();
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, java.util.List<String>> headersField = urlConnection.getHeaderFields();
|
||||
if (_cancelled.get()) {
|
||||
urlConnection.disconnect();
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
final Vector<String> headers = new Vector<>(headersField.size() * 2);
|
||||
for (Map.Entry<String, java.util.List<String>> e : headersField.entrySet()) {
|
||||
String key = e.getKey();
|
||||
if (key == null) {
|
||||
// The status line is placed in the map with a null key: ignore it
|
||||
continue;
|
||||
}
|
||||
List<String> values = e.getValue();
|
||||
headers.add(key.toLowerCase(Locale.ROOT));
|
||||
headers.add(values.get(values.size() - 1));
|
||||
}
|
||||
_manager.enqueue(() -> gotHeaders(_nativePointer, headers.toArray(new String[0])));
|
||||
|
||||
int contentLength = urlConnection.getContentLength();
|
||||
|
||||
InputStream in = urlConnection.getInputStream();
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
boolean finished = false;
|
||||
while(!finished) {
|
||||
final byte[] inputData = new byte[DEFAULT_BUFFER_SIZE];
|
||||
int offset = 0;
|
||||
while(offset < DEFAULT_BUFFER_SIZE) {
|
||||
final int size = in.read(inputData, offset, DEFAULT_BUFFER_SIZE - offset);
|
||||
if (size == -1) {
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
if (_cancelled.get()) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
|
||||
final int offset_ = offset;
|
||||
_manager.enqueue(() -> gotData(_nativePointer, inputData, offset_, contentLength));
|
||||
}
|
||||
// Update URL field
|
||||
url = urlConnection.getURL();
|
||||
_url.set(url.toExternalForm());
|
||||
|
||||
final int responseCode = urlConnection.getResponseCode();
|
||||
_manager.enqueue(() -> finished(_nativePointer, responseCode, null));
|
||||
} catch (FileNotFoundException e) {
|
||||
// The server returned an error, return the error code and no data
|
||||
int responseCode = -1;
|
||||
try {
|
||||
responseCode = urlConnection.getResponseCode();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
final int responseCode_ = responseCode;
|
||||
final String errorMsg = e.getMessage();
|
||||
_manager.enqueue(() -> finished(_nativePointer, responseCode_, errorMsg));
|
||||
cleanup();
|
||||
} catch (IOException e) {
|
||||
Log.w(LOG_TAG, "Error when making HTTP request", e);
|
||||
final String errorMsg = e.getMessage();
|
||||
_manager.enqueue(() -> finished(_nativePointer, -1, errorMsg));
|
||||
cleanup();
|
||||
} finally {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
/*
|
||||
* Inspiration taken from http://blog.novoj.net/2016/02/29/how-to-make-apache-httpclient-trust-lets-encrypt-certificate-authority/
|
||||
*/
|
||||
|
||||
class LETrustManager implements X509TrustManager {
|
||||
private static final String[] derLECerts = {
|
||||
/* ISRG Root X1 */ "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=",
|
||||
/* ISRG Root X2 */ "MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn",
|
||||
};
|
||||
|
||||
private static LETrustManager instance;
|
||||
|
||||
private final X509TrustManager _systemTrustManager;
|
||||
private final X509TrustManager _leTrustManager;
|
||||
|
||||
static SSLContext getContext() throws NoSuchAlgorithmException, KeyManagementException {
|
||||
try {
|
||||
if (instance == null) {
|
||||
instance = new LETrustManager();
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace(System.err);
|
||||
return SSLContext.getDefault();
|
||||
}
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, new TrustManager[]{instance}, null);
|
||||
return sslContext;
|
||||
}
|
||||
|
||||
public LETrustManager() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException {
|
||||
final TrustManagerFactory mainTrustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
mainTrustFactory.init((KeyStore)null);
|
||||
this._systemTrustManager = (X509TrustManager)mainTrustFactory.getTrustManagers()[0];
|
||||
|
||||
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
ks.load(null);
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
int i = 1;
|
||||
for (String derCert : derLECerts) {
|
||||
ByteArrayInputStream is = new ByteArrayInputStream(Base64.decode(derCert, Base64.DEFAULT));
|
||||
Certificate cert = cf.generateCertificate(is);
|
||||
ks.setCertificateEntry(String.format(Locale.getDefault(), "%d", i), cert);
|
||||
i++;
|
||||
}
|
||||
|
||||
final TrustManagerFactory leTrustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
leTrustFactory.init(ks);
|
||||
this._leTrustManager = (X509TrustManager)leTrustFactory.getTrustManagers()[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(final X509Certificate[] x509Certificates, final String authType) throws CertificateException {
|
||||
// LE doesn't issue client certificates
|
||||
_systemTrustManager.checkClientTrusted(x509Certificates, authType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(final X509Certificate[] x509Certificates, final String authType) throws CertificateException {
|
||||
try {
|
||||
_systemTrustManager.checkServerTrusted(x509Certificates, authType);
|
||||
} catch(CertificateException ignored) {
|
||||
this._leTrustManager.checkServerTrusted(x509Certificates, authType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
X509Certificate[] systemAccepted = this._systemTrustManager.getAcceptedIssuers();
|
||||
X509Certificate[] leAccepted = this._leTrustManager.getAcceptedIssuers();
|
||||
|
||||
X509Certificate[] allAccepted = Arrays.copyOf(systemAccepted, systemAccepted.length + leAccepted.length);
|
||||
System.arraycopy(leAccepted, 0, allAccepted, systemAccepted.length, leAccepted.length);
|
||||
|
||||
return allAccepted;
|
||||
}
|
||||
}
|
||||
267
backends/platform/android/org/scummvm/scummvm/net/SSocket.java
Normal file
267
backends/platform/android/org/scummvm/scummvm/net/SSocket.java
Normal file
@@ -0,0 +1,267 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
/** @noinspection unused*/
|
||||
public class SSocket {
|
||||
final static String LOG_TAG = "ScummVM";
|
||||
|
||||
protected Socket _socket;
|
||||
|
||||
protected int _buffer = -2;
|
||||
|
||||
public SSocket(String url_) {
|
||||
final URL url;
|
||||
try {
|
||||
url = new URL(url_);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String scheme = url.getProtocol().toLowerCase();
|
||||
if (!scheme.equals("http") && !scheme.equals("https")) {
|
||||
throw new RuntimeException("Unsupported protocol");
|
||||
}
|
||||
|
||||
String host = url.getHost();
|
||||
if (host == null) {
|
||||
throw new RuntimeException("Missing host name");
|
||||
}
|
||||
if (host.contains(":")) {
|
||||
host = String.format("[%s]", host);
|
||||
}
|
||||
int port = url.getPort();
|
||||
if (port == -1) {
|
||||
port = url.getDefaultPort();
|
||||
}
|
||||
|
||||
Socket socket;
|
||||
try {
|
||||
socket = proxyConnect(url, host, port);
|
||||
|
||||
if (scheme.equals("https")) {
|
||||
SSLSocketFactory ssf = new TLSSocketFactory();
|
||||
socket = ssf.createSocket(socket, host, port, true);
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Socket proxyConnect(URL url, String host, int port) throws IOException {
|
||||
Socket ret;
|
||||
|
||||
final URI uri;
|
||||
try {
|
||||
uri = url.toURI();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
SocketAddress sa = new InetSocketAddress(host, port);
|
||||
|
||||
final ProxySelector proxySelector = ProxySelector.getDefault();
|
||||
final List<Proxy> proxies = proxySelector.select(uri);
|
||||
|
||||
IOException lastExc = null;
|
||||
for (Proxy proxy : proxies) {
|
||||
final Proxy.Type proxyType = proxy.type();
|
||||
try {
|
||||
if (proxyType != Proxy.Type.HTTP) {
|
||||
ret = new Socket(proxy);
|
||||
ret.connect(sa);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// HTTP proxy with Socket is not supported on Android
|
||||
// Let's do it ourselves with a CONNECT method
|
||||
|
||||
// First, resolve the proxy address: it's not resolved in Proxy
|
||||
InetSocketAddress proxyAddress = (InetSocketAddress)proxy.address();
|
||||
InetAddress addr = proxyAddress.getAddress();
|
||||
String proxyHost;
|
||||
if (addr != null) {
|
||||
proxyHost = addr.getHostName();
|
||||
} else {
|
||||
proxyHost = proxyAddress.getHostName();
|
||||
}
|
||||
int proxyPort = proxyAddress.getPort();
|
||||
proxyAddress = new InetSocketAddress(proxyHost, proxyPort);
|
||||
|
||||
ret = new Socket();
|
||||
ret.connect(proxyAddress);
|
||||
|
||||
proxyHTTPConnect(ret, host, port);
|
||||
return ret;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while connecting", e);
|
||||
if (proxy.address() != null) {
|
||||
proxySelector.connectFailed(uri, proxy.address(), e);
|
||||
}
|
||||
lastExc = e;
|
||||
}
|
||||
}
|
||||
if (lastExc == null) {
|
||||
throw new RuntimeException("No proxy specified");
|
||||
}
|
||||
throw lastExc;
|
||||
}
|
||||
|
||||
private static void proxyHTTPConnect(Socket socket, String host, int port) throws IOException {
|
||||
String requestLine = String.format(Locale.ROOT, "CONNECT %s:%d HTTP/1.0\r\n\r\n", host, port);
|
||||
socket.getOutputStream().write(requestLine.getBytes());
|
||||
byte[] buffer = readLine(socket);
|
||||
|
||||
// HTTP/1.x SP 2xx SP
|
||||
if (buffer.length < 13 ||
|
||||
buffer[0] != 'H' ||
|
||||
buffer[1] != 'T' ||
|
||||
buffer[2] != 'T' ||
|
||||
buffer[3] != 'P' ||
|
||||
buffer[4] != '/' ||
|
||||
buffer[5] != '1' ||
|
||||
buffer[6] != '.' ||
|
||||
(buffer[7] != '0' && buffer[7] != '1') ||
|
||||
buffer[8] != ' ' ||
|
||||
buffer[9] != '2' ||
|
||||
!Character.isDigit(buffer[10]) ||
|
||||
!Character.isDigit(buffer[11]) ||
|
||||
buffer[12] != ' ') {
|
||||
throw new IOException("Invalid proxy reply");
|
||||
}
|
||||
|
||||
for (int i = 0; i < 64 && buffer.length > 0; i++) {
|
||||
buffer = readLine(socket);
|
||||
}
|
||||
if (buffer.length > 0) {
|
||||
throw new IOException("Invalid proxy reply: too much headers");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readLine(Socket socket) throws IOException {
|
||||
InputStream is = socket.getInputStream();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int b;
|
||||
while (true) {
|
||||
b = is.read();
|
||||
if (b == -1) {
|
||||
return baos.toByteArray();
|
||||
}
|
||||
if (b == '\r') {
|
||||
continue;
|
||||
}
|
||||
if (b == '\n') {
|
||||
return baos.toByteArray();
|
||||
}
|
||||
baos.write(b);
|
||||
}
|
||||
}
|
||||
|
||||
public int ready() {
|
||||
if (_buffer != -2) {
|
||||
// We have at least one byte or an EOF
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
// Set receive timeout to something ridiculously low to mimic a non-blocking socket
|
||||
_socket.setSoTimeout(1);
|
||||
_buffer = _socket.getInputStream().read();
|
||||
return 1;
|
||||
} catch (SocketTimeoutException e) {
|
||||
// Nothing was ready to consume
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while checking ready status", e);
|
||||
// Make it like if there was something ready
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public int send(byte[] data) {
|
||||
try {
|
||||
// Setup unlimited read timeout to allow for SSL exchanges to work
|
||||
_socket.setSoTimeout(0);
|
||||
_socket.getOutputStream().write(data);
|
||||
return data.length;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while sending socket data", e);
|
||||
// This likely failed
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int recv(byte[] data) {
|
||||
if (data.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (_buffer == -1) {
|
||||
_buffer = -2;
|
||||
return -1;
|
||||
}
|
||||
int offset = 0;
|
||||
if (_buffer != -2) {
|
||||
data[0] = (byte)_buffer;
|
||||
offset = 1;
|
||||
_buffer = -2;
|
||||
}
|
||||
try {
|
||||
int recvd = 0;
|
||||
long end = System.currentTimeMillis() + 5000;
|
||||
while (true) {
|
||||
try {
|
||||
// Allow for some timeout but not too much
|
||||
_socket.setSoTimeout(500);
|
||||
recvd = _socket.getInputStream().read(data, offset, data.length - offset);
|
||||
break;
|
||||
} catch (SocketTimeoutException e1) {
|
||||
if (System.currentTimeMillis() >= end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offset == 0) {
|
||||
// Nothing was buffered
|
||||
return recvd;
|
||||
}
|
||||
if (recvd == -1) {
|
||||
// Buffer the EOF and return the previous buffered data;
|
||||
_buffer = -1;
|
||||
return offset;
|
||||
}
|
||||
return offset + recvd;
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while receiving socket data", e);
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
try {
|
||||
_socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, "Got an exception while closing socket", e);
|
||||
}
|
||||
_socket = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.scummvm.scummvm.net;
|
||||
|
||||
/*
|
||||
* Customized from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
|
||||
* Added TLS1.3 support and keep old protocols enabled for maximum compatibility
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
/**
|
||||
* @author fkrauthan
|
||||
*/
|
||||
public class TLSSocketFactory extends SSLSocketFactory {
|
||||
|
||||
private final SSLSocketFactory _factory;
|
||||
private static String[] _protocols;
|
||||
|
||||
private static boolean _init;
|
||||
|
||||
public static void init() {
|
||||
if (_init) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(new TLSSocketFactory());
|
||||
} catch (RuntimeException ignored) {
|
||||
}
|
||||
_init = true;
|
||||
}
|
||||
|
||||
public TLSSocketFactory() {
|
||||
SSLContext context = null;
|
||||
try {
|
||||
context = LETrustManager.getContext();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (KeyManagementException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
_factory = context.getSocketFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return _factory.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return _factory.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket() throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(_factory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
|
||||
return enableTLSOnSocket(_factory.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return enableTLSOnSocket(_factory.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
private Socket enableTLSOnSocket(Socket socket) {
|
||||
if(socket instanceof SSLSocket) {
|
||||
SSLSocket sslSocket = (SSLSocket)socket;
|
||||
|
||||
if (_protocols == null) {
|
||||
String[] newProtocols = {"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"};
|
||||
|
||||
// Build the list of protocols to enable
|
||||
Set<String> protocols = new HashSet<>(Arrays.asList(sslSocket.getEnabledProtocols()));
|
||||
Set<String> supported = new HashSet<>(Arrays.asList(sslSocket.getSupportedProtocols()));
|
||||
for (String protocol : newProtocols) {
|
||||
if (protocols.contains(protocol)) {
|
||||
continue;
|
||||
}
|
||||
if (!supported.contains(protocol)) {
|
||||
continue;
|
||||
}
|
||||
protocols.add(protocol);
|
||||
}
|
||||
_protocols = protocols.toArray(new String[]{});
|
||||
}
|
||||
|
||||
sslSocket.setEnabledProtocols(_protocols);
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
}
|
||||
261
backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java
Normal file
261
backends/platform/android/org/scummvm/scummvm/zip/ZipCoder.java
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright (c) 2009, 2021, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code 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 General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package org.scummvm.scummvm.zip;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CharacterCodingException;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Utility class for zipfile name and comment decoding and encoding
|
||||
*/
|
||||
class ZipCoder {
|
||||
|
||||
// Android-removed:
|
||||
// private static final jdk.internal.access.JavaLangAccess JLA =
|
||||
// jdk.internal.access.SharedSecrets.getJavaLangAccess();
|
||||
|
||||
// Encoding/decoding is stateless, so make it singleton.
|
||||
// Android-changed: use StandardCharsets.
|
||||
// static final UTF8ZipCoder UTF8 = new UTF8ZipCoder(UTF_8.INSTANCE);
|
||||
// ScummVM-changed: use ZipUtils.
|
||||
static final UTF8ZipCoder UTF8 = new UTF8ZipCoder(ZipUtils.UTF_8);
|
||||
|
||||
public static ZipCoder get(Charset charset) {
|
||||
// Android-changed: use equals method, not reference comparison.
|
||||
// if (charset == UTF_8.INSTANCE) {
|
||||
// ScummVM-changed: use ZipUtils.
|
||||
if (ZipUtils.UTF_8.equals(charset)) {
|
||||
return UTF8;
|
||||
}
|
||||
return new ZipCoder(charset);
|
||||
}
|
||||
|
||||
String toString(byte[] ba, int off, int length) {
|
||||
try {
|
||||
return decoder().decode(ByteBuffer.wrap(ba, off, length)).toString();
|
||||
} catch (CharacterCodingException x) {
|
||||
throw new IllegalArgumentException(x);
|
||||
}
|
||||
}
|
||||
|
||||
String toString(byte[] ba, int length) {
|
||||
return toString(ba, 0, length);
|
||||
}
|
||||
|
||||
String toString(byte[] ba) {
|
||||
return toString(ba, 0, ba.length);
|
||||
}
|
||||
|
||||
byte[] getBytes(String s) {
|
||||
try {
|
||||
ByteBuffer bb = encoder().encode(CharBuffer.wrap(s));
|
||||
int pos = bb.position();
|
||||
int limit = bb.limit();
|
||||
if (bb.hasArray() && pos == 0 && limit == bb.capacity()) {
|
||||
return bb.array();
|
||||
}
|
||||
byte[] bytes = new byte[bb.limit() - bb.position()];
|
||||
bb.get(bytes);
|
||||
return bytes;
|
||||
} catch (CharacterCodingException x) {
|
||||
throw new IllegalArgumentException(x);
|
||||
}
|
||||
}
|
||||
|
||||
static String toStringUTF8(byte[] ba, int len) {
|
||||
return UTF8.toString(ba, 0, len);
|
||||
}
|
||||
|
||||
boolean isUTF8() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hash code functions for ZipFile entry names. We generate the hash as-if
|
||||
// we first decoded the byte sequence to a String, then appended '/' if no
|
||||
// trailing slash was found, then called String.hashCode(). This
|
||||
// normalization ensures we can simplify and speed up lookups.
|
||||
//
|
||||
// Does encoding error checking and hashing in a single pass for efficiency.
|
||||
// On an error, this function will throw CharacterCodingException while the
|
||||
// UTF8ZipCoder override will throw IllegalArgumentException, so we declare
|
||||
// throws Exception to keep things simple.
|
||||
int checkedHash(byte[] a, int off, int len) throws Exception {
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int h = 0;
|
||||
// cb will be a newly allocated CharBuffer with pos == 0,
|
||||
// arrayOffset == 0, backed by an array.
|
||||
CharBuffer cb = decoder().decode(ByteBuffer.wrap(a, off, len));
|
||||
int limit = cb.limit();
|
||||
char[] decoded = cb.array();
|
||||
for (int i = 0; i < limit; i++) {
|
||||
h = 31 * h + decoded[i];
|
||||
}
|
||||
if (limit > 0 && decoded[limit - 1] != '/') {
|
||||
h = 31 * h + '/';
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// Hash function equivalent of checkedHash for String inputs
|
||||
static int hash(String name) {
|
||||
int hsh = name.hashCode();
|
||||
int len = name.length();
|
||||
if (len > 0 && name.charAt(len - 1) != '/') {
|
||||
hsh = hsh * 31 + '/';
|
||||
}
|
||||
return hsh;
|
||||
}
|
||||
|
||||
boolean hasTrailingSlash(byte[] a, int end) {
|
||||
byte[] slashBytes = slashBytes();
|
||||
return end >= slashBytes.length &&
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
Arrays.mismatch(a, end - slashBytes.length, end, slashBytes, 0, slashBytes.length) == -1;
|
||||
*/
|
||||
Arrays.equals(Arrays.copyOfRange(a, end - slashBytes.length, end), slashBytes);
|
||||
}
|
||||
|
||||
private byte[] slashBytes;
|
||||
private final Charset cs;
|
||||
protected CharsetDecoder dec;
|
||||
private CharsetEncoder enc;
|
||||
|
||||
private ZipCoder(Charset cs) {
|
||||
this.cs = cs;
|
||||
}
|
||||
|
||||
protected CharsetDecoder decoder() {
|
||||
if (dec == null) {
|
||||
dec = cs.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
}
|
||||
return dec;
|
||||
}
|
||||
|
||||
private CharsetEncoder encoder() {
|
||||
if (enc == null) {
|
||||
enc = cs.newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
}
|
||||
return enc;
|
||||
}
|
||||
|
||||
// This method produces an array with the bytes that will correspond to a
|
||||
// trailing '/' in the chosen character encoding.
|
||||
//
|
||||
// While in most charsets a trailing slash will be encoded as the byte
|
||||
// value of '/', this does not hold in the general case. E.g., in charsets
|
||||
// such as UTF-16 and UTF-32 it will be represented by a sequence of 2 or 4
|
||||
// bytes, respectively.
|
||||
private byte[] slashBytes() {
|
||||
if (slashBytes == null) {
|
||||
// Take into account charsets that produce a BOM, e.g., UTF-16
|
||||
byte[] slash = "/".getBytes(cs);
|
||||
byte[] doubleSlash = "//".getBytes(cs);
|
||||
slashBytes = Arrays.copyOfRange(doubleSlash, slash.length, doubleSlash.length);
|
||||
}
|
||||
return slashBytes;
|
||||
}
|
||||
|
||||
static final class UTF8ZipCoder extends ZipCoder {
|
||||
|
||||
private UTF8ZipCoder(Charset utf8) {
|
||||
super(utf8);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isUTF8() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
String toString(byte[] ba, int off, int length) {
|
||||
// Android-changed: JLA is not yet available.
|
||||
// return JLA.newStringUTF8NoRepl(ba, off, length);
|
||||
// ScummVM-changed: use ZipUtils.
|
||||
return new String(ba, off, length, ZipUtils.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
byte[] getBytes(String s) {
|
||||
// Android-changed: JLA is not yet available.
|
||||
// return JLA.getBytesUTF8NoRepl(s);
|
||||
// ScummVM-changed: use ZipUtils.
|
||||
return s.getBytes(ZipUtils.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
int checkedHash(byte[] a, int off, int len) throws Exception {
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int end = off + len;
|
||||
int h = 0;
|
||||
while (off < end) {
|
||||
byte b = a[off];
|
||||
if (b >= 0) {
|
||||
// ASCII, keep going
|
||||
h = 31 * h + b;
|
||||
off++;
|
||||
} else {
|
||||
// Non-ASCII, fall back to decoding a String
|
||||
// We avoid using decoder() here since the UTF8ZipCoder is
|
||||
// shared and that decoder is not thread safe.
|
||||
// We use the JLA.newStringUTF8NoRepl variant to throw
|
||||
// exceptions eagerly when opening ZipFiles
|
||||
// Android-changed: JLA is not yet available.
|
||||
// return hash(JLA.newStringUTF8NoRepl(a, end - len, len));
|
||||
// ScummVM-changed: use ZipUtils.
|
||||
return hash(new String(a, end - len, len, ZipUtils.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
if (a[end - 1] != '/') {
|
||||
h = 31 * h + '/';
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean hasTrailingSlash(byte[] a, int end) {
|
||||
return end > 0 && a[end - 1] == '/';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright (c) 1995, 2020, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code 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 General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package org.scummvm.scummvm.zip;
|
||||
|
||||
/*
|
||||
* This interface defines the constants that are used by the classes
|
||||
* which manipulate ZIP files.
|
||||
*
|
||||
* @author David Connelly
|
||||
* @since 1.1
|
||||
*/
|
||||
interface ZipConstants {
|
||||
|
||||
/**
|
||||
* Local file (LOC) header signature.
|
||||
*/
|
||||
static long LOCSIG = 0x04034b50L; // "PK\003\004"
|
||||
|
||||
/**
|
||||
* Extra local (EXT) header signature.
|
||||
*/
|
||||
static long EXTSIG = 0x08074b50L; // "PK\007\008"
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header signature.
|
||||
*/
|
||||
static long CENSIG = 0x02014b50L; // "PK\001\002"
|
||||
|
||||
/**
|
||||
* End of central directory (END) header signature.
|
||||
*/
|
||||
static long ENDSIG = 0x06054b50L; // "PK\005\006"
|
||||
|
||||
/**
|
||||
* Local file (LOC) header size in bytes (including signature).
|
||||
*/
|
||||
static final int LOCHDR = 30;
|
||||
|
||||
/**
|
||||
* Extra local (EXT) header size in bytes (including signature).
|
||||
*/
|
||||
static final int EXTHDR = 16;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header size in bytes (including signature).
|
||||
*/
|
||||
static final int CENHDR = 46;
|
||||
|
||||
/**
|
||||
* End of central directory (END) header size in bytes (including signature).
|
||||
*/
|
||||
static final int ENDHDR = 22;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header version needed to extract field offset.
|
||||
*/
|
||||
static final int LOCVER = 4;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header general purpose bit flag field offset.
|
||||
*/
|
||||
static final int LOCFLG = 6;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header compression method field offset.
|
||||
*/
|
||||
static final int LOCHOW = 8;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header modification time field offset.
|
||||
*/
|
||||
static final int LOCTIM = 10;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header uncompressed file crc-32 value field offset.
|
||||
*/
|
||||
static final int LOCCRC = 14;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header compressed size field offset.
|
||||
*/
|
||||
static final int LOCSIZ = 18;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header uncompressed size field offset.
|
||||
*/
|
||||
static final int LOCLEN = 22;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header filename length field offset.
|
||||
*/
|
||||
static final int LOCNAM = 26;
|
||||
|
||||
/**
|
||||
* Local file (LOC) header extra field length field offset.
|
||||
*/
|
||||
static final int LOCEXT = 28;
|
||||
|
||||
/**
|
||||
* Extra local (EXT) header uncompressed file crc-32 value field offset.
|
||||
*/
|
||||
static final int EXTCRC = 4;
|
||||
|
||||
/**
|
||||
* Extra local (EXT) header compressed size field offset.
|
||||
*/
|
||||
static final int EXTSIZ = 8;
|
||||
|
||||
/**
|
||||
* Extra local (EXT) header uncompressed size field offset.
|
||||
*/
|
||||
static final int EXTLEN = 12;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header version made by field offset.
|
||||
*/
|
||||
static final int CENVEM = 4;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header version needed to extract field offset.
|
||||
*/
|
||||
static final int CENVER = 6;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header encrypt, decrypt flags field offset.
|
||||
*/
|
||||
static final int CENFLG = 8;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header compression method field offset.
|
||||
*/
|
||||
static final int CENHOW = 10;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header modification time field offset.
|
||||
*/
|
||||
static final int CENTIM = 12;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header uncompressed file crc-32 value field offset.
|
||||
*/
|
||||
static final int CENCRC = 16;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header compressed size field offset.
|
||||
*/
|
||||
static final int CENSIZ = 20;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header uncompressed size field offset.
|
||||
*/
|
||||
static final int CENLEN = 24;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header filename length field offset.
|
||||
*/
|
||||
static final int CENNAM = 28;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header extra field length field offset.
|
||||
*/
|
||||
static final int CENEXT = 30;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header comment length field offset.
|
||||
*/
|
||||
static final int CENCOM = 32;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header disk number start field offset.
|
||||
*/
|
||||
static final int CENDSK = 34;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header internal file attributes field offset.
|
||||
*/
|
||||
static final int CENATT = 36;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header external file attributes field offset.
|
||||
*/
|
||||
static final int CENATX = 38;
|
||||
|
||||
/**
|
||||
* Central directory (CEN) header LOC header offset field offset.
|
||||
*/
|
||||
static final int CENOFF = 42;
|
||||
|
||||
/**
|
||||
* End of central directory (END) header number of entries on this disk field offset.
|
||||
*/
|
||||
static final int ENDSUB = 8;
|
||||
|
||||
/**
|
||||
* End of central directory (END) header total number of entries field offset.
|
||||
*/
|
||||
static final int ENDTOT = 10;
|
||||
|
||||
/**
|
||||
* End of central directory (END) header central directory size in bytes field offset.
|
||||
*/
|
||||
static final int ENDSIZ = 12;
|
||||
|
||||
/**
|
||||
* End of central directory (END) header offset for the first CEN header field offset.
|
||||
*/
|
||||
static final int ENDOFF = 16;
|
||||
|
||||
/**
|
||||
* End of central directory (END) header zip file comment length field offset.
|
||||
*/
|
||||
static final int ENDCOM = 20;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (c) 1995, 2013, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code 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 General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package org.scummvm.scummvm.zip;
|
||||
|
||||
/*
|
||||
* This class defines the constants that are used by the classes
|
||||
* which manipulate Zip64 files.
|
||||
*/
|
||||
|
||||
class ZipConstants64 {
|
||||
|
||||
/*
|
||||
* ZIP64 constants
|
||||
*/
|
||||
static final long ZIP64_ENDSIG = 0x06064b50L; // "PK\006\006"
|
||||
static final long ZIP64_LOCSIG = 0x07064b50L; // "PK\006\007"
|
||||
static final int ZIP64_ENDHDR = 56; // ZIP64 end header size
|
||||
static final int ZIP64_LOCHDR = 20; // ZIP64 end loc header size
|
||||
static final int ZIP64_EXTHDR = 24; // EXT header size
|
||||
static final int ZIP64_EXTID = 0x0001; // Extra field Zip64 header ID
|
||||
|
||||
static final int ZIP64_MAGICCOUNT = 0xFFFF;
|
||||
static final long ZIP64_MAGICVAL = 0xFFFFFFFFL;
|
||||
|
||||
/*
|
||||
* Zip64 End of central directory (END) header field offsets
|
||||
*/
|
||||
static final int ZIP64_ENDLEN = 4; // size of zip64 end of central dir
|
||||
static final int ZIP64_ENDVEM = 12; // version made by
|
||||
static final int ZIP64_ENDVER = 14; // version needed to extract
|
||||
static final int ZIP64_ENDNMD = 16; // number of this disk
|
||||
static final int ZIP64_ENDDSK = 20; // disk number of start
|
||||
static final int ZIP64_ENDTOD = 24; // total number of entries on this disk
|
||||
static final int ZIP64_ENDTOT = 32; // total number of entries
|
||||
static final int ZIP64_ENDSIZ = 40; // central directory size in bytes
|
||||
static final int ZIP64_ENDOFF = 48; // offset of first CEN header
|
||||
static final int ZIP64_ENDEXT = 56; // zip64 extensible data sector
|
||||
|
||||
/*
|
||||
* Zip64 End of central directory locator field offsets
|
||||
*/
|
||||
static final int ZIP64_LOCDSK = 4; // disk number start
|
||||
static final int ZIP64_LOCOFF = 8; // offset of zip64 end
|
||||
static final int ZIP64_LOCTOT = 16; // total number of disks
|
||||
|
||||
/*
|
||||
* Zip64 Extra local (EXT) header field offsets
|
||||
*/
|
||||
static final int ZIP64_EXTCRC = 4; // uncompressed file crc-32 value
|
||||
static final int ZIP64_EXTSIZ = 8; // compressed size, 8-byte
|
||||
static final int ZIP64_EXTLEN = 16; // uncompressed size, 8-byte
|
||||
|
||||
/*
|
||||
* Language encoding flag (general purpose flag bit 11)
|
||||
*
|
||||
* If this bit is set the filename and comment fields for this
|
||||
* entry must be encoded using UTF-8.
|
||||
*/
|
||||
static final int USE_UTF8 = 0x800;
|
||||
|
||||
/*
|
||||
* Constants below are defined here (instead of in ZipConstants)
|
||||
* to avoid being exposed as public fields of ZipFile, ZipEntry,
|
||||
* ZipInputStream and ZipOutputstream.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Extra field header ID
|
||||
*/
|
||||
static final int EXTID_ZIP64 = 0x0001; // Zip64
|
||||
static final int EXTID_NTFS = 0x000a; // NTFS
|
||||
static final int EXTID_UNIX = 0x000d; // UNIX
|
||||
static final int EXTID_EXTT = 0x5455; // Info-ZIP Extended Timestamp
|
||||
|
||||
/*
|
||||
* EXTT timestamp flags
|
||||
*/
|
||||
static final int EXTT_FLAG_LMT = 0x1; // LastModifiedTime
|
||||
static final int EXTT_FLAG_LAT = 0x2; // LastAccessTime
|
||||
static final int EXTT_FLAT_CT = 0x4; // CreationTime
|
||||
|
||||
private ZipConstants64() {}
|
||||
}
|
||||
799
backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java
Normal file
799
backends/platform/android/org/scummvm/scummvm/zip/ZipEntry.java
Normal file
@@ -0,0 +1,799 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
* Copyright (c) 1995, 2020, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code 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 General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package org.scummvm.scummvm.zip;
|
||||
|
||||
import static org.scummvm.scummvm.zip.ZipUtils.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import static org.scummvm.scummvm.zip.ZipConstants64.*;
|
||||
|
||||
/**
|
||||
* This class is used to represent a ZIP file entry.
|
||||
*
|
||||
* @author David Connelly
|
||||
* @since 1.1
|
||||
*/
|
||||
public class ZipEntry implements ZipConstants, Cloneable {
|
||||
|
||||
String name; // entry name
|
||||
long xdostime = -1; // last modification time (in extended DOS time,
|
||||
// where milliseconds lost in conversion might
|
||||
// be encoded into the upper half)
|
||||
// ScummVM-changed: Don't use FileTime to improve compatibility.
|
||||
/*
|
||||
FileTime mtime; // last modification time, from extra field data
|
||||
FileTime atime; // last access time, from extra field data
|
||||
FileTime ctime; // creation time, from extra field data
|
||||
*/
|
||||
long crc = -1; // crc-32 of entry data
|
||||
long size = -1; // uncompressed size of entry data
|
||||
long csize = -1; // compressed size of entry data
|
||||
boolean csizeSet = false; // Only true if csize was explicitely set by
|
||||
// a call to setCompressedSize()
|
||||
int method = -1; // compression method
|
||||
int flag = 0; // general purpose flag
|
||||
byte[] extra; // optional extra field data for entry
|
||||
String comment; // optional comment string for entry
|
||||
int extraAttributes = -1; // e.g. POSIX permissions, sym links.
|
||||
// Android-added: Add dataOffset for internal use.
|
||||
// Used by android.util.jar.StrictJarFile from frameworks.
|
||||
long dataOffset;
|
||||
|
||||
/**
|
||||
* Compression method for uncompressed entries.
|
||||
*/
|
||||
public static final int STORED = 0;
|
||||
|
||||
/**
|
||||
* Compression method for compressed (deflated) entries.
|
||||
*/
|
||||
public static final int DEFLATED = 8;
|
||||
|
||||
/**
|
||||
* DOS time constant for representing timestamps before 1980.
|
||||
*/
|
||||
static final long DOSTIME_BEFORE_1980 = (1 << 21) | (1 << 16);
|
||||
|
||||
/**
|
||||
* Approximately 128 years, in milliseconds (ignoring leap years etc).
|
||||
*
|
||||
* This establish an approximate high-bound value for DOS times in
|
||||
* milliseconds since epoch, used to enable an efficient but
|
||||
* sufficient bounds check to avoid generating extended last modified
|
||||
* time entries.
|
||||
*
|
||||
* Calculating the exact number is locale dependent, would require loading
|
||||
* TimeZone data eagerly, and would make little practical sense. Since DOS
|
||||
* times theoretically go to 2107 - with compatibility not guaranteed
|
||||
* after 2099 - setting this to a time that is before but near 2099
|
||||
* should be sufficient.
|
||||
* @hide
|
||||
*/
|
||||
// Android-changed: Make UPPER_DOSTIME_BOUND public hidden for testing purposes.
|
||||
public static final long UPPER_DOSTIME_BOUND =
|
||||
128L * 365 * 24 * 60 * 60 * 1000;
|
||||
|
||||
// Android-added: New constructor for use by StrictJarFile native code.
|
||||
/** @hide */
|
||||
public ZipEntry(String name, String comment, long crc, long compressedSize,
|
||||
long size, int compressionMethod, int xdostime, byte[] extra,
|
||||
long dataOffset) {
|
||||
this.name = name;
|
||||
this.comment = comment;
|
||||
this.crc = crc;
|
||||
this.csize = compressedSize;
|
||||
this.size = size;
|
||||
this.method = compressionMethod;
|
||||
this.xdostime = xdostime;
|
||||
this.dataOffset = dataOffset;
|
||||
this.setExtra0(extra, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new zip entry with the specified name.
|
||||
*
|
||||
* @param name
|
||||
* The entry name
|
||||
*
|
||||
* @throws NullPointerException if the entry name is null
|
||||
* @throws IllegalArgumentException if the entry name is longer than
|
||||
* 0xFFFF bytes
|
||||
*/
|
||||
public ZipEntry(String name) {
|
||||
Objects.requireNonNull(name, "name");
|
||||
// Android-changed: Explicitly use UTF_8 instead of the default charset.
|
||||
// if (name.length() > 0xFFFF) {
|
||||
// throw new IllegalArgumentException("entry name too long");
|
||||
// }
|
||||
// ScummVM-changed: use ZipUtils.
|
||||
if (name.getBytes(ZipUtils.UTF_8).length > 0xffff) {
|
||||
throw new IllegalArgumentException(name + " too long: " +
|
||||
name.getBytes(ZipUtils.UTF_8).length);
|
||||
}
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new zip entry with fields taken from the specified
|
||||
* zip entry.
|
||||
*
|
||||
* @param e
|
||||
* A zip Entry object
|
||||
*
|
||||
* @throws NullPointerException if the entry object is null
|
||||
*/
|
||||
public ZipEntry(ZipEntry e) {
|
||||
Objects.requireNonNull(e, "entry");
|
||||
name = e.name;
|
||||
xdostime = e.xdostime;
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
mtime = e.mtime;
|
||||
atime = e.atime;
|
||||
ctime = e.ctime;
|
||||
*/
|
||||
crc = e.crc;
|
||||
size = e.size;
|
||||
csize = e.csize;
|
||||
csizeSet = e.csizeSet;
|
||||
method = e.method;
|
||||
flag = e.flag;
|
||||
extra = e.extra;
|
||||
comment = e.comment;
|
||||
extraAttributes = e.extraAttributes;
|
||||
// Android-added: Add dataOffset for internal use.
|
||||
dataOffset = e.dataOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new un-initialized zip entry
|
||||
*/
|
||||
ZipEntry() {}
|
||||
|
||||
// BEGIN Android-added: Add dataOffset for internal use.
|
||||
/** @hide */
|
||||
public long getDataOffset() {
|
||||
return dataOffset;
|
||||
}
|
||||
// END Android-added: Add dataOffset for internal use.
|
||||
|
||||
/**
|
||||
* Returns the name of the entry.
|
||||
* @return the name of the entry
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last modification time of the entry.
|
||||
*
|
||||
* <p> If the entry is output to a ZIP file or ZIP file formatted
|
||||
* output stream the last modification time set by this method will
|
||||
* be stored into the {@code date and time fields} of the zip file
|
||||
* entry and encoded in standard {@code MS-DOS date and time format}.
|
||||
* The {@link java.util.TimeZone#getDefault() default TimeZone} is
|
||||
* used to convert the epoch time to the MS-DOS data and time.
|
||||
*
|
||||
* @param time
|
||||
* The last modification time of the entry in milliseconds
|
||||
* since the epoch
|
||||
*
|
||||
* @see #getTime()
|
||||
* @see #getLastModifiedTime()
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public void setTime(long time) {
|
||||
this.xdostime = javaToExtendedDosTime(time);
|
||||
// Avoid setting the mtime field if time is in the valid
|
||||
// range for a DOS time
|
||||
if (this.xdostime != DOSTIME_BEFORE_1980 && time <= UPPER_DOSTIME_BOUND) {
|
||||
this.mtime = null;
|
||||
} else {
|
||||
int localYear = javaEpochToLocalDateTime(time).getYear();
|
||||
if (localYear >= 1980 && localYear <= 2099) {
|
||||
this.mtime = null;
|
||||
} else {
|
||||
this.mtime = FileTime.from(time, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the last modification time of the entry.
|
||||
*
|
||||
* <p> If the entry is read from a ZIP file or ZIP file formatted
|
||||
* input stream, this is the last modification time from the {@code
|
||||
* date and time fields} of the zip file entry. The
|
||||
* {@link java.util.TimeZone#getDefault() default TimeZone} is used
|
||||
* to convert the standard MS-DOS formatted date and time to the
|
||||
* epoch time.
|
||||
*
|
||||
* @return The last modification time of the entry in milliseconds
|
||||
* since the epoch, or -1 if not specified
|
||||
*
|
||||
* //@see #setTime(long)
|
||||
* //@see #setLastModifiedTime(FileTime)
|
||||
*/
|
||||
public long getTime() {
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
if (mtime != null) {
|
||||
return mtime.toMillis();
|
||||
}
|
||||
*/
|
||||
return (xdostime != -1) ? extendedDosToJavaTime(xdostime) : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last modification time of the entry in local date-time.
|
||||
*
|
||||
* <p> If the entry is output to a ZIP file or ZIP file formatted
|
||||
* output stream the last modification time set by this method will
|
||||
* be stored into the {@code date and time fields} of the zip file
|
||||
* entry and encoded in standard {@code MS-DOS date and time format}.
|
||||
* If the date-time set is out of the range of the standard {@code
|
||||
* MS-DOS date and time format}, the time will also be stored into
|
||||
* zip file entry's extended timestamp fields in {@code optional
|
||||
* extra data} in UTC time. The {@link java.time.ZoneId#systemDefault()
|
||||
* system default TimeZone} is used to convert the local date-time
|
||||
* to UTC time.
|
||||
*
|
||||
* <p> {@code LocalDateTime} uses a precision of nanoseconds, whereas
|
||||
* this class uses a precision of milliseconds. The conversion will
|
||||
* truncate any excess precision information as though the amount in
|
||||
* nanoseconds was subject to integer division by one million.
|
||||
*
|
||||
* @param time
|
||||
* The last modification time of the entry in local date-time
|
||||
*
|
||||
* @see #getTimeLocal()
|
||||
* @since 9
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public void setTimeLocal(LocalDateTime time) {
|
||||
int year = time.getYear() - 1980;
|
||||
if (year < 0) {
|
||||
this.xdostime = DOSTIME_BEFORE_1980;
|
||||
} else {
|
||||
this.xdostime = ((year << 25 |
|
||||
time.getMonthValue() << 21 |
|
||||
time.getDayOfMonth() << 16 |
|
||||
time.getHour() << 11 |
|
||||
time.getMinute() << 5 |
|
||||
time.getSecond() >> 1) & 0xffffffffL)
|
||||
+ ((long)(((time.getSecond() & 0x1) * 1000) +
|
||||
time.getNano() / 1000_000) << 32);
|
||||
}
|
||||
if (xdostime != DOSTIME_BEFORE_1980 && year <= 0x7f) {
|
||||
this.mtime = null;
|
||||
} else {
|
||||
this.mtime = FileTime.from(
|
||||
ZonedDateTime.of(time, ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the last modification time of the entry in local date-time.
|
||||
*
|
||||
* <p> If the entry is read from a ZIP file or ZIP file formatted
|
||||
* input stream, this is the last modification time from the zip
|
||||
* file entry's {@code optional extra data} if the extended timestamp
|
||||
* fields are present. Otherwise, the last modification time is read
|
||||
* from entry's standard MS-DOS formatted {@code date and time fields}.
|
||||
*
|
||||
* <p> The {@link java.time.ZoneId#systemDefault() system default TimeZone}
|
||||
* is used to convert the UTC time to local date-time.
|
||||
*
|
||||
* @return The last modification time of the entry in local date-time
|
||||
*
|
||||
* @see #setTimeLocal(LocalDateTime)
|
||||
* @since 9
|
||||
*/
|
||||
// ScummVM-changed: Don't use LocalDateTime.
|
||||
/*
|
||||
public LocalDateTime getTimeLocal() {
|
||||
if (mtime != null) {
|
||||
return LocalDateTime.ofInstant(mtime.toInstant(), ZoneId.systemDefault());
|
||||
}
|
||||
int ms = (int)(xdostime >> 32);
|
||||
return LocalDateTime.of((int)(((xdostime >> 25) & 0x7f) + 1980),
|
||||
(int)((xdostime >> 21) & 0x0f),
|
||||
(int)((xdostime >> 16) & 0x1f),
|
||||
(int)((xdostime >> 11) & 0x1f),
|
||||
(int)((xdostime >> 5) & 0x3f),
|
||||
(int)((xdostime << 1) & 0x3e) + ms / 1000,
|
||||
(ms % 1000) * 1000_000);
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Sets the last modification time of the entry.
|
||||
*
|
||||
* <p> When output to a ZIP file or ZIP file formatted output stream
|
||||
* the last modification time set by this method will be stored into
|
||||
* zip file entry's {@code date and time fields} in {@code standard
|
||||
* MS-DOS date and time format}), and the extended timestamp fields
|
||||
* in {@code optional extra data} in UTC time.
|
||||
*
|
||||
* @param time
|
||||
* The last modification time of the entry
|
||||
* @return This zip entry
|
||||
*
|
||||
* @throws NullPointerException if the {@code time} is null
|
||||
*
|
||||
* @see #getLastModifiedTime()
|
||||
* @since 1.8
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public ZipEntry setLastModifiedTime(FileTime time) {
|
||||
this.mtime = Objects.requireNonNull(time, "lastModifiedTime");
|
||||
this.xdostime = javaToExtendedDosTime(time.to(TimeUnit.MILLISECONDS));
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the last modification time of the entry.
|
||||
*
|
||||
* <p> If the entry is read from a ZIP file or ZIP file formatted
|
||||
* input stream, this is the last modification time from the zip
|
||||
* file entry's {@code optional extra data} if the extended timestamp
|
||||
* fields are present. Otherwise the last modification time is read
|
||||
* from the entry's {@code date and time fields}, the {@link
|
||||
* java.util.TimeZone#getDefault() default TimeZone} is used to convert
|
||||
* the standard MS-DOS formatted date and time to the epoch time.
|
||||
*
|
||||
* @return The last modification time of the entry, null if not specified
|
||||
*
|
||||
* @see #setLastModifiedTime(FileTime)
|
||||
* @since 1.8
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public FileTime getLastModifiedTime() {
|
||||
if (mtime != null)
|
||||
return mtime;
|
||||
if (xdostime == -1)
|
||||
return null;
|
||||
return FileTime.from(getTime(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the last access time of the entry.
|
||||
*
|
||||
* <p> If set, the last access time will be stored into the extended
|
||||
* timestamp fields of entry's {@code optional extra data}, when output
|
||||
* to a ZIP file or ZIP file formatted stream.
|
||||
*
|
||||
* @param time
|
||||
* The last access time of the entry
|
||||
* @return This zip entry
|
||||
*
|
||||
* @throws NullPointerException if the {@code time} is null
|
||||
*
|
||||
* @see #getLastAccessTime()
|
||||
* @since 1.8
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public ZipEntry setLastAccessTime(FileTime time) {
|
||||
this.atime = Objects.requireNonNull(time, "lastAccessTime");
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the last access time of the entry.
|
||||
*
|
||||
* <p> The last access time is from the extended timestamp fields
|
||||
* of entry's {@code optional extra data} when read from a ZIP file
|
||||
* or ZIP file formatted stream.
|
||||
*
|
||||
* @return The last access time of the entry, null if not specified
|
||||
* @see #setLastAccessTime(FileTime)
|
||||
* @since 1.8
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public FileTime getLastAccessTime() {
|
||||
return atime;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the creation time of the entry.
|
||||
*
|
||||
* <p> If set, the creation time will be stored into the extended
|
||||
* timestamp fields of entry's {@code optional extra data}, when
|
||||
* output to a ZIP file or ZIP file formatted stream.
|
||||
*
|
||||
* @param time
|
||||
* The creation time of the entry
|
||||
* @return This zip entry
|
||||
*
|
||||
* @throws NullPointerException if the {@code time} is null
|
||||
*
|
||||
* @see #getCreationTime()
|
||||
* @since 1.8
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public ZipEntry setCreationTime(FileTime time) {
|
||||
this.ctime = Objects.requireNonNull(time, "creationTime");
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the creation time of the entry.
|
||||
*
|
||||
* <p> The creation time is from the extended timestamp fields of
|
||||
* entry's {@code optional extra data} when read from a ZIP file
|
||||
* or ZIP file formatted stream.
|
||||
*
|
||||
* @return the creation time of the entry, null if not specified
|
||||
* @see #setCreationTime(FileTime)
|
||||
* @since 1.8
|
||||
*/
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
public FileTime getCreationTime() {
|
||||
return ctime;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the uncompressed size of the entry data.
|
||||
*
|
||||
* @param size the uncompressed size in bytes
|
||||
*
|
||||
* @throws IllegalArgumentException if the specified size is less
|
||||
* than 0, is greater than 0xFFFFFFFF when
|
||||
* <a href="package-summary.html#zip64">ZIP64 format</a> is not supported,
|
||||
* or is less than 0 when ZIP64 is supported
|
||||
* @see #getSize()
|
||||
*/
|
||||
public void setSize(long size) {
|
||||
if (size < 0) {
|
||||
throw new IllegalArgumentException("invalid entry size");
|
||||
}
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uncompressed size of the entry data.
|
||||
*
|
||||
* @return the uncompressed size of the entry data, or -1 if not known
|
||||
* @see #setSize(long)
|
||||
*/
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the compressed entry data.
|
||||
*
|
||||
* <p> In the case of a stored entry, the compressed size will be the same
|
||||
* as the uncompressed size of the entry.
|
||||
*
|
||||
* @return the size of the compressed entry data, or -1 if not known
|
||||
* @see #setCompressedSize(long)
|
||||
*/
|
||||
public long getCompressedSize() {
|
||||
return csize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of the compressed entry data.
|
||||
*
|
||||
* @param csize the compressed size to set
|
||||
*
|
||||
* @see #getCompressedSize()
|
||||
*/
|
||||
public void setCompressedSize(long csize) {
|
||||
this.csize = csize;
|
||||
this.csizeSet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CRC-32 checksum of the uncompressed entry data.
|
||||
*
|
||||
* @param crc the CRC-32 value
|
||||
*
|
||||
* @throws IllegalArgumentException if the specified CRC-32 value is
|
||||
* less than 0 or greater than 0xFFFFFFFF
|
||||
* @see #getCrc()
|
||||
*/
|
||||
public void setCrc(long crc) {
|
||||
if (crc < 0 || crc > 0xFFFFFFFFL) {
|
||||
throw new IllegalArgumentException("invalid entry crc-32");
|
||||
}
|
||||
this.crc = crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CRC-32 checksum of the uncompressed entry data.
|
||||
*
|
||||
* @return the CRC-32 checksum of the uncompressed entry data, or -1 if
|
||||
* not known
|
||||
*
|
||||
* @see #setCrc(long)
|
||||
*/
|
||||
public long getCrc() {
|
||||
return crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the compression method for the entry.
|
||||
*
|
||||
* @param method the compression method, either STORED or DEFLATED
|
||||
*
|
||||
* @throws IllegalArgumentException if the specified compression
|
||||
* method is invalid
|
||||
* @see #getMethod()
|
||||
*/
|
||||
public void setMethod(int method) {
|
||||
if (method != STORED && method != DEFLATED) {
|
||||
throw new IllegalArgumentException("invalid compression method");
|
||||
}
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the compression method of the entry.
|
||||
*
|
||||
* @return the compression method of the entry, or -1 if not specified
|
||||
* @see #setMethod(int)
|
||||
*/
|
||||
public int getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional extra field data for the entry.
|
||||
*
|
||||
* <p> Invoking this method may change this entry's last modification
|
||||
* time, last access time and creation time, if the {@code extra} field
|
||||
* data includes the extensible timestamp fields, such as {@code NTFS tag
|
||||
* 0x0001} or {@code Info-ZIP Extended Timestamp}, as specified in
|
||||
* <a href="http://www.info-zip.org/doc/appnote-19970311-iz.zip">Info-ZIP
|
||||
* Application Note 970311</a>.
|
||||
*
|
||||
* @param extra
|
||||
* The extra field data bytes
|
||||
*
|
||||
* @throws IllegalArgumentException if the length of the specified
|
||||
* extra field data is greater than 0xFFFF bytes
|
||||
*
|
||||
* @see #getExtra()
|
||||
*/
|
||||
public void setExtra(byte[] extra) {
|
||||
setExtra0(extra, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional extra field data for the entry.
|
||||
*
|
||||
* @param extra
|
||||
* the extra field data bytes
|
||||
* @param doZIP64
|
||||
* if true, set size and csize from ZIP64 fields if present
|
||||
* @param isLOC
|
||||
* true if setting the extra field for a LOC, false if for
|
||||
* a CEN
|
||||
*/
|
||||
void setExtra0(byte[] extra, boolean doZIP64, boolean isLOC) {
|
||||
if (extra != null) {
|
||||
if (extra.length > 0xFFFF) {
|
||||
throw new IllegalArgumentException("invalid extra field length");
|
||||
}
|
||||
// extra fields are in "HeaderID(2)DataSize(2)Data... format
|
||||
int off = 0;
|
||||
int len = extra.length;
|
||||
while (off + 4 < len) {
|
||||
int tag = get16(extra, off);
|
||||
int sz = get16(extra, off + 2);
|
||||
off += 4;
|
||||
if (off + sz > len) // invalid data
|
||||
break;
|
||||
switch (tag) {
|
||||
case EXTID_ZIP64:
|
||||
if (doZIP64) {
|
||||
if (isLOC) {
|
||||
// LOC extra zip64 entry MUST include BOTH original
|
||||
// and compressed file size fields.
|
||||
// If invalid zip64 extra fields, simply skip. Even
|
||||
// it's rare, it's possible the entry size happens to
|
||||
// be the magic value and it "accidentally" has some
|
||||
// bytes in extra match the id.
|
||||
if (sz >= 16) {
|
||||
size = get64(extra, off);
|
||||
csize = get64(extra, off + 8);
|
||||
}
|
||||
} else {
|
||||
// CEN extra zip64
|
||||
if (size == ZIP64_MAGICVAL) {
|
||||
if (off + 8 > len) // invalid zip64 extra
|
||||
break; // fields, just skip
|
||||
size = get64(extra, off);
|
||||
}
|
||||
if (csize == ZIP64_MAGICVAL) {
|
||||
if (off + 16 > len) // invalid zip64 extra
|
||||
break; // fields, just skip
|
||||
csize = get64(extra, off + 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// ScummVM-changed: Don't use FileTime.
|
||||
/*
|
||||
case EXTID_NTFS:
|
||||
if (sz < 32) // reserved 4 bytes + tag 2 bytes + size 2 bytes
|
||||
break; // m[a|c]time 24 bytes
|
||||
int pos = off + 4; // reserved 4 bytes
|
||||
if (get16(extra, pos) != 0x0001 || get16(extra, pos + 2) != 24)
|
||||
break;
|
||||
long wtime = get64(extra, pos + 4);
|
||||
if (wtime != WINDOWS_TIME_NOT_AVAILABLE) {
|
||||
mtime = winTimeToFileTime(wtime);
|
||||
}
|
||||
wtime = get64(extra, pos + 12);
|
||||
if (wtime != WINDOWS_TIME_NOT_AVAILABLE) {
|
||||
atime = winTimeToFileTime(wtime);
|
||||
}
|
||||
wtime = get64(extra, pos + 20);
|
||||
if (wtime != WINDOWS_TIME_NOT_AVAILABLE) {
|
||||
ctime = winTimeToFileTime(wtime);
|
||||
}
|
||||
break;
|
||||
case EXTID_EXTT:
|
||||
int flag = Byte.toUnsignedInt(extra[off]);
|
||||
int sz0 = 1;
|
||||
// The CEN-header extra field contains the modification
|
||||
// time only, or no timestamp at all. 'sz' is used to
|
||||
// flag its presence or absence. But if mtime is present
|
||||
// in LOC it must be present in CEN as well.
|
||||
if ((flag & 0x1) != 0 && (sz0 + 4) <= sz) {
|
||||
mtime = unixTimeToFileTime(get32S(extra, off + sz0));
|
||||
sz0 += 4;
|
||||
}
|
||||
if ((flag & 0x2) != 0 && (sz0 + 4) <= sz) {
|
||||
atime = unixTimeToFileTime(get32S(extra, off + sz0));
|
||||
sz0 += 4;
|
||||
}
|
||||
if ((flag & 0x4) != 0 && (sz0 + 4) <= sz) {
|
||||
ctime = unixTimeToFileTime(get32S(extra, off + sz0));
|
||||
sz0 += 4;
|
||||
}
|
||||
break;
|
||||
*/
|
||||
default:
|
||||
}
|
||||
off += sz;
|
||||
}
|
||||
}
|
||||
this.extra = extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extra field data for the entry.
|
||||
*
|
||||
* @return the extra field data for the entry, or null if none
|
||||
*
|
||||
* @see #setExtra(byte[])
|
||||
*/
|
||||
public byte[] getExtra() {
|
||||
return extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional comment string for the entry.
|
||||
*
|
||||
* <p>ZIP entry comments have maximum length of 0xffff. If the length of the
|
||||
* specified comment string is greater than 0xFFFF bytes after encoding, only
|
||||
* the first 0xFFFF bytes are output to the ZIP file entry.
|
||||
*
|
||||
* @param comment the comment string
|
||||
*
|
||||
* @see #getComment()
|
||||
*/
|
||||
public void setComment(String comment) {
|
||||
// BEGIN Android-added: Explicitly use UTF_8 instead of the default charset.
|
||||
// ScummVM-changed: use ZipUtils.
|
||||
if (comment != null && comment.getBytes(ZipUtils.UTF_8).length > 0xffff) {
|
||||
throw new IllegalArgumentException(comment + " too long: " +
|
||||
comment.getBytes(ZipUtils.UTF_8).length);
|
||||
}
|
||||
// END Android-added: Explicitly use UTF_8 instead of the default charset.
|
||||
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the comment string for the entry.
|
||||
*
|
||||
* @return the comment string for the entry, or null if none
|
||||
*
|
||||
* @see #setComment(String)
|
||||
*/
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a directory entry. A directory entry is
|
||||
* defined to be one whose name ends with a '/'.
|
||||
* @return true if this is a directory entry
|
||||
*/
|
||||
public boolean isDirectory() {
|
||||
return name.endsWith("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the ZIP entry.
|
||||
*/
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hash code value for this entry.
|
||||
*/
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this entry.
|
||||
*/
|
||||
public Object clone() {
|
||||
try {
|
||||
ZipEntry e = (ZipEntry)super.clone();
|
||||
e.extra = (extra == null) ? null : extra.clone();
|
||||
return e;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
// This should never happen, since we are Cloneable
|
||||
//throw new InternalError(e);
|
||||
// ScummVM-changed: Don't use InternalError to improve compatibility.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
2130
backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
Normal file
2130
backends/platform/android/org/scummvm/scummvm/zip/ZipFile.java
Normal file
File diff suppressed because it is too large
Load Diff
356
backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java
Normal file
356
backends/platform/android/org/scummvm/scummvm/zip/ZipUtils.java
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright (c) 2013, 2020, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code 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 General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package org.scummvm.scummvm.zip;
|
||||
|
||||
// ScummVM-changed: improve compatibility.
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
// BEGIN ScummVM-changed: improve compatibility.
|
||||
import java.nio.charset.Charset;
|
||||
/*
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
*/
|
||||
import java.util.Date;
|
||||
/*
|
||||
import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
import java.util.GregorianCalendar;
|
||||
// END ScummVM-changed: improve compatibility.
|
||||
|
||||
import static org.scummvm.scummvm.zip.ZipConstants.ENDHDR;
|
||||
|
||||
// ScummVM-changed: don't use internal APIs.
|
||||
//import jdk.internal.misc.Unsafe;
|
||||
|
||||
class ZipUtils {
|
||||
|
||||
// ScummVM-changed: improve compatibility.
|
||||
static final Charset UTF_8 = Charset.defaultCharset();
|
||||
|
||||
// used to adjust values between Windows and java epoch
|
||||
private static final long WINDOWS_EPOCH_IN_MICROSECONDS = -11644473600000000L;
|
||||
|
||||
// used to indicate the corresponding windows time is not available
|
||||
public static final long WINDOWS_TIME_NOT_AVAILABLE = Long.MIN_VALUE;
|
||||
|
||||
// static final ByteBuffer defaultBuf = ByteBuffer.allocateDirect(0);
|
||||
static final ByteBuffer defaultBuf = ByteBuffer.allocate(0);
|
||||
|
||||
/**
|
||||
* Converts Windows time (in microseconds, UTC/GMT) time to FileTime.
|
||||
*/
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
public static final FileTime winTimeToFileTime(long wtime) {
|
||||
return FileTime.from(wtime / 10 + WINDOWS_EPOCH_IN_MICROSECONDS,
|
||||
TimeUnit.MICROSECONDS);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts FileTime to Windows time.
|
||||
*/
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
public static final long fileTimeToWinTime(FileTime ftime) {
|
||||
return (ftime.to(TimeUnit.MICROSECONDS) - WINDOWS_EPOCH_IN_MICROSECONDS) * 10;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The upper bound of the 32-bit unix time, the "year 2038 problem".
|
||||
*/
|
||||
public static final long UPPER_UNIXTIME_BOUND = 0x7fffffff;
|
||||
|
||||
/**
|
||||
* Converts "standard Unix time"(in seconds, UTC/GMT) to FileTime
|
||||
*/
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
public static final FileTime unixTimeToFileTime(long utime) {
|
||||
return FileTime.from(utime, TimeUnit.SECONDS);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts FileTime to "standard Unix time".
|
||||
*/
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
public static final long fileTimeToUnixTime(FileTime ftime) {
|
||||
return ftime.to(TimeUnit.SECONDS);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts DOS time to Java time (number of milliseconds since epoch).
|
||||
*/
|
||||
public static long dosToJavaTime(long dtime) {
|
||||
int year = (int) (((dtime >> 25) & 0x7f) + 1980);
|
||||
int month = (int) ((dtime >> 21) & 0x0f);
|
||||
int day = (int) ((dtime >> 16) & 0x1f);
|
||||
int hour = (int) ((dtime >> 11) & 0x1f);
|
||||
int minute = (int) ((dtime >> 5) & 0x3f);
|
||||
int second = (int) ((dtime << 1) & 0x3e);
|
||||
|
||||
if (month > 0 && month < 13 && day > 0 && hour < 24 && minute < 60 && second < 60) {
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
try {
|
||||
LocalDateTime ldt = LocalDateTime.of(year, month, day, hour, minute, second);
|
||||
return TimeUnit.MILLISECONDS.convert(ldt.toEpochSecond(
|
||||
ZoneId.systemDefault().getRules().getOffset(ldt)), TimeUnit.SECONDS);
|
||||
} catch (DateTimeException dte) {
|
||||
// ignore
|
||||
}
|
||||
*/
|
||||
return (new GregorianCalendar(year, month, day, hour, minute, second)).getTimeInMillis();
|
||||
}
|
||||
return overflowDosToJavaTime(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
/*
|
||||
* Deal with corner cases where an arguably mal-formed DOS time is used
|
||||
*/
|
||||
@SuppressWarnings("deprecation") // Use of Date constructor
|
||||
private static long overflowDosToJavaTime(int year, int month, int day,
|
||||
int hour, int minute, int second) {
|
||||
return new Date(year - 1900, month - 1, day, hour, minute, second).getTime();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts extended DOS time to Java time, where up to 1999 milliseconds
|
||||
* might be encoded into the upper half of the returned long.
|
||||
*
|
||||
* @param xdostime the extended DOS time value
|
||||
* @return milliseconds since epoch
|
||||
*/
|
||||
public static long extendedDosToJavaTime(long xdostime) {
|
||||
long time = dosToJavaTime(xdostime);
|
||||
return time + (xdostime >> 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Java time to DOS time.
|
||||
*/
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
private static long javaToDosTime(LocalDateTime ldt) {
|
||||
int year = ldt.getYear() - 1980;
|
||||
return (year << 25 |
|
||||
ldt.getMonthValue() << 21 |
|
||||
ldt.getDayOfMonth() << 16 |
|
||||
ldt.getHour() << 11 |
|
||||
ldt.getMinute() << 5 |
|
||||
ldt.getSecond() >> 1) & 0xffffffffL;
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts Java time to DOS time, encoding any milliseconds lost
|
||||
* in the conversion into the upper half of the returned long.
|
||||
*
|
||||
* @param time milliseconds since epoch
|
||||
* @return DOS time with 2s remainder encoded into upper half
|
||||
*/
|
||||
// ScummVM-changed: improve compatibility.
|
||||
/*
|
||||
static long javaToExtendedDosTime(long time) {
|
||||
LocalDateTime ldt = javaEpochToLocalDateTime(time);
|
||||
if (ldt.getYear() >= 1980) {
|
||||
return javaToDosTime(ldt) + ((time % 2000) << 32);
|
||||
}
|
||||
return ZipEntry.DOSTIME_BEFORE_1980;
|
||||
}
|
||||
|
||||
static LocalDateTime javaEpochToLocalDateTime(long time) {
|
||||
Instant instant = Instant.ofEpochMilli(time);
|
||||
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetches unsigned 16-bit value from byte array at specified offset.
|
||||
* The bytes are assumed to be in Intel (little-endian) byte order.
|
||||
*/
|
||||
public static final int get16(byte b[], int off) {
|
||||
return (b[off] & 0xff) | ((b[off + 1] & 0xff) << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches unsigned 32-bit value from byte array at specified offset.
|
||||
* The bytes are assumed to be in Intel (little-endian) byte order.
|
||||
*/
|
||||
public static final long get32(byte b[], int off) {
|
||||
return (get16(b, off) | ((long)get16(b, off+2) << 16)) & 0xffffffffL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches signed 64-bit value from byte array at specified offset.
|
||||
* The bytes are assumed to be in Intel (little-endian) byte order.
|
||||
*/
|
||||
public static final long get64(byte b[], int off) {
|
||||
return get32(b, off) | (get32(b, off+4) << 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches signed 32-bit value from byte array at specified offset.
|
||||
* The bytes are assumed to be in Intel (little-endian) byte order.
|
||||
*
|
||||
*/
|
||||
public static final int get32S(byte b[], int off) {
|
||||
return (get16(b, off) | (get16(b, off+2) << 16));
|
||||
}
|
||||
|
||||
// fields access methods
|
||||
static final int CH(byte[] b, int n) {
|
||||
return b[n] & 0xff ;
|
||||
}
|
||||
|
||||
static final int SH(byte[] b, int n) {
|
||||
return (b[n] & 0xff) | ((b[n + 1] & 0xff) << 8);
|
||||
}
|
||||
|
||||
static final long LG(byte[] b, int n) {
|
||||
return ((SH(b, n)) | (SH(b, n + 2) << 16)) & 0xffffffffL;
|
||||
}
|
||||
|
||||
static final long LL(byte[] b, int n) {
|
||||
return (LG(b, n)) | (LG(b, n + 4) << 32);
|
||||
}
|
||||
|
||||
static final long GETSIG(byte[] b) {
|
||||
return LG(b, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* File attribute compatibility types of CEN field "version made by"
|
||||
*/
|
||||
static final int FILE_ATTRIBUTES_UNIX = 3; // Unix
|
||||
|
||||
/*
|
||||
* Base values for CEN field "version made by"
|
||||
*/
|
||||
static final int VERSION_MADE_BY_BASE_UNIX = FILE_ATTRIBUTES_UNIX << 8; // Unix
|
||||
|
||||
|
||||
// local file (LOC) header fields
|
||||
static final long LOCSIG(byte[] b) { return LG(b, 0); } // signature
|
||||
static final int LOCVER(byte[] b) { return SH(b, 4); } // version needed to extract
|
||||
static final int LOCFLG(byte[] b) { return SH(b, 6); } // general purpose bit flags
|
||||
static final int LOCHOW(byte[] b) { return SH(b, 8); } // compression method
|
||||
static final long LOCTIM(byte[] b) { return LG(b, 10);} // modification time
|
||||
static final long LOCCRC(byte[] b) { return LG(b, 14);} // crc of uncompressed data
|
||||
static final long LOCSIZ(byte[] b) { return LG(b, 18);} // compressed data size
|
||||
static final long LOCLEN(byte[] b) { return LG(b, 22);} // uncompressed data size
|
||||
static final int LOCNAM(byte[] b) { return SH(b, 26);} // filename length
|
||||
static final int LOCEXT(byte[] b) { return SH(b, 28);} // extra field length
|
||||
|
||||
// extra local (EXT) header fields
|
||||
static final long EXTCRC(byte[] b) { return LG(b, 4);} // crc of uncompressed data
|
||||
static final long EXTSIZ(byte[] b) { return LG(b, 8);} // compressed size
|
||||
static final long EXTLEN(byte[] b) { return LG(b, 12);} // uncompressed size
|
||||
|
||||
// end of central directory header (END) fields
|
||||
static final int ENDSUB(byte[] b) { return SH(b, 8); } // number of entries on this disk
|
||||
static final int ENDTOT(byte[] b) { return SH(b, 10);} // total number of entries
|
||||
static final long ENDSIZ(byte[] b) { return LG(b, 12);} // central directory size
|
||||
static final long ENDOFF(byte[] b) { return LG(b, 16);} // central directory offset
|
||||
static final int ENDCOM(byte[] b) { return SH(b, 20);} // size of zip file comment
|
||||
static final int ENDCOM(byte[] b, int off) { return SH(b, off + 20);}
|
||||
|
||||
// zip64 end of central directory recoder fields
|
||||
static final long ZIP64_ENDTOD(byte[] b) { return LL(b, 24);} // total number of entries on disk
|
||||
static final long ZIP64_ENDTOT(byte[] b) { return LL(b, 32);} // total number of entries
|
||||
static final long ZIP64_ENDSIZ(byte[] b) { return LL(b, 40);} // central directory size
|
||||
static final long ZIP64_ENDOFF(byte[] b) { return LL(b, 48);} // central directory offset
|
||||
static final long ZIP64_LOCOFF(byte[] b) { return LL(b, 8);} // zip64 end offset
|
||||
|
||||
// central directory header (CEN) fields
|
||||
static final long CENSIG(byte[] b, int pos) { return LG(b, pos + 0); }
|
||||
static final int CENVEM(byte[] b, int pos) { return SH(b, pos + 4); }
|
||||
static final int CENVEM_FA(byte[] b, int pos) { return CH(b, pos + 5); } // file attribute compatibility
|
||||
static final int CENVER(byte[] b, int pos) { return SH(b, pos + 6); }
|
||||
static final int CENFLG(byte[] b, int pos) { return SH(b, pos + 8); }
|
||||
static final int CENHOW(byte[] b, int pos) { return SH(b, pos + 10);}
|
||||
static final long CENTIM(byte[] b, int pos) { return LG(b, pos + 12);}
|
||||
static final long CENCRC(byte[] b, int pos) { return LG(b, pos + 16);}
|
||||
static final long CENSIZ(byte[] b, int pos) { return LG(b, pos + 20);}
|
||||
static final long CENLEN(byte[] b, int pos) { return LG(b, pos + 24);}
|
||||
static final int CENNAM(byte[] b, int pos) { return SH(b, pos + 28);}
|
||||
static final int CENEXT(byte[] b, int pos) { return SH(b, pos + 30);}
|
||||
static final int CENCOM(byte[] b, int pos) { return SH(b, pos + 32);}
|
||||
static final int CENDSK(byte[] b, int pos) { return SH(b, pos + 34);}
|
||||
static final int CENATT(byte[] b, int pos) { return SH(b, pos + 36);}
|
||||
static final long CENATX(byte[] b, int pos) { return LG(b, pos + 38);}
|
||||
static final int CENATX_PERMS(byte[] b, int pos) { return SH(b, pos + 40);} // posix permission data
|
||||
static final long CENOFF(byte[] b, int pos) { return LG(b, pos + 42);}
|
||||
|
||||
// The END header is followed by a variable length comment of size < 64k.
|
||||
static final long END_MAXLEN = 0xFFFF + ENDHDR;
|
||||
static final int READBLOCKSZ = 128;
|
||||
|
||||
// Android-removed: not available on Android.
|
||||
/*
|
||||
* Loads zip native library, if not already laoded
|
||||
*
|
||||
static void loadLibrary() {
|
||||
jdk.internal.loader.BootLoader.loadLibrary("zip");
|
||||
}
|
||||
*/
|
||||
|
||||
// ScummVM-changed: don't use internal APIs.
|
||||
/*
|
||||
private static final Unsafe unsafe = Unsafe.getUnsafe();
|
||||
|
||||
private static final long byteBufferArrayOffset = unsafe.objectFieldOffset(ByteBuffer.class, "hb");
|
||||
private static final long byteBufferOffsetOffset = unsafe.objectFieldOffset(ByteBuffer.class, "offset");
|
||||
|
||||
static byte[] getBufferArray(ByteBuffer byteBuffer) {
|
||||
return (byte[]) unsafe.getReference(byteBuffer, byteBufferArrayOffset);
|
||||
}
|
||||
|
||||
static int getBufferOffset(ByteBuffer byteBuffer) {
|
||||
return unsafe.getInt(byteBuffer, byteBufferOffsetOffset);
|
||||
}
|
||||
*/
|
||||
|
||||
// ScummVM-changed: improve compatibility.
|
||||
static class UncheckedIOException extends RuntimeException {
|
||||
UncheckedIOException(IOException ioe) {
|
||||
super(ioe);
|
||||
}
|
||||
|
||||
public IOException getCause() {
|
||||
return (IOException) super.getCause();
|
||||
}
|
||||
}
|
||||
}
|
||||
60
backends/platform/android/portdefs.h
Normal file
60
backends/platform/android/portdefs.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _PORTDEFS_H_
|
||||
#define _PORTDEFS_H_
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#define _USE_MATH_DEFINES
|
||||
#include <math.h>
|
||||
#include <new>
|
||||
#include <limits>
|
||||
|
||||
// This is defined in snprintf.c
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
int rpl_vsnprintf(char *text, size_t maxlen, const char *fmt, va_list ap);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#define vsnprintf rpl_vsnprintf
|
||||
|
||||
// Bionic libc up to Android 6/API 23 (excluded) is non-conformant with ANSI C.
|
||||
// It compares with integer character without casting it to unsigned char as required.
|
||||
// On AArch64, another (optimized) implementation is used which behaves properly.
|
||||
// If char is unsigned the problem does not exist.
|
||||
// strchr calls are also replaced by memchr calls when the compiler knows the haystack size.
|
||||
#if !defined(__CHAR_UNSIGNED__) && !defined(__aarch64__)
|
||||
#define memchr(s,c,n) memchr(s, (unsigned char)(c), n)
|
||||
#define strchr(s,c) strchr(s, (unsigned char)(c))
|
||||
#endif
|
||||
|
||||
#endif // _PORTDEFS_H_
|
||||
1319
backends/platform/android/snprintf.cpp
Normal file
1319
backends/platform/android/snprintf.cpp
Normal file
File diff suppressed because it is too large
Load Diff
784
backends/platform/android/touchcontrols.cpp
Normal file
784
backends/platform/android/touchcontrols.cpp
Normal file
@@ -0,0 +1,784 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Allow use of stuff in <time.h>
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
|
||||
//
|
||||
// Disable printf override in common/forbidden.h to avoid
|
||||
// clashes with log.h from the Android SDK.
|
||||
// That header file uses
|
||||
// __attribute__ ((format(printf, 3, 4)))
|
||||
// which gets messed up by our override mechanism; this could
|
||||
// be avoided by either changing the Android SDK to use the equally
|
||||
// legal and valid
|
||||
// __attribute__ ((format(__printf__, 3, 4)))
|
||||
// or by refining our printf override to use a varadic macro
|
||||
// (which then wouldn't be portable, though).
|
||||
// Anyway, for now we just disable the printf override globally
|
||||
// for the Android port
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_printf
|
||||
|
||||
#include "backends/platform/android/android.h"
|
||||
#include "backends/platform/android/jni-android.h"
|
||||
#include "backends/platform/android/touchcontrols.h"
|
||||
|
||||
#include "common/file.h"
|
||||
#include "graphics/svg.h"
|
||||
|
||||
#define SVG_WIDTH 384
|
||||
#define SVG_HEIGHT 256
|
||||
#define BG_WIDTH 128
|
||||
#define BG_HEIGHT 128
|
||||
#define ARROW_DEAD 21
|
||||
#define ARROW_SUPER 42
|
||||
#define ARROW_END 64
|
||||
#define ARROW_WIDTH 64
|
||||
#define ARROW_SEMI_HEIGHT 52
|
||||
#define ARROW_HEIGHT 64
|
||||
#define ARROW_HL_LEFT 0
|
||||
#define ARROW_HL_TOP 128
|
||||
#define BUTTON_DEAD 12
|
||||
#define BUTTON_END 64
|
||||
#define BUTTON_WIDTH 64
|
||||
#define BUTTON_HEIGHT 64
|
||||
#define BUTTON_HL_LEFT 128
|
||||
#define BUTTON_HL_TOP 128
|
||||
#define BUTTON2_HL_LEFT 256
|
||||
#define BUTTON2_HL_TOP 128
|
||||
|
||||
#define ALPHA_OPAQUE 255
|
||||
#define ZOMBIE_TIMEOUT 500 // ms
|
||||
|
||||
// The scale factor is stored as a fixed point 30.2 bits
|
||||
// This avoids floating point operations
|
||||
#define SCALE_FACTOR_FXP 4
|
||||
|
||||
// gamepad.svg was designed with a basis of 128x128 sized widget
|
||||
// As it's too small on screen, we apply a factor of 1.5x
|
||||
// It can be tuned here and in SVG viewBox without changing anything else
|
||||
#define SVG_UNSCALED(x) ((x) * 3 / 2)
|
||||
#define SVG_SQ_UNSCALED(x) ((x * x) * 9 / 4)
|
||||
|
||||
#define SVG_SCALED(x) (SVG_UNSCALED(x) * _scale)
|
||||
#define SCALED_PIXELS(x) ((x) / SCALE_FACTOR_FXP)
|
||||
|
||||
#define SVG_PIXELS(x) SCALED_PIXELS(SVG_SCALED(x))
|
||||
|
||||
#define FUNC_SVG_SQ_SCALED(x) (SVG_SQ_UNSCALED(x) * parent->_scale2)
|
||||
|
||||
TouchControls::TouchControls() :
|
||||
_drawer(nullptr),
|
||||
_screen_width(0),
|
||||
_screen_height(0),
|
||||
_svg(nullptr),
|
||||
_zombieCount(0),
|
||||
_scale(0),
|
||||
_scale2(0) {
|
||||
_functions[kFunctionLeft] = new FunctionLeft(this);
|
||||
_functions[kFunctionRight] = new FunctionRight(this);
|
||||
_functions[kFunctionCenter] = new FunctionCenter(this);
|
||||
}
|
||||
|
||||
void TouchControls::init(float scale) {
|
||||
_scale = scale * SCALE_FACTOR_FXP;
|
||||
// As scale is small, this should fit in int
|
||||
_scale2 = _scale * _scale;
|
||||
|
||||
Common::File stream;
|
||||
|
||||
if (!stream.open("gamepad.svg")) {
|
||||
error("Failed to fetch gamepad image");
|
||||
}
|
||||
|
||||
delete _svg;
|
||||
_svg = new Graphics::SVGBitmap(&stream, SVG_PIXELS(SVG_WIDTH), SVG_PIXELS(SVG_HEIGHT));
|
||||
}
|
||||
|
||||
TouchControls::~TouchControls() {
|
||||
delete _svg;
|
||||
for(unsigned int i = 0; i < kFunctionCount; i++) {
|
||||
delete _functions[i];
|
||||
}
|
||||
}
|
||||
|
||||
void TouchControls::beforeDraw() {
|
||||
if (!_zombieCount) {
|
||||
return;
|
||||
}
|
||||
// We have zombies, force redraw to render fading out
|
||||
if (_drawer) {
|
||||
_drawer->touchControlNotifyChanged();
|
||||
}
|
||||
|
||||
// Check for zombie functions, clear them out if expired
|
||||
unsigned int zombieCount = 0;
|
||||
uint32 now = g_system->getMillis(true);
|
||||
for (uint i = 0; i < kFunctionCount; i++) {
|
||||
Function *func = _functions[i];
|
||||
if (func->status != kFunctionZombie) {
|
||||
continue;
|
||||
}
|
||||
if (func->lastActivable < now) {
|
||||
func->status = kFunctionInactive;
|
||||
continue;
|
||||
}
|
||||
zombieCount++;
|
||||
}
|
||||
_zombieCount = zombieCount;
|
||||
}
|
||||
|
||||
TouchControls::FunctionId TouchControls::getFunctionId(int x, int y) {
|
||||
if (_screen_width == 0) {
|
||||
// Avoid divide by 0 error
|
||||
return kFunctionNone;
|
||||
}
|
||||
|
||||
// Exclude areas reserved for system
|
||||
if ((x < JNI::gestures_insets[0] * SCALE_FACTOR_FXP) ||
|
||||
(y < JNI::gestures_insets[1] * SCALE_FACTOR_FXP) ||
|
||||
(x >= _screen_width - JNI::gestures_insets[2] * SCALE_FACTOR_FXP) ||
|
||||
(y >= _screen_height - JNI::gestures_insets[3] * SCALE_FACTOR_FXP)) {
|
||||
return kFunctionNone;
|
||||
}
|
||||
|
||||
float xRatio = float(x) / _screen_width;
|
||||
|
||||
if (xRatio < 0.3) {
|
||||
return kFunctionLeft;
|
||||
} else if (xRatio < 0.7) {
|
||||
return kFunctionCenter;
|
||||
} else {
|
||||
return kFunctionRight;
|
||||
}
|
||||
}
|
||||
|
||||
void TouchControls::setDrawer(TouchControlsDrawer *drawer, int width, int height) {
|
||||
_drawer = drawer;
|
||||
_screen_width = width * SCALE_FACTOR_FXP;
|
||||
_screen_height = height * SCALE_FACTOR_FXP;
|
||||
|
||||
if (drawer) {
|
||||
drawer->touchControlInitSurface(*_svg);
|
||||
}
|
||||
}
|
||||
|
||||
TouchControls::Function *TouchControls::getFunctionFromPointerId(int ptrId) {
|
||||
for (uint i = 0; i < kFunctionCount; i++) {
|
||||
Function *func = _functions[i];
|
||||
if (func->status != kFunctionActive) {
|
||||
continue;
|
||||
}
|
||||
if (func->pointerId == ptrId) {
|
||||
return func;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TouchControls::Function *TouchControls::getZombieFunctionFromPos(int x, int y) {
|
||||
if (!_zombieCount) {
|
||||
return nullptr;
|
||||
}
|
||||
for (uint i = 0; i < kFunctionCount; i++) {
|
||||
Function *func = _functions[i];
|
||||
if (func->status != kFunctionZombie) {
|
||||
// Already assigned to a finger or dead
|
||||
continue;
|
||||
}
|
||||
if (func->isInside(x, y)) {
|
||||
return func;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TouchControls::draw() {
|
||||
assert(_drawer != nullptr);
|
||||
|
||||
uint32 now = g_system->getMillis(true);
|
||||
|
||||
for (uint i = 0; i < kFunctionCount; i++) {
|
||||
Function *func = _functions[i];
|
||||
uint8 alpha;
|
||||
switch (func->status) {
|
||||
case kFunctionActive:
|
||||
alpha = ALPHA_OPAQUE;
|
||||
break;
|
||||
case kFunctionZombie:
|
||||
if (func->lastActivable < now) {
|
||||
// This function is definitely dead
|
||||
continue;
|
||||
}
|
||||
alpha = (func->lastActivable - now) * ALPHA_OPAQUE / ZOMBIE_TIMEOUT;
|
||||
break;
|
||||
case kFunctionInactive:
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
func->draw(alpha);
|
||||
}
|
||||
}
|
||||
|
||||
void TouchControls::update(Action action, int ptrId, int x, int y) {
|
||||
x *= SCALE_FACTOR_FXP;
|
||||
y *= SCALE_FACTOR_FXP;
|
||||
if (action == JACTION_DOWN) {
|
||||
Function *func = getZombieFunctionFromPos(x, y);
|
||||
if (!func) {
|
||||
// Finger was not pressed on a zombie function
|
||||
// Determine which function it could be
|
||||
FunctionId funcId = getFunctionId(x, y);
|
||||
if (funcId != kFunctionNone) {
|
||||
func = _functions[funcId];
|
||||
}
|
||||
if (!func) {
|
||||
// No function for this finger
|
||||
return;
|
||||
}
|
||||
if (func->status == kFunctionActive) {
|
||||
// Another finger is already on this function
|
||||
return;
|
||||
}
|
||||
|
||||
// When zombie, we reuse the old start coordinates
|
||||
// but not when starting over
|
||||
func->reset();
|
||||
func->startX = x;
|
||||
func->startY = y;
|
||||
}
|
||||
|
||||
|
||||
func->status = kFunctionActive;
|
||||
func->pointerId = ptrId;
|
||||
func->currentX = x;
|
||||
func->currentY = y;
|
||||
|
||||
int dX = x - func->startX;
|
||||
int dY = y - func->startY;
|
||||
|
||||
func->touch(dX, dY, action);
|
||||
if (_drawer) {
|
||||
_drawer->touchControlNotifyChanged();
|
||||
}
|
||||
} else if (action == JACTION_MOVE) {
|
||||
Function *func = getFunctionFromPointerId(ptrId);
|
||||
if (!func) {
|
||||
return;
|
||||
}
|
||||
|
||||
func->currentX = x;
|
||||
func->currentY = y;
|
||||
|
||||
int dX = x - func->startX;
|
||||
int dY = y - func->startY;
|
||||
|
||||
func->touch(dX, dY, action);
|
||||
if (_drawer) {
|
||||
_drawer->touchControlNotifyChanged();
|
||||
}
|
||||
} else if (action == JACTION_UP) {
|
||||
Function *func = getFunctionFromPointerId(ptrId);
|
||||
if (!func) {
|
||||
return;
|
||||
}
|
||||
|
||||
func->currentX = x;
|
||||
func->currentY = y;
|
||||
|
||||
int dX = x - func->startX;
|
||||
int dY = y - func->startY;
|
||||
|
||||
func->touch(dX, dY, action);
|
||||
func->status = kFunctionZombie;
|
||||
func->lastActivable = g_system->getMillis(true) + ZOMBIE_TIMEOUT;
|
||||
if (_drawer) {
|
||||
_drawer->touchControlNotifyChanged();
|
||||
}
|
||||
_zombieCount++;
|
||||
} else if (action == JACTION_CANCEL) {
|
||||
for (uint i = 0; i < kFunctionCount; i++) {
|
||||
Function *func = _functions[i];
|
||||
|
||||
if (func->status == kFunctionActive) {
|
||||
func->touch(0, 0, action);
|
||||
}
|
||||
func->reset();
|
||||
}
|
||||
if (_drawer) {
|
||||
_drawer->touchControlNotifyChanged();
|
||||
}
|
||||
_zombieCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void TouchControls::buttonDown(Common::JoystickButton jb) {
|
||||
if (jb == Common::JOYSTICK_BUTTON_INVALID) {
|
||||
return;
|
||||
}
|
||||
|
||||
//LOGD("TouchControls::buttonDown: %d", jb);
|
||||
Common::Event ev;
|
||||
ev.type = Common::EVENT_JOYBUTTON_DOWN;
|
||||
ev.joystick.button = jb;
|
||||
dynamic_cast<OSystem_Android *>(g_system)->pushEvent(ev);
|
||||
}
|
||||
|
||||
void TouchControls::buttonUp(Common::JoystickButton jb) {
|
||||
if (jb == Common::JOYSTICK_BUTTON_INVALID) {
|
||||
return;
|
||||
}
|
||||
|
||||
//LOGD("TouchControls::buttonUp: %d", jb);
|
||||
Common::Event ev;
|
||||
ev.type = Common::EVENT_JOYBUTTON_UP;
|
||||
ev.joystick.button = jb;
|
||||
dynamic_cast<OSystem_Android *>(g_system)->pushEvent(ev);
|
||||
}
|
||||
|
||||
void TouchControls::buttonPress(Common::JoystickButton jb) {
|
||||
if (jb == Common::JOYSTICK_BUTTON_INVALID) {
|
||||
return;
|
||||
}
|
||||
|
||||
//LOGD("TouchControls::buttonPress: %d", jb);
|
||||
Common::Event ev1, ev2;
|
||||
ev1.type = Common::EVENT_JOYBUTTON_DOWN;
|
||||
ev1.joystick.button = jb;
|
||||
ev2.type = Common::EVENT_JOYBUTTON_UP;
|
||||
ev2.joystick.button = jb;
|
||||
dynamic_cast<OSystem_Android *>(g_system)->pushEvent(ev1, ev2);
|
||||
}
|
||||
|
||||
void TouchControls::drawSurface(uint8 alpha, int x, int y, int offX, int offY, const Common::Rect &clip) const {
|
||||
Common::Rect clip_(SVG_PIXELS(clip.left), SVG_PIXELS(clip.top),
|
||||
SVG_PIXELS(clip.right), SVG_PIXELS(clip.bottom));
|
||||
_drawer->touchControlDraw(alpha,
|
||||
SCALED_PIXELS(x + SVG_SCALED(offX)), SCALED_PIXELS(y + SVG_SCALED(offY)),
|
||||
clip_.width(), clip_.height(), clip_);
|
||||
}
|
||||
|
||||
bool TouchControls::FunctionLeft::isInside(int x, int y) {
|
||||
int dX = x - startX;
|
||||
int dY = y - startY;
|
||||
// norm 2 squared (to avoid square root)
|
||||
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
|
||||
|
||||
if (sqNorm <= FUNC_SVG_SQ_SCALED(ARROW_END)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also accept touches near the old last touch
|
||||
dX = x - currentX;
|
||||
dY = y - currentY;
|
||||
sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
|
||||
|
||||
if (sqNorm <= FUNC_SVG_SQ_SCALED(ARROW_DEAD)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void TouchControls::FunctionLeft::maskToLeftButtons(uint32 oldMask, uint32 newMask) {
|
||||
static const Common::JoystickButton buttons[] = {
|
||||
Common::JOYSTICK_BUTTON_DPAD_UP, Common::JOYSTICK_BUTTON_DPAD_RIGHT,
|
||||
Common::JOYSTICK_BUTTON_DPAD_DOWN, Common::JOYSTICK_BUTTON_DPAD_LEFT
|
||||
};
|
||||
|
||||
uint32 diff = newMask ^ oldMask;
|
||||
|
||||
for(int i = 0, m = 1; i < ARRAYSIZE(buttons); i++, m <<= 1) {
|
||||
if (!(diff & m)) {
|
||||
continue;
|
||||
}
|
||||
if (oldMask & m) {
|
||||
TouchControls::buttonUp(buttons[i]);
|
||||
}
|
||||
}
|
||||
if (diff & 16) {
|
||||
if (oldMask & 16) {
|
||||
TouchControls::buttonUp(Common::JOYSTICK_BUTTON_RIGHT_SHOULDER);
|
||||
} else {
|
||||
TouchControls::buttonDown(Common::JOYSTICK_BUTTON_RIGHT_SHOULDER);
|
||||
}
|
||||
}
|
||||
for(int i = 0, m = 1; i < ARRAYSIZE(buttons); i++, m <<= 1) {
|
||||
if (!(diff & m)) {
|
||||
continue;
|
||||
}
|
||||
if (newMask & m) {
|
||||
TouchControls::buttonDown(buttons[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TouchControls::FunctionLeft::touch(int dX, int dY, Action action) {
|
||||
if (action == JACTION_CANCEL ||
|
||||
action == JACTION_UP) {
|
||||
maskToLeftButtons(mask, 0);
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32 newMask = 0;
|
||||
|
||||
// norm 2 squared (to avoid square root)
|
||||
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
|
||||
|
||||
if (sqNorm >= FUNC_SVG_SQ_SCALED(ARROW_DEAD)) {
|
||||
// We are far enough from the center
|
||||
// For right we use angles -60,60 as a sensitive zone
|
||||
// For left it's the same but mirrored in negative
|
||||
// We must be between the two lines which are of tan(60),tan(-60) and this corrsponds to sqrt(3)
|
||||
// For up down we use angles -30,30 as a sensitive zone
|
||||
// We must be outside the two lines which are of tan(30),tan(-30) and this corrsponds to 1/sqrt(3)
|
||||
/*
|
||||
static const double SQRT3 = 1.73205080756887719318;
|
||||
unsigned int sq3 = SQRT3 * abs(dX);
|
||||
*/
|
||||
// Optimize by using an approximation of sqrt(3)
|
||||
unsigned int sq3 = abs(dX) * 51409 / 29681;
|
||||
unsigned int isq3 = abs(dX) * 29681 / 51409;
|
||||
|
||||
unsigned int adY = abs(dY);
|
||||
|
||||
if (adY <= sq3) {
|
||||
// Left or right
|
||||
if (dX < 0) {
|
||||
newMask |= 8;
|
||||
} else {
|
||||
newMask |= 2;
|
||||
}
|
||||
}
|
||||
if (adY >= isq3) {
|
||||
// Up or down
|
||||
if (dY < 0) {
|
||||
newMask |= 1;
|
||||
} else {
|
||||
newMask |= 4;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (sqNorm > FUNC_SVG_SQ_SCALED(ARROW_SUPER)) {
|
||||
newMask |= 16;
|
||||
}
|
||||
|
||||
if (mask != newMask) {
|
||||
maskToLeftButtons(mask, newMask);
|
||||
}
|
||||
|
||||
mask = newMask;
|
||||
}
|
||||
|
||||
void TouchControls::FunctionLeft::draw(uint8 alpha) {
|
||||
// Draw background
|
||||
{
|
||||
Common::Rect clip(0, 0, BG_WIDTH, BG_HEIGHT);
|
||||
parent->drawSurface(alpha, startX, startY, -clip.width() / 2, -clip.height() / 2, clip);
|
||||
}
|
||||
|
||||
if (mask == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Width and height here are rotated for left/right
|
||||
uint16 width = ARROW_WIDTH;
|
||||
uint16 height;
|
||||
if (mask & 16) {
|
||||
height = ARROW_HEIGHT;
|
||||
} else {
|
||||
height = ARROW_SEMI_HEIGHT;
|
||||
}
|
||||
|
||||
// We can draw multiple arrows
|
||||
if (mask & 1) {
|
||||
// Draw UP
|
||||
Common::Rect clip(width, height);
|
||||
clip.translate(0, ARROW_HL_TOP + ARROW_HEIGHT - height);
|
||||
int16 offX = -1, offY = -2;
|
||||
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
|
||||
}
|
||||
if (mask & 2) {
|
||||
// Draw RIGHT
|
||||
Common::Rect clip(height, width);
|
||||
clip.translate(ARROW_WIDTH, ARROW_HL_TOP);
|
||||
int16 offX = 0, offY = -1;
|
||||
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
|
||||
}
|
||||
if (mask & 4) {
|
||||
// Draw DOWN
|
||||
Common::Rect clip(width, height);
|
||||
clip.translate(0, ARROW_HL_TOP + ARROW_HEIGHT);
|
||||
int16 offX = -1, offY = 0;
|
||||
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
|
||||
}
|
||||
if (mask & 8) {
|
||||
// Draw LEFT
|
||||
Common::Rect clip(height, width);
|
||||
clip.translate(ARROW_WIDTH + ARROW_WIDTH - height, ARROW_HL_TOP + ARROW_HEIGHT);
|
||||
int16 offX = -2, offY = -1;
|
||||
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
|
||||
}
|
||||
}
|
||||
|
||||
bool TouchControls::FunctionRight::isInside(int x, int y) {
|
||||
int dX = x - startX;
|
||||
int dY = y - startY;
|
||||
// norm 2 squared (to avoid square root)
|
||||
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
|
||||
|
||||
return sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END);
|
||||
}
|
||||
|
||||
void TouchControls::FunctionRight::touch(int dX, int dY, Action action) {
|
||||
static const Common::JoystickButton buttons[] = {
|
||||
Common::JOYSTICK_BUTTON_Y, Common::JOYSTICK_BUTTON_B,
|
||||
Common::JOYSTICK_BUTTON_A, Common::JOYSTICK_BUTTON_X
|
||||
};
|
||||
static const Common::JoystickButton modifiers[] = {
|
||||
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID,
|
||||
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID
|
||||
};
|
||||
if (action == JACTION_CANCEL ||
|
||||
action == JACTION_UP) {
|
||||
if (button) {
|
||||
buttonUp(buttons[button - 1]);
|
||||
buttonUp(modifiers[button - 1]);
|
||||
}
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32 newButton = 0;
|
||||
|
||||
// norm 2 squared (to avoid square root)
|
||||
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
|
||||
|
||||
if (sqNorm >= FUNC_SVG_SQ_SCALED(BUTTON_DEAD) && sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END)) {
|
||||
// We are far enough from the center
|
||||
// For right we use angles -45,45 as a sensitive zone
|
||||
// For left it's the same but mirrored in negative
|
||||
// We must be between the two lines which are of tan(45),tan(-45) and this corrsponds to 1
|
||||
// For up down we use angles -45,45 as a sensitive zone
|
||||
// We must be outside the two lines which are of tan(45),tan(-45) and this corrsponds to 1
|
||||
unsigned int adX = abs(dX);
|
||||
unsigned int adY = abs(dY);
|
||||
|
||||
if (adY <= adX) {
|
||||
// X or B
|
||||
if (dX < 0) {
|
||||
newButton = 4;
|
||||
} else {
|
||||
newButton = 2;
|
||||
}
|
||||
} else {
|
||||
// Y or A
|
||||
if (dY < 0) {
|
||||
newButton = 1;
|
||||
} else {
|
||||
newButton = 3;
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
newButton = 0;
|
||||
}
|
||||
|
||||
if (button != newButton) {
|
||||
// Release the previously pressed button, if any
|
||||
if (button) {
|
||||
buttonUp(buttons[button - 1]);
|
||||
buttonUp(modifiers[button - 1]);
|
||||
}
|
||||
button = newButton;
|
||||
// Press the new button
|
||||
if (button) {
|
||||
buttonDown(modifiers[button - 1]);
|
||||
buttonDown(buttons[button - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TouchControls::FunctionRight::draw(uint8 alpha) {
|
||||
// Draw background
|
||||
{
|
||||
Common::Rect clip(BG_WIDTH, 0, 2 * BG_WIDTH, BG_HEIGHT);
|
||||
parent->drawSurface(alpha, startX, startY, -clip.width() / 2, -clip.height() / 2, clip);
|
||||
}
|
||||
|
||||
if (button == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Common::Rect clip(BUTTON_WIDTH, BUTTON_HEIGHT);
|
||||
|
||||
int16 offX, offY;
|
||||
|
||||
if (button == 1) {
|
||||
// Draw Y
|
||||
clip.translate(BUTTON_HL_LEFT, BUTTON_HL_TOP);
|
||||
offX = -1;
|
||||
offY = -2;
|
||||
} else if (button == 2) {
|
||||
// Draw B
|
||||
clip.translate(BUTTON_HL_LEFT + BUTTON_WIDTH, BUTTON_HL_TOP);
|
||||
offX = 0;
|
||||
offY = -1;
|
||||
} else if (button == 3) {
|
||||
// Draw A
|
||||
clip.translate(BUTTON_HL_LEFT, BUTTON_HL_TOP + BUTTON_HEIGHT);
|
||||
offX = -1;
|
||||
offY = 0;
|
||||
} else if (button == 4) {
|
||||
// Draw X
|
||||
clip.translate(BUTTON_HL_LEFT + BUTTON_WIDTH, BUTTON_HL_TOP + BUTTON_HEIGHT);
|
||||
offX = -2;
|
||||
offY = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
|
||||
}
|
||||
|
||||
bool TouchControls::FunctionCenter::isInside(int x, int y) {
|
||||
int dX = x - startX;
|
||||
int dY = y - startY;
|
||||
// norm 2 squared (to avoid square root)
|
||||
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
|
||||
|
||||
return sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END);
|
||||
}
|
||||
|
||||
void TouchControls::FunctionCenter::touch(int dX, int dY, Action action) {
|
||||
static const Common::JoystickButton buttons[] = {
|
||||
Common::JOYSTICK_BUTTON_GUIDE, Common::JOYSTICK_BUTTON_RIGHT_STICK,
|
||||
Common::JOYSTICK_BUTTON_START, Common::JOYSTICK_BUTTON_LEFT_STICK
|
||||
};
|
||||
static const Common::JoystickButton modifiers[] = {
|
||||
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID,
|
||||
Common::JOYSTICK_BUTTON_INVALID, Common::JOYSTICK_BUTTON_INVALID
|
||||
};
|
||||
if (action == JACTION_CANCEL ||
|
||||
action == JACTION_UP) {
|
||||
if (button) {
|
||||
buttonUp(buttons[button - 1]);
|
||||
buttonUp(modifiers[button - 1]);
|
||||
}
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
|
||||
uint32 newButton = 0;
|
||||
|
||||
// norm 2 squared (to avoid square root)
|
||||
unsigned int sqNorm = (unsigned int)(dX * dX) + (unsigned int)(dY * dY);
|
||||
|
||||
if (sqNorm >= FUNC_SVG_SQ_SCALED(BUTTON_DEAD) && sqNorm <= FUNC_SVG_SQ_SCALED(BUTTON_END)) {
|
||||
// We are far enough from the center
|
||||
// For right we use angles -45,45 as a sensitive zone
|
||||
// For left it's the same but mirrored in negative
|
||||
// We must be between the two lines which are of tan(45),tan(-45) and this corrsponds to 1
|
||||
// For up down we use angles -45,45 as a sensitive zone
|
||||
// We must be outside the two lines which are of tan(45),tan(-45) and this corrsponds to 1
|
||||
unsigned int adX = abs(dX);
|
||||
unsigned int adY = abs(dY);
|
||||
|
||||
if (adY <= adX) {
|
||||
// X or B
|
||||
if (dX < 0) {
|
||||
newButton = 4;
|
||||
} else {
|
||||
newButton = 2;
|
||||
}
|
||||
} else {
|
||||
// Y or A
|
||||
if (dY < 0) {
|
||||
newButton = 1;
|
||||
} else {
|
||||
newButton = 3;
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
newButton = 0;
|
||||
}
|
||||
|
||||
if (button != newButton) {
|
||||
// Release the previously pressed button, if any
|
||||
if (button) {
|
||||
buttonUp(buttons[button - 1]);
|
||||
buttonUp(modifiers[button - 1]);
|
||||
}
|
||||
button = newButton;
|
||||
// Press the new button
|
||||
if (button) {
|
||||
buttonDown(modifiers[button - 1]);
|
||||
buttonDown(buttons[button - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TouchControls::FunctionCenter::draw(uint8 alpha) {
|
||||
// Draw background
|
||||
{
|
||||
Common::Rect clip(BG_WIDTH * 2, 0, 3 * BG_WIDTH, BG_HEIGHT);
|
||||
parent->drawSurface(alpha, startX, startY, -clip.width() / 2, -clip.height() / 2, clip);
|
||||
}
|
||||
|
||||
if (button == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Common::Rect clip(BUTTON_WIDTH, BUTTON_HEIGHT);
|
||||
|
||||
int16 offX, offY;
|
||||
|
||||
if (button == 1) {
|
||||
// Draw Y
|
||||
clip.translate(BUTTON2_HL_LEFT, BUTTON2_HL_TOP);
|
||||
offX = -1;
|
||||
offY = -2;
|
||||
} else if (button == 2) {
|
||||
// Draw B
|
||||
clip.translate(BUTTON2_HL_LEFT + BUTTON_WIDTH, BUTTON2_HL_TOP);
|
||||
offX = 0;
|
||||
offY = -1;
|
||||
} else if (button == 3) {
|
||||
// Draw A
|
||||
clip.translate(BUTTON2_HL_LEFT, BUTTON2_HL_TOP + BUTTON_HEIGHT);
|
||||
offX = -1;
|
||||
offY = 0;
|
||||
} else if (button == 4) {
|
||||
// Draw X
|
||||
clip.translate(BUTTON2_HL_LEFT + BUTTON_WIDTH, BUTTON2_HL_TOP + BUTTON_HEIGHT);
|
||||
offX = -2;
|
||||
offY = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Always draw overlay as opaque
|
||||
parent->drawSurface(ALPHA_OPAQUE, startX, startY, offX * clip.width() / 2, offY * clip.height() / 2, clip);
|
||||
}
|
||||
174
backends/platform/android/touchcontrols.h
Normal file
174
backends/platform/android/touchcontrols.h
Normal file
@@ -0,0 +1,174 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ANDROID_TOUCHCONTROLS_H_
|
||||
#define ANDROID_TOUCHCONTROLS_H_
|
||||
|
||||
#include "common/events.h"
|
||||
|
||||
namespace Graphics {
|
||||
class ManagedSurface;
|
||||
}
|
||||
|
||||
class TouchControlsDrawer {
|
||||
public:
|
||||
virtual void touchControlInitSurface(const Graphics::ManagedSurface &surf) = 0;
|
||||
virtual void touchControlNotifyChanged() = 0;
|
||||
virtual void touchControlDraw(uint8 alpha, int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) = 0;
|
||||
|
||||
protected:
|
||||
~TouchControlsDrawer() {}
|
||||
};
|
||||
|
||||
class TouchControls {
|
||||
public:
|
||||
// action type
|
||||
enum Action {
|
||||
JACTION_DOWN = 0,
|
||||
JACTION_MOVE = 1,
|
||||
JACTION_UP = 2,
|
||||
JACTION_CANCEL = 3
|
||||
};
|
||||
|
||||
TouchControls();
|
||||
~TouchControls();
|
||||
|
||||
void init(float scale);
|
||||
void setDrawer(TouchControlsDrawer *drawer, int width, int height);
|
||||
void beforeDraw();
|
||||
void draw();
|
||||
void update(Action action, int ptr, int x, int y);
|
||||
|
||||
private:
|
||||
TouchControlsDrawer *_drawer;
|
||||
|
||||
unsigned int _screen_width, _screen_height;
|
||||
unsigned int _scale, _scale2;
|
||||
|
||||
Graphics::ManagedSurface *_svg;
|
||||
|
||||
unsigned int _zombieCount;
|
||||
|
||||
enum State {
|
||||
kFunctionInactive = 0,
|
||||
kFunctionActive = 1,
|
||||
kFunctionZombie = 2
|
||||
};
|
||||
|
||||
struct Function {
|
||||
virtual bool isInside(int, int) = 0;
|
||||
virtual void touch(int, int, Action) = 0;
|
||||
virtual void draw(uint8 alpha) = 0;
|
||||
virtual void resetState() {}
|
||||
|
||||
Function(const TouchControls *parent_) :
|
||||
parent(parent_), pointerId(-1),
|
||||
startX(-1), startY(-1),
|
||||
currentX(-1), currentY(-1),
|
||||
lastActivable(0), status(kFunctionInactive) {}
|
||||
virtual ~Function() {}
|
||||
|
||||
void reset() {
|
||||
pointerId = -1;
|
||||
startX = startY = currentX = currentY = -1;
|
||||
lastActivable = 0;
|
||||
status = kFunctionInactive;
|
||||
resetState();
|
||||
}
|
||||
|
||||
const TouchControls *parent;
|
||||
|
||||
int pointerId;
|
||||
uint16 startX, startY;
|
||||
uint16 currentX, currentY;
|
||||
uint32 lastActivable;
|
||||
State status;
|
||||
};
|
||||
Function *getFunctionFromPointerId(int ptr);
|
||||
Function *getZombieFunctionFromPos(int x, int y);
|
||||
|
||||
enum FunctionId {
|
||||
kFunctionNone = -1,
|
||||
kFunctionLeft = 0,
|
||||
kFunctionRight = 1,
|
||||
kFunctionCenter = 2,
|
||||
kFunctionCount = 3
|
||||
};
|
||||
FunctionId getFunctionId(int x, int y);
|
||||
|
||||
Function *_functions[kFunctionCount];
|
||||
|
||||
static void buttonDown(Common::JoystickButton jb);
|
||||
static void buttonUp(Common::JoystickButton jb);
|
||||
static void buttonPress(Common::JoystickButton jb);
|
||||
|
||||
/**
|
||||
* Draws a part of the joystick surface on the screen
|
||||
*
|
||||
* @param x The left coordinate in fixed-point screen pixels
|
||||
* @param y The top coordinate in fixed-point screen pixels
|
||||
* @param offX The left offset in SVG pixels
|
||||
* @param offY The top offset in SVG pixels
|
||||
* @param clip The clipping rectangle in source surface in SVG pixels
|
||||
*/
|
||||
void drawSurface(uint8 alpha, int x, int y, int offX, int offY, const Common::Rect &clip) const;
|
||||
|
||||
|
||||
// Functions implementations
|
||||
struct FunctionLeft : Function {
|
||||
FunctionLeft(const TouchControls *parent) :
|
||||
Function(parent), mask(0) {}
|
||||
void resetState() override { mask = 0; }
|
||||
|
||||
bool isInside(int, int) override;
|
||||
void touch(int, int, Action) override;
|
||||
void draw(uint8 alpha) override;
|
||||
|
||||
uint32 mask;
|
||||
void maskToLeftButtons(uint32 oldMask, uint32 newMask);
|
||||
};
|
||||
|
||||
struct FunctionRight : Function {
|
||||
FunctionRight(const TouchControls *parent) :
|
||||
Function(parent), button(0) {}
|
||||
void resetState() override { button = 0; }
|
||||
|
||||
bool isInside(int, int) override;
|
||||
void touch(int, int, Action) override;
|
||||
void draw(uint8 alpha) override;
|
||||
|
||||
uint32 button;
|
||||
};
|
||||
|
||||
struct FunctionCenter : Function {
|
||||
FunctionCenter(const TouchControls *parent) :
|
||||
Function(parent), button(0) {}
|
||||
void resetState() override { button = 0; }
|
||||
|
||||
bool isInside(int, int) override;
|
||||
void touch(int, int, Action) override;
|
||||
void draw(uint8 alpha) override;
|
||||
|
||||
uint32 button;
|
||||
};
|
||||
};
|
||||
|
||||
#endif
|
||||
40
backends/platform/atari/atari-debug.cpp
Normal file
40
backends/platform/atari/atari-debug.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "backends/platform/atari/atari-debug.h"
|
||||
|
||||
#ifdef DISABLE_TEXT_CONSOLE
|
||||
|
||||
void atari_debug(const char *s, ...) {
|
||||
va_list va;
|
||||
|
||||
va_start(va, s);
|
||||
|
||||
Common::String buf = Common::String::vformat(s, va);
|
||||
buf += '\n';
|
||||
|
||||
if (g_system)
|
||||
g_system->logMessage(LogMessageType::kDebug, buf.c_str());
|
||||
|
||||
va_end(va);
|
||||
}
|
||||
|
||||
#endif
|
||||
42
backends/platform/atari/atari-debug.h
Normal file
42
backends/platform/atari/atari-debug.h
Normal file
@@ -0,0 +1,42 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_ATARI_DEBUG_H
|
||||
#define PLATFORM_ATARI_DEBUG_H
|
||||
|
||||
#include "common/debug.h"
|
||||
#include "common/str.h"
|
||||
#include "common/system.h"
|
||||
#include "common/textconsole.h"
|
||||
|
||||
#ifdef DISABLE_TEXT_CONSOLE
|
||||
|
||||
void atari_debug(const char *s, ...);
|
||||
#define atari_warning atari_debug
|
||||
|
||||
#else
|
||||
|
||||
#define atari_debug debug
|
||||
#define atari_warning warning
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
137
backends/platform/atari/atari.mk
Normal file
137
backends/platform/atari/atari.mk
Normal file
@@ -0,0 +1,137 @@
|
||||
.PHONY: atarilitedist atarifulldist fbdist
|
||||
|
||||
DIST_FILES_PLATFORM := $(srcdir)/backends/platform/atari/readme.txt
|
||||
ifneq (${BACKEND},sdl)
|
||||
DIST_FILES_PLATFORM += $(srcdir)/backends/platform/atari/patches
|
||||
endif
|
||||
|
||||
LITE_DIR := scummvm-${VERSION}-atari-lite
|
||||
LITE_DATA := ${LITE_DIR}/data
|
||||
LITE_DOCS := ${LITE_DIR}/doc
|
||||
LITE_THEMES :=
|
||||
|
||||
FULL_DIR := scummvm-${VERSION}-atari-full
|
||||
FULL_DATA := ${FULL_DIR}/data
|
||||
FULL_DOCS := ${FULL_DIR}/doc
|
||||
FULL_THEMES := ${FULL_DIR}/themes
|
||||
|
||||
FB_DIR := scummvm-${VERSION}-firebee
|
||||
FB_DATA := ${FB_DIR}
|
||||
FB_DOCS := ${FB_DIR}/doc
|
||||
FB_THEMES := ${FB_DIR}
|
||||
|
||||
atarilitedist: $(EXECUTABLE)
|
||||
$(RM_REC) ${LITE_DIR}
|
||||
$(MKDIR) ${LITE_DIR}
|
||||
|
||||
$(CP) $(EXECUTABLE) ${LITE_DIR}
|
||||
$(NM) -C ${LITE_DIR}/$(EXECUTABLE) | grep -vF ' .L' | grep ' [TtWV] ' | $(CXXFILT) | sort -u > ${LITE_DIR}/scummvm.sym
|
||||
$(STRIP) -s ${LITE_DIR}/$(EXECUTABLE)
|
||||
|
||||
$(MKDIR) ${LITE_DOCS}
|
||||
$(CP) $(DIST_FILES_DOCS) ${LITE_DOCS}
|
||||
|
||||
$(MKDIR) ${LITE_DATA}
|
||||
$(CP) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) ${LITE_DATA}
|
||||
|
||||
# remove unused files
|
||||
$(RM) ${LITE_DATA}/helpdialog.zip
|
||||
$(RM) $(addsuffix .dat, $(addprefix ${LITE_DATA}/, achievements classicmacfonts encoding macgui))
|
||||
|
||||
# rename remaining files still not fitting into the 8+3 limit (this has to be supported by the backend, too)
|
||||
! [ -f ${LITE_DATA}/supernova.dat ] || mv ${LITE_DATA}/supernova.dat ${LITE_DATA}/supernov.dat
|
||||
! [ -f ${LITE_DATA}/teenagent.dat ] || mv ${LITE_DATA}/teenagent.dat ${LITE_DATA}/teenagen.dat
|
||||
|
||||
# readme.txt
|
||||
$(CP) -r $(DIST_FILES_PLATFORM) ${LITE_DIR}
|
||||
unix2dos ${LITE_DIR}/readme.txt
|
||||
|
||||
ifeq ($(CREATE_ZIP),y)
|
||||
$(RM) ../${LITE_DIR}.zip
|
||||
$(ZIP) -r -9 ../${LITE_DIR}.zip ${LITE_DIR}
|
||||
endif
|
||||
|
||||
atarifulldist: $(EXECUTABLE)
|
||||
$(RM_REC) ${FULL_DIR}
|
||||
$(MKDIR) ${FULL_DIR}
|
||||
|
||||
$(CP) $(EXECUTABLE) ${FULL_DIR}
|
||||
$(NM) -C ${FULL_DIR}/$(EXECUTABLE) | grep -vF ' .L' | grep ' [TtWV] ' | $(CXXFILT) | sort -u > ${FULL_DIR}/scummvm.sym
|
||||
$(STRIP) -s ${FULL_DIR}/$(EXECUTABLE)
|
||||
|
||||
$(MKDIR) ${FULL_DOCS}
|
||||
$(CP) $(DIST_FILES_DOCS) ${FULL_DOCS}
|
||||
|
||||
$(MKDIR) ${FULL_DATA}
|
||||
$(CP) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) ${FULL_DATA}
|
||||
|
||||
# remove unused files
|
||||
$(RM) ${FULL_DATA}/helpdialog.zip
|
||||
$(RM) $(addsuffix .dat, $(addprefix ${FULL_DATA}/, achievements classicmacfonts encoding hadesch_translations macgui prince_translation))
|
||||
|
||||
# rename remaining files still not fitting into the 8+3 limit (this has to be supported by the backend, too)
|
||||
! [ -f ${FULL_DATA}/cryomni3d.dat ] || mv ${FULL_DATA}/cryomni3d.dat ${FULL_DATA}/cryomni3.dat
|
||||
! [ -f ${FULL_DATA}/neverhood.dat ] || mv ${FULL_DATA}/neverhood.dat ${FULL_DATA}/neverhoo.dat
|
||||
! [ -f ${FULL_DATA}/supernova.dat ] || mv ${FULL_DATA}/supernova.dat ${FULL_DATA}/supernov.dat
|
||||
! [ -f ${FULL_DATA}/teenagent.dat ] || mv ${FULL_DATA}/teenagent.dat ${FULL_DATA}/teenagen.dat
|
||||
|
||||
$(MKDIR) ${FULL_THEMES}
|
||||
$(CP) $(DIST_FILES_THEMES) ${FULL_THEMES}
|
||||
|
||||
# remove unused files; absent gui-icons.dat massively speeds up startup time (it is used for the grid mode only)
|
||||
$(RM) ${FULL_THEMES}/gui-icons.dat ${FULL_THEMES}/shaders.dat
|
||||
|
||||
# adjust to compression level zero for faster depacking
|
||||
cd ${FULL_THEMES} && \
|
||||
for f in *.zip; \
|
||||
do \
|
||||
unzip -q -d tmp "$$f" && $(RM) "$$f" && cd tmp && $(ZIP) -0 "../$$f" * && cd .. && $(RM_REC) tmp; \
|
||||
done
|
||||
|
||||
# readme.txt
|
||||
$(CP) -r $(DIST_FILES_PLATFORM) ${FULL_DIR}
|
||||
unix2dos ${FULL_DIR}/readme.txt
|
||||
|
||||
ifeq ($(CREATE_ZIP),y)
|
||||
$(RM) ../${FULL_DIR}.zip
|
||||
$(ZIP) -r -9 ../${FULL_DIR}.zip ${FULL_DIR}
|
||||
endif
|
||||
|
||||
fbdist: $(EXECUTABLE)
|
||||
$(RM_REC) ${FB_DIR}
|
||||
$(MKDIR) ${FB_DIR}
|
||||
|
||||
$(CP) $(EXECUTABLE) ${FB_DIR}
|
||||
$(STRIP) -s ${FB_DIR}/$(EXECUTABLE)
|
||||
|
||||
$(MKDIR) ${FB_DOCS}
|
||||
$(CP) $(DIST_FILES_DOCS) ${FB_DOCS}
|
||||
|
||||
$(MKDIR) ${FB_DATA}
|
||||
$(CP) $(DIST_FILES_ENGINEDATA) $(DIST_FILES_ENGINEDATA_BIG) ${FB_DATA}
|
||||
|
||||
# remove unused files
|
||||
$(RM) ${FB_DATA}/helpdialog.zip
|
||||
$(RM) $(addsuffix .dat, $(addprefix ${FB_DATA}/, achievements classicmacfonts encoding hadesch_translations macgui prince_translation))
|
||||
|
||||
$(MKDIR) ${FB_THEMES}
|
||||
$(CP) $(DIST_FILES_THEMES) ${FB_THEMES}
|
||||
|
||||
# remove unused files
|
||||
$(RM) ${FB_THEMES}/shaders.dat
|
||||
|
||||
# adjust to compression level zero for faster depacking
|
||||
cd ${FB_THEMES} && \
|
||||
for f in *.zip; \
|
||||
do \
|
||||
unzip -q -d tmp "$$f" && $(RM) "$$f" && cd tmp && $(ZIP) -0 "../$$f" * && cd .. && $(RM_REC) tmp; \
|
||||
done
|
||||
|
||||
# readme.txt
|
||||
$(CP) -r $(DIST_FILES_PLATFORM) ${FB_DIR}
|
||||
unix2dos ${FB_DIR}/readme.txt
|
||||
|
||||
ifeq ($(CREATE_ZIP),y)
|
||||
$(RM) ../${FB_DIR}.zip
|
||||
$(ZIP) -r -9 ../${FB_DIR}.zip ${FB_DIR}
|
||||
endif
|
||||
121
backends/platform/atari/atari_ikbd.S
Normal file
121
backends/platform/atari/atari_ikbd.S
Normal file
@@ -0,0 +1,121 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "symbols.h"
|
||||
|
||||
.global SYM(atari_kbdvec)
|
||||
.global SYM(atari_mousevec)
|
||||
|
||||
.global SYM(atari_old_kbdvec)
|
||||
.global SYM(atari_old_mousevec)
|
||||
|
||||
.extern SYM(g_atari_ikbd_mousebuttons)
|
||||
.extern SYM(g_atari_ikbd_mousebuttons_mask)
|
||||
.extern SYM(g_atari_ikbd_mousebuttons_head)
|
||||
|
||||
.extern SYM(g_atari_ikbd_mouse_delta_x)
|
||||
.extern SYM(g_atari_ikbd_mouse_delta_y)
|
||||
|
||||
.extern SYM(g_atari_ikbd_scancodes)
|
||||
.extern SYM(g_atari_ikbd_scancodes_mask)
|
||||
.extern SYM(g_atari_ikbd_scancodes_head)
|
||||
|
||||
.text
|
||||
|
||||
.ascii "XBRA"
|
||||
.ascii "SCUM"
|
||||
SYM(atari_old_kbdvec):
|
||||
dc.l 0
|
||||
SYM(atari_kbdvec):
|
||||
lea pressed_keys,a0
|
||||
clr.l d1
|
||||
move.b d0,d1
|
||||
bpl.b key_pressed | bit 7 cleared
|
||||
|
||||
key_released:
|
||||
and.b #0x7f,d1
|
||||
tst.b (a0,d1.l) | pressed before?
|
||||
bne.b key_released_ok
|
||||
|
||||
| if we get a sudden release key event,
|
||||
| let the original handler process it
|
||||
move.l (atari_old_kbdvec,pc),a0
|
||||
jmp (a0)
|
||||
|
||||
key_released_ok:
|
||||
clr.b (a0,d1.l) | mark as released
|
||||
bra.b kbdvec_process
|
||||
|
||||
key_pressed:
|
||||
addq.b #1,(a0,d1.l) | mark as pressed
|
||||
|
||||
kbdvec_process:
|
||||
lea SYM(g_atari_ikbd_scancodes),a0
|
||||
move.w SYM(g_atari_ikbd_scancodes_head),d1
|
||||
|
||||
| g_atari_ikbd_scancodes[g_atari_ikbd_scancodes_head] = scancode
|
||||
|
||||
move.b d0,(0.b,a0,d1.w)
|
||||
|
||||
addq.l #1,d1
|
||||
and.w SYM(g_atari_ikbd_scancodes_mask),d1
|
||||
move.w d1,SYM(g_atari_ikbd_scancodes_head)
|
||||
rts
|
||||
|
||||
|
||||
.ascii "XBRA"
|
||||
.ascii "SCUM"
|
||||
SYM(atari_old_mousevec):
|
||||
dc.l 0
|
||||
SYM(atari_mousevec):
|
||||
move.b (a0)+,d0
|
||||
cmp.b old_buttons,d0
|
||||
beq.b no_buttons
|
||||
|
||||
move.b d0,old_buttons
|
||||
|
||||
lea SYM(g_atari_ikbd_mousebuttons),a1
|
||||
move.w SYM(g_atari_ikbd_mousebuttons_head),d1
|
||||
|
||||
| g_atari_ikbd_mousebuttons[g_atari_ikbd_mousebuttons_head] = buttons
|
||||
|
||||
move.b d0,(0.b,a1,d1.w)
|
||||
|
||||
addq.w #1,d1
|
||||
and.w SYM(g_atari_ikbd_mousebuttons_mask),d1
|
||||
move.w d1,SYM(g_atari_ikbd_mousebuttons_head)
|
||||
|
||||
no_buttons:
|
||||
move.b (a0)+,d0
|
||||
ext.w d0
|
||||
add.w d0,SYM(g_atari_ikbd_mouse_delta_x)
|
||||
|
||||
move.b (a0)+,d0
|
||||
ext.w d0
|
||||
add.w d0,SYM(g_atari_ikbd_mouse_delta_y)
|
||||
rts
|
||||
|
||||
.bss
|
||||
|
||||
pressed_keys:
|
||||
ds.b 128
|
||||
old_buttons:
|
||||
ds.b 1
|
||||
45
backends/platform/atari/build-firebee.sh
Normal file
45
backends/platform/atari/build-firebee.sh
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash -eux
|
||||
# -e: Exit immediately if a command exits with a non-zero status.
|
||||
# -u: Treat unset variables as an error when substituting.
|
||||
# -x: Display expanded script commands
|
||||
|
||||
mkdir -p build-firebee
|
||||
cd build-firebee
|
||||
|
||||
PLATFORM=m68k-atari-mintelf
|
||||
FASTCALL=false
|
||||
export CXXFLAGS="-mcpu=5475"
|
||||
export LDFLAGS="-mcpu=5475"
|
||||
#export CXXFLAGS="-m68020-60"
|
||||
#export LDFLAGS="-m68020-60"
|
||||
|
||||
CPU_DIR=$(${PLATFORM}-gcc ${CXXFLAGS} -print-multi-directory)
|
||||
|
||||
export PKG_CONFIG_LIBDIR="$(${PLATFORM}-gcc -print-sysroot)/usr/lib/${CPU_DIR}/pkgconfig"
|
||||
|
||||
if $FASTCALL
|
||||
then
|
||||
CXXFLAGS="$CXXFLAGS -mfastcall"
|
||||
LDFLAGS="$LDFLAGS -mfastcall"
|
||||
fi
|
||||
|
||||
if [ -f ../backends/platform/atari/.patched ]
|
||||
then
|
||||
echo "FireBee SDL target shouldn't contain any ATARI patches!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [ ! -f config.log ]
|
||||
then
|
||||
../configure \
|
||||
--backend=sdl \
|
||||
--host=${PLATFORM} \
|
||||
--with-sdl-prefix="$(${PLATFORM}-gcc -print-sysroot)/usr/bin/${CPU_DIR}" \
|
||||
--with-freetype2-prefix="$(${PLATFORM}-gcc -print-sysroot)/usr/bin/${CPU_DIR}" \
|
||||
--with-mikmod-prefix="$(${PLATFORM}-gcc -print-sysroot)/usr/bin/${CPU_DIR}" \
|
||||
--enable-release \
|
||||
--enable-verbose-build
|
||||
fi
|
||||
|
||||
make -j$(getconf _NPROCESSORS_CONF) fbdist
|
||||
41
backends/platform/atari/build-release.sh
Normal file
41
backends/platform/atari/build-release.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash -eux
|
||||
# -e: Exit immediately if a command exits with a non-zero status.
|
||||
# -u: Treat unset variables as an error when substituting.
|
||||
# -x: Display expanded script commands
|
||||
|
||||
mkdir -p build-release
|
||||
cd build-release
|
||||
|
||||
PLATFORM=m68k-atari-mintelf
|
||||
FASTCALL=false
|
||||
export ASFLAGS="-m68020-60"
|
||||
export CXXFLAGS="-m68020-60 -DUSE_MOVE16 -DUSE_SUPERVIDEL -DUSE_SV_BLITTER -DDISABLE_LAUNCHERDISPLAY_GRID"
|
||||
export LDFLAGS="-m68020-60"
|
||||
|
||||
export PKG_CONFIG_LIBDIR="$(${PLATFORM}-gcc -print-sysroot)/usr/lib/m68020-60/pkgconfig"
|
||||
|
||||
if $FASTCALL
|
||||
then
|
||||
ASFLAGS="$ASFLAGS -mfastcall"
|
||||
CXXFLAGS="$CXXFLAGS -mfastcall"
|
||||
LDFLAGS="$LDFLAGS -mfastcall"
|
||||
fi
|
||||
|
||||
if [ ! -f ../backends/platform/atari/.patched ]
|
||||
then
|
||||
cd .. && cat backends/platform/atari/patches/print_rate.patch | patch -p1 && cd -
|
||||
cd .. && cat backends/platform/atari/patches/tooltips.patch | patch -p1 && cd -
|
||||
touch ../backends/platform/atari/.patched
|
||||
fi
|
||||
|
||||
if [ ! -f config.log ]
|
||||
then
|
||||
../configure \
|
||||
--backend=atari \
|
||||
--host=${PLATFORM} \
|
||||
--enable-release \
|
||||
--enable-verbose-build \
|
||||
--disable-engine=hugo,director,cine,ultima
|
||||
fi
|
||||
|
||||
make -j$(getconf _NPROCESSORS_CONF) atarifulldist
|
||||
43
backends/platform/atari/build-release030.sh
Normal file
43
backends/platform/atari/build-release030.sh
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash -eux
|
||||
# -e: Exit immediately if a command exits with a non-zero status.
|
||||
# -u: Treat unset variables as an error when substituting.
|
||||
# -x: Display expanded script commands
|
||||
|
||||
mkdir -p build-release030
|
||||
cd build-release030
|
||||
|
||||
PLATFORM=m68k-atari-mintelf
|
||||
FASTCALL=false
|
||||
export ASFLAGS="-m68030"
|
||||
export CXXFLAGS="-m68030 -DDISABLE_FANCY_THEMES"
|
||||
export LDFLAGS="-m68030"
|
||||
|
||||
export PKG_CONFIG_LIBDIR="$(${PLATFORM}-gcc -print-sysroot)/usr/lib/m68020-60/pkgconfig"
|
||||
|
||||
if $FASTCALL
|
||||
then
|
||||
ASFLAGS="$ASFLAGS -mfastcall"
|
||||
CXXFLAGS="$CXXFLAGS -mfastcall"
|
||||
LDFLAGS="$LDFLAGS -mfastcall"
|
||||
fi
|
||||
|
||||
if [ ! -f ../backends/platform/atari/.patched ]
|
||||
then
|
||||
cd .. && cat backends/platform/atari/patches/print_rate.patch | patch -p1 && cd -
|
||||
cd .. && cat backends/platform/atari/patches/tooltips.patch | patch -p1 && cd -
|
||||
touch ../backends/platform/atari/.patched
|
||||
fi
|
||||
|
||||
if [ ! -f config.log ]
|
||||
then
|
||||
../configure \
|
||||
--backend=atari \
|
||||
--host=${PLATFORM} \
|
||||
--enable-release \
|
||||
--disable-highres \
|
||||
--disable-bink \
|
||||
--enable-verbose-build \
|
||||
--disable-engine=hugo,director,cine,ultima
|
||||
fi
|
||||
|
||||
make -j$(getconf _NPROCESSORS_CONF) atarilitedist
|
||||
4875
backends/platform/atari/dlmalloc.cpp
Normal file
4875
backends/platform/atari/dlmalloc.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1472
backends/platform/atari/dlmalloc.h
Normal file
1472
backends/platform/atari/dlmalloc.h
Normal file
File diff suppressed because it is too large
Load Diff
13
backends/platform/atari/module.mk
Normal file
13
backends/platform/atari/module.mk
Normal file
@@ -0,0 +1,13 @@
|
||||
MODULE := backends/platform/atari
|
||||
|
||||
MODULE_OBJS := \
|
||||
osystem_atari.o \
|
||||
atari-debug.o \
|
||||
atari_ikbd.o \
|
||||
native_features.o \
|
||||
dlmalloc.o
|
||||
|
||||
# We don't use rules.mk but rather manually update OBJS and MODULE_DIRS.
|
||||
MODULE_OBJS := $(addprefix $(MODULE)/, $(MODULE_OBJS))
|
||||
OBJS := $(MODULE_OBJS) $(OBJS)
|
||||
MODULE_DIRS += $(sort $(dir $(MODULE_OBJS)))
|
||||
96
backends/platform/atari/native_features.cpp
Normal file
96
backends/platform/atari/native_features.cpp
Normal file
@@ -0,0 +1,96 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Taken from mintlib (https://github.com/freemint/mintlib)
|
||||
// (c) Thorsten Otto
|
||||
|
||||
#include <mint/osbind.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define NATFEAT_ID 0x7300
|
||||
#define NATFEAT_CALL 0x7301
|
||||
|
||||
#define ASM_NATFEAT3(opcode) "\t.word " #opcode "\n"
|
||||
#define ASM_NATFEAT2(opcode) ASM_NATFEAT3(opcode)
|
||||
#define ASM_NATFEAT(n) ASM_NATFEAT2(n)
|
||||
|
||||
static unsigned short const nf_id_opcodes[] = { NATFEAT_ID, 0x4e75 };
|
||||
static unsigned short const nf_call_opcodes[] = { NATFEAT_CALL, 0x4e75 };
|
||||
|
||||
#define _nf_get_id(feature_name) ((long (__CDECL *)(const char *))nf_id_opcodes)(feature_name)
|
||||
#define _nf_call(id, ...) ((long (__CDECL *)(long, ...))nf_call_opcodes)(id, __VA_ARGS__)
|
||||
|
||||
/*
|
||||
* on ColdFire, the NATFEAT_ID opcode is actually
|
||||
* "mvs.b d0,d1",
|
||||
* which means the following code will NOT detect
|
||||
* the presence of an emulator (should there ever
|
||||
* be an emulator capable of emulating a ColdFire processor).
|
||||
* Luckily, executing the code on a CF processor is still
|
||||
* harmless since all it does is clobber D1.
|
||||
*/
|
||||
static long _nf_detect_tos(void) {
|
||||
register long ret __asm__ ("d0");
|
||||
register const char *nf_version __asm__("a1") = "NF_VERSION";
|
||||
|
||||
__asm__ volatile(
|
||||
"\tmove.l %1,-(%%sp)\n"
|
||||
"\tmoveq #0,%%d0\n" /* assume no NatFeats available */
|
||||
"\tmove.l %%d0,-(%%sp)\n"
|
||||
"\tlea (1f:w,%%pc),%%a1\n"
|
||||
"\tmove.l (0x0010).w,%%a0\n" /* illegal instruction vector */
|
||||
"\tmove.l %%a1,(0x0010).w\n"
|
||||
"\tmove.l %%sp,%%a1\n" /* save the ssp */
|
||||
|
||||
"\tnop\n" /* flush pipelines (for 68040+) */
|
||||
|
||||
ASM_NATFEAT(NATFEAT_ID) /* Jump to NATFEAT_ID */
|
||||
"\ttst.l %%d0\n"
|
||||
"\tbeq.s 1f\n"
|
||||
"\tmoveq #1,%%d0\n" /* NatFeats detected */
|
||||
"\tmove.l %%d0,(%%sp)\n"
|
||||
|
||||
"1:\n"
|
||||
"\tmove.l %%a1,%%sp\n"
|
||||
"\tmove.l %%a0,(0x0010).w\n"
|
||||
"\tmove.l (%%sp)+,%%d0\n"
|
||||
"\taddq.l #4,%%sp\n" /* pop nf_version argument */
|
||||
|
||||
"\tnop\n" /* flush pipelines (for 68040+) */
|
||||
: "=g"(ret) /* outputs */
|
||||
: "g"(nf_version) /* inputs */
|
||||
: __CLOBBER_RETURN("d0") "a0", "d1", "cc" AND_MEMORY
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
long nf_stderr_id;
|
||||
|
||||
void nf_init(void) {
|
||||
long ret = Supexec(_nf_detect_tos);
|
||||
if (ret == 1)
|
||||
nf_stderr_id = _nf_get_id("NF_STDERR");
|
||||
}
|
||||
|
||||
void nf_print(const char* msg) {
|
||||
if (nf_stderr_id)
|
||||
_nf_call(nf_stderr_id | 0, (uint32_t)msg);
|
||||
}
|
||||
519
backends/platform/atari/osystem_atari.cpp
Normal file
519
backends/platform/atari/osystem_atari.cpp
Normal file
@@ -0,0 +1,519 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <gem.h>
|
||||
#include <mint/cookie.h>
|
||||
#include <mint/falcon.h>
|
||||
#include <mint/osbind.h>
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_FILE
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_fputs
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_getenv
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_sprintf
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_stderr
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_stdout
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_fprintf
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_exit
|
||||
|
||||
#include "backends/platform/atari/osystem_atari.h"
|
||||
|
||||
#include "backends/audiocd/default/default-audiocd.h"
|
||||
#include "common/config-manager.h"
|
||||
#include "backends/events/atari/atari-events.h"
|
||||
#include "backends/events/default/default-events.h"
|
||||
#include "backends/graphics/atari/atari-graphics.h"
|
||||
#include "backends/keymapper/hardware-input.h"
|
||||
#include "backends/mixer/atari/atari-mixer.h"
|
||||
#include "backends/mutex/null/null-mutex.h"
|
||||
#include "backends/platform/atari/atari-debug.h"
|
||||
#include "backends/saves/default/default-saves.h"
|
||||
#include "backends/timer/default/default-timer.h"
|
||||
#include "base/main.h"
|
||||
|
||||
#define INPUT_ACTIVE
|
||||
|
||||
/*
|
||||
* Include header files needed for the getFilesystemFactory() method.
|
||||
*/
|
||||
#include "backends/fs/atari/atari-fs-factory.h"
|
||||
|
||||
bool g_gameEngineActive = false;
|
||||
|
||||
extern "C" void atari_kbdvec(void *);
|
||||
extern "C" void atari_mousevec(void *);
|
||||
typedef void (*KBDVEC)(void *);
|
||||
extern "C" KBDVEC atari_old_kbdvec;
|
||||
extern "C" KBDVEC atari_old_mousevec;
|
||||
|
||||
extern void nf_init(void);
|
||||
extern void nf_print(const char* msg);
|
||||
|
||||
static int s_app_id = -1;
|
||||
static void (*s_old_procterm)(void) = nullptr;
|
||||
|
||||
static volatile uint32 counter_200hz;
|
||||
|
||||
static bool s_dtor_already_called = false;
|
||||
|
||||
static long atari_200hz_init(void)
|
||||
{
|
||||
__asm__ __volatile__(
|
||||
"\tmove %%sr,-(%%sp)\n"
|
||||
"\tor.w #0x700,%%sr\n"
|
||||
|
||||
"\tmove.l 0x114.w,old_200hz\n"
|
||||
"\tmove.l #my_200hz,0x114.w\n"
|
||||
|
||||
"\tmove (%%sp)+,%%sr\n"
|
||||
"\tjbra 1f\n"
|
||||
|
||||
"\tdc.l 0x58425241\n" /* "XBRA" */
|
||||
"\tdc.l 0x5343554d\n" /* "SCUM" */
|
||||
"old_200hz:\n"
|
||||
"\tdc.l 0\n"
|
||||
"my_200hz:\n"
|
||||
"\taddq.l #1,%0\n"
|
||||
|
||||
"\tmove.l old_200hz(%%pc),-(%%sp)\n"
|
||||
"\trts\n"
|
||||
"1:\n"
|
||||
: /* output */
|
||||
: "m"(counter_200hz) /* inputs */
|
||||
: "memory", "cc");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long atari_200hz_shutdown(void)
|
||||
{
|
||||
__asm__ __volatile__(
|
||||
"\tmove %%sr,-(%%sp)\n"
|
||||
"\tor.w #0x700,%%sr\n"
|
||||
|
||||
"\tmove.l old_200hz,0x114.w\n"
|
||||
|
||||
"\tmove (%%sp)+,%%sr\n"
|
||||
: /* output */
|
||||
: /* inputs */
|
||||
: "memory", "cc");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void critical_restore() {
|
||||
//atari_debug("critical_restore()");
|
||||
|
||||
Supexec(atari_200hz_shutdown);
|
||||
|
||||
#ifdef INPUT_ACTIVE
|
||||
if (atari_old_kbdvec && atari_old_mousevec) {
|
||||
_KBDVECS *kbdvecs = Kbdvbase();
|
||||
((uintptr *)kbdvecs)[-1] = (uintptr)atari_old_kbdvec;
|
||||
kbdvecs->mousevec = atari_old_mousevec;
|
||||
atari_old_kbdvec = atari_old_mousevec = nullptr;
|
||||
}
|
||||
|
||||
// don't call GEM cleanup in the critical handler: it seems that v_clsvwk()
|
||||
// somehow manipulates the same memory area used for the critical handler's stack
|
||||
// what causes v_clsvwk() never returning and leading to a bus error (and another
|
||||
// critical_restore() called...)
|
||||
if (s_app_id != -1) {
|
||||
// ok, restore mouse cursor at least
|
||||
graf_mouse(M_ON, NULL);
|
||||
}
|
||||
#endif
|
||||
|
||||
// avoid infinite recursion if either of the shutdown procedures fails
|
||||
(void)Setexc(VEC_PROCTERM, s_old_procterm);
|
||||
|
||||
extern void AtariAudioShutdown();
|
||||
extern void AtariGraphicsShutdown();
|
||||
|
||||
AtariAudioShutdown();
|
||||
AtariGraphicsShutdown();
|
||||
}
|
||||
|
||||
// called on normal program termination (via exit() or returning from main())
|
||||
static void exit_restore() {
|
||||
// causes a crash upon termination
|
||||
//atari_debug("exit_restore()");
|
||||
|
||||
if (!s_dtor_already_called)
|
||||
g_system->destroy();
|
||||
// else critical_restore() will be called, too
|
||||
}
|
||||
|
||||
OSystem_Atari::OSystem_Atari() {
|
||||
_fsFactory = new AtariFilesystemFactory();
|
||||
|
||||
nf_init();
|
||||
|
||||
enum {
|
||||
VDO_NO_ATARI_HW = 0xffff,
|
||||
VDO_ST = 0,
|
||||
VDO_STE,
|
||||
VDO_TT,
|
||||
VDO_FALCON,
|
||||
VDO_MILAN
|
||||
};
|
||||
|
||||
long vdo = VDO_NO_ATARI_HW<<16;
|
||||
Getcookie(C__VDO, &vdo);
|
||||
vdo >>= 16;
|
||||
|
||||
if (vdo != VDO_TT && vdo != VDO_FALCON) {
|
||||
fprintf(stderr, "ScummVM requires Atari TT/Falcon compatible video\n");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
enum {
|
||||
MCH_ST = 0,
|
||||
MCH_STE,
|
||||
MCH_TT,
|
||||
MCH_FALCON,
|
||||
MCH_CLONE,
|
||||
MCH_ARANYM
|
||||
};
|
||||
|
||||
long mch = MCH_ST<<16;
|
||||
Getcookie(C__MCH, &mch);
|
||||
mch >>= 16;
|
||||
|
||||
if (mch == MCH_ARANYM && Getcookie(C_fVDI, NULL) == C_FOUND) {
|
||||
fprintf(stderr, "Disable fVDI, ScummVM uses XBIOS video calls\n");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
#ifdef INPUT_ACTIVE
|
||||
_KBDVECS *kbdvecs = Kbdvbase();
|
||||
atari_old_kbdvec = (KBDVEC)(((uintptr *)kbdvecs)[-1]);
|
||||
atari_old_mousevec = kbdvecs->mousevec;
|
||||
|
||||
((uintptr *)kbdvecs)[-1] = (uintptr)atari_kbdvec;
|
||||
kbdvecs->mousevec = atari_mousevec;
|
||||
#endif
|
||||
|
||||
Supexec(atari_200hz_init);
|
||||
_timerInitialized = true;
|
||||
|
||||
// protect against sudden exit()
|
||||
atexit(exit_restore);
|
||||
// protect against sudden crash
|
||||
s_old_procterm = Setexc(VEC_PROCTERM, -1);
|
||||
(void)Setexc(VEC_PROCTERM, critical_restore);
|
||||
}
|
||||
|
||||
OSystem_Atari::~OSystem_Atari() {
|
||||
atari_debug("OSystem_Atari::~OSystem_Atari()");
|
||||
|
||||
s_dtor_already_called = true;
|
||||
|
||||
// _audiocdManager needs to be deleted before _mixerManager to avoid a crash.
|
||||
delete _audiocdManager;
|
||||
_audiocdManager = nullptr;
|
||||
|
||||
delete _mixerManager;
|
||||
_mixerManager = nullptr;
|
||||
|
||||
delete _graphicsManager;
|
||||
_graphicsManager = nullptr;
|
||||
|
||||
delete _eventManager;
|
||||
_eventManager = nullptr;
|
||||
|
||||
delete _savefileManager;
|
||||
_savefileManager = nullptr;
|
||||
|
||||
delete _timerManager;
|
||||
_timerManager = nullptr;
|
||||
|
||||
delete _fsFactory;
|
||||
_fsFactory = nullptr;
|
||||
|
||||
if (_timerInitialized) {
|
||||
Supexec(atari_200hz_shutdown);
|
||||
_timerInitialized = false;
|
||||
}
|
||||
|
||||
if (atari_old_kbdvec && atari_old_mousevec) {
|
||||
_KBDVECS *kbdvecs = Kbdvbase();
|
||||
((uintptr *)kbdvecs)[-1] = (uintptr)atari_old_kbdvec;
|
||||
kbdvecs->mousevec = atari_old_mousevec;
|
||||
atari_old_kbdvec = atari_old_mousevec = nullptr;
|
||||
}
|
||||
|
||||
if (s_app_id != -1) {
|
||||
//wind_update(END_UPDATE);
|
||||
|
||||
// redraw screen
|
||||
form_dial(FMD_FINISH, 0, 0, 0, 0, 0, 0, _vdi_width, _vdi_height);
|
||||
graf_mouse(M_ON, NULL);
|
||||
|
||||
v_clsvwk(_vdi_handle);
|
||||
appl_exit();
|
||||
}
|
||||
|
||||
// graceful exit
|
||||
(void)Setexc(VEC_PROCTERM, s_old_procterm);
|
||||
}
|
||||
|
||||
void OSystem_Atari::initBackend() {
|
||||
atari_debug("OSystem_Atari::initBackend()");
|
||||
|
||||
s_app_id = appl_init();
|
||||
if (s_app_id != -1) {
|
||||
// get the ID of the current physical screen workstation
|
||||
int16 dummy;
|
||||
_vdi_handle = graf_handle(&dummy, &dummy, &dummy, &dummy);
|
||||
if (_vdi_handle < 1) {
|
||||
appl_exit();
|
||||
error("graf_handle() failed");
|
||||
}
|
||||
|
||||
int16 work_in[16] = {};
|
||||
int16 work_out[57] = {};
|
||||
|
||||
// open a virtual screen workstation
|
||||
v_opnvwk(work_in, &_vdi_handle, work_out);
|
||||
|
||||
if (_vdi_handle == 0) {
|
||||
appl_exit();
|
||||
error("v_opnvwk() failed");
|
||||
}
|
||||
|
||||
_vdi_width = work_out[0] + 1;
|
||||
_vdi_height = work_out[1] + 1;
|
||||
|
||||
#ifdef INPUT_ACTIVE
|
||||
graf_mouse(M_OFF, NULL);
|
||||
// see https://github.com/freemint/freemint/issues/312
|
||||
//wind_update(BEG_UPDATE);
|
||||
#endif
|
||||
}
|
||||
|
||||
_timerManager = new DefaultTimerManager();
|
||||
_savefileManager = new DefaultSaveFileManager("saves");
|
||||
|
||||
AtariEventSource *atariEventSource = new AtariEventSource();
|
||||
_eventManager = new DefaultEventManager(makeKeyboardRepeatingEventSource(atariEventSource));
|
||||
|
||||
// AtariGraphicsManager needs _eventManager ready
|
||||
AtariGraphicsManager *atariGraphicsManager = new AtariGraphicsManager();
|
||||
_graphicsManager = atariGraphicsManager;
|
||||
|
||||
atariEventSource->setGraphicsManager(atariGraphicsManager);
|
||||
|
||||
#ifdef DISABLE_FANCY_THEMES
|
||||
// On the lite build force "STMIDI" as the audio driver, i.e. do not attempt
|
||||
// to emulate anything by default. That prevents mixing silence and enable
|
||||
// us to stop DMA playback which takes unnecessary cycles.
|
||||
if (!ConfMan.hasKey("music_driver")) {
|
||||
ConfMan.set("music_driver", "stmidi");
|
||||
}
|
||||
if (!ConfMan.hasKey("gm_device")) {
|
||||
ConfMan.set("gm_device", "auto");
|
||||
}
|
||||
if (!ConfMan.hasKey("mt32_device")) {
|
||||
ConfMan.set("mt32_device", "auto");
|
||||
}
|
||||
#endif
|
||||
|
||||
_mixerManager = new AtariMixerManager();
|
||||
// Setup and start mixer
|
||||
_mixerManager->init();
|
||||
|
||||
_startTime = counter_200hz;
|
||||
|
||||
BaseBackend::initBackend();
|
||||
}
|
||||
|
||||
void OSystem_Atari::engineInit() {
|
||||
//atari_debug("engineInit");
|
||||
|
||||
g_gameEngineActive = true;
|
||||
}
|
||||
|
||||
void OSystem_Atari::engineDone() {
|
||||
//atari_debug("engineDone");
|
||||
|
||||
g_gameEngineActive = false;
|
||||
}
|
||||
|
||||
Common::MutexInternal *OSystem_Atari::createMutex() {
|
||||
return new NullMutexInternal();
|
||||
}
|
||||
|
||||
uint32 OSystem_Atari::getMillis(bool skipRecord) {
|
||||
// CLOCKS_PER_SEC is 200, so no need to use floats
|
||||
return 1000 * (counter_200hz - _startTime) / CLOCKS_PER_SEC;
|
||||
}
|
||||
|
||||
void OSystem_Atari::delayMillis(uint msecs) {
|
||||
const uint32 threshold = getMillis() + msecs;
|
||||
while (getMillis() < threshold) {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Atari::getTimeAndDate(TimeDate &td, bool skipRecord) const {
|
||||
//atari_debug("getTimeAndDate");
|
||||
time_t curTime = time(0);
|
||||
struct tm t = *localtime(&curTime);
|
||||
td.tm_sec = t.tm_sec;
|
||||
td.tm_min = t.tm_min;
|
||||
td.tm_hour = t.tm_hour;
|
||||
td.tm_mday = t.tm_mday;
|
||||
td.tm_mon = t.tm_mon;
|
||||
td.tm_year = t.tm_year;
|
||||
td.tm_wday = t.tm_wday;
|
||||
}
|
||||
|
||||
Common::KeymapArray OSystem_Atari::getGlobalKeymaps() {
|
||||
Common::KeymapArray globalMaps = BaseBackend::getGlobalKeymaps();
|
||||
|
||||
Common::Keymap *keymap = ((AtariGraphicsManager*)_graphicsManager)->getKeymap();
|
||||
globalMaps.push_back(keymap);
|
||||
|
||||
return globalMaps;
|
||||
}
|
||||
|
||||
Common::HardwareInputSet *OSystem_Atari::getHardwareInputSet() {
|
||||
Common::CompositeHardwareInputSet *inputSet = new Common::CompositeHardwareInputSet();
|
||||
inputSet->addHardwareInputSet(new Common::MouseHardwareInputSet(Common::defaultMouseButtons));
|
||||
inputSet->addHardwareInputSet(new Common::KeyboardHardwareInputSet(Common::defaultKeys, Common::defaultModifiers));
|
||||
|
||||
return inputSet;
|
||||
}
|
||||
|
||||
void OSystem_Atari::quit() {
|
||||
atari_debug("OSystem_Atari::quit()");
|
||||
|
||||
if (!s_dtor_already_called)
|
||||
destroy();
|
||||
}
|
||||
|
||||
void OSystem_Atari::fatalError() {
|
||||
atari_debug("OSystem_Atari::fatalError()");
|
||||
|
||||
quit();
|
||||
|
||||
// let exit_restore() and critical_restore() handle the recovery
|
||||
exit(1);
|
||||
}
|
||||
|
||||
void OSystem_Atari::logMessage(LogMessageType::Type type, const char *message) {
|
||||
extern long nf_stderr_id;
|
||||
|
||||
static char str[1024+1];
|
||||
snprintf(str, sizeof(str), "[%08d] %s", getMillis(), message);
|
||||
|
||||
if (nf_stderr_id) {
|
||||
nf_print(str);
|
||||
} else {
|
||||
FILE *output = 0;
|
||||
|
||||
if (type == LogMessageType::kInfo || type == LogMessageType::kDebug)
|
||||
output = stdout;
|
||||
else
|
||||
output = stderr;
|
||||
|
||||
fputs(str, output);
|
||||
fflush(output);
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Atari::addSysArchivesToSearchSet(Common::SearchSet &s, int priority) {
|
||||
{
|
||||
Common::FSDirectory currentDirectory{ Common::Path(getFilesystemFactory()->makeCurrentDirectoryFileNode()->getPath()) };
|
||||
Common::FSDirectory *dataDirectory = currentDirectory.getSubDirectory("data");
|
||||
if (dataDirectory) {
|
||||
Common::FSNode dataNode = dataDirectory->getFSNode();
|
||||
if (dataNode.exists() && dataNode.isDirectory() && dataNode.isReadable()) {
|
||||
s.addDirectory(dataNode.getPath(), priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef DATA_PATH
|
||||
{
|
||||
// Add the global DATA_PATH to the directory search list
|
||||
// See also OSystem_SDL::addSysArchivesToSearchSet()
|
||||
Common::FSNode dataNode(DATA_PATH);
|
||||
if (dataNode.exists() && dataNode.isDirectory() && dataNode.isReadable()) {
|
||||
s.add(DATA_PATH, new Common::FSDirectory(dataNode, 4), priority);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Common::Path OSystem_Atari::getDefaultConfigFileName() {
|
||||
const Common::Path baseConfigName = OSystem::getDefaultConfigFileName();
|
||||
|
||||
const char *envVar = getenv("HOME");
|
||||
if (envVar && *envVar) {
|
||||
Common::Path configFile(envVar);
|
||||
configFile.joinInPlace(baseConfigName);
|
||||
|
||||
if (configFile.toString(Common::Path::kNativeSeparator).size() < MAXPATHLEN)
|
||||
return configFile;
|
||||
}
|
||||
|
||||
return baseConfigName;
|
||||
}
|
||||
|
||||
void OSystem_Atari::update() {
|
||||
// avoid a recursion loop if a timer callback decides to call OSystem::delayMillis()
|
||||
static bool inTimer = false;
|
||||
|
||||
if (!inTimer) {
|
||||
inTimer = true;
|
||||
((DefaultTimerManager *)_timerManager)->checkTimers();
|
||||
inTimer = false;
|
||||
} else {
|
||||
const Common::ConfigManager::Domain *activeDomain = ConfMan.getActiveDomain();
|
||||
assert(activeDomain);
|
||||
|
||||
warning("%s/%s calls update() from timer",
|
||||
activeDomain->getValOrDefault("engineid").c_str(),
|
||||
activeDomain->getValOrDefault("gameid").c_str());
|
||||
}
|
||||
|
||||
((AtariMixerManager *)_mixerManager)->update();
|
||||
}
|
||||
|
||||
OSystem *OSystem_Atari_create() {
|
||||
return new OSystem_Atari();
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
g_system = OSystem_Atari_create();
|
||||
assert(g_system);
|
||||
|
||||
// Invoke the actual ScummVM main entry point:
|
||||
int res = scummvm_main(argc, argv);
|
||||
g_system->destroy();
|
||||
|
||||
return res;
|
||||
}
|
||||
65
backends/platform/atari/osystem_atari.h
Normal file
65
backends/platform/atari/osystem_atari.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_ATARI_H
|
||||
#define PLATFORM_ATARI_H
|
||||
|
||||
#include "backends/modular-backend.h"
|
||||
|
||||
class OSystem_Atari final : public ModularMixerBackend, public ModularGraphicsBackend {
|
||||
public:
|
||||
OSystem_Atari();
|
||||
virtual ~OSystem_Atari();
|
||||
|
||||
void initBackend() override;
|
||||
|
||||
void engineInit() override;
|
||||
void engineDone() override;
|
||||
|
||||
Common::MutexInternal *createMutex() override;
|
||||
uint32 getMillis(bool skipRecord = false) override;
|
||||
void delayMillis(uint msecs) override;
|
||||
void getTimeAndDate(TimeDate &td, bool skipRecord = false) const override;
|
||||
|
||||
Common::KeymapArray getGlobalKeymaps() override;
|
||||
Common::HardwareInputSet *getHardwareInputSet() override;
|
||||
|
||||
void quit() override;
|
||||
void fatalError() override;
|
||||
|
||||
void logMessage(LogMessageType::Type type, const char *message) override;
|
||||
|
||||
void addSysArchivesToSearchSet(Common::SearchSet &s, int priority) override;
|
||||
Common::Path getDefaultConfigFileName() override;
|
||||
|
||||
void update();
|
||||
|
||||
private:
|
||||
long _startTime;
|
||||
|
||||
bool _timerInitialized = false;
|
||||
|
||||
int16 _vdi_handle;
|
||||
int _vdi_width;
|
||||
int _vdi_height;
|
||||
};
|
||||
|
||||
#endif
|
||||
69
backends/platform/atari/patches/print_rate.patch
Normal file
69
backends/platform/atari/patches/print_rate.patch
Normal file
@@ -0,0 +1,69 @@
|
||||
commit 21a79a50b54df8b3c377f275e1d7bfa76ee50899
|
||||
Author: Miro Kropacek <miro.kropacek@gmail.com>
|
||||
Date: Tue Oct 31 23:48:25 2023 +0100
|
||||
|
||||
Introduce "print_rate"
|
||||
|
||||
This will never go to upstream.
|
||||
|
||||
diff --git a/audio/rate.cpp b/audio/rate.cpp
|
||||
index 7f0c50f9250..83dde41462f 100644
|
||||
--- a/audio/rate.cpp
|
||||
+++ b/audio/rate.cpp
|
||||
@@ -30,6 +30,8 @@
|
||||
#include "audio/audiostream.h"
|
||||
#include "audio/rate.h"
|
||||
#include "audio/mixer.h"
|
||||
+#include "backends/platform/atari/atari-debug.h"
|
||||
+#include "common/config-manager.h"
|
||||
#include "common/util.h"
|
||||
|
||||
namespace Audio {
|
||||
@@ -80,6 +82,20 @@ private:
|
||||
int simpleConvert(AudioStream &input, st_sample_t *outBuffer, st_size_t numSamples, st_volume_t vol_l, st_volume_t vol_r);
|
||||
int interpolateConvert(AudioStream &input, st_sample_t *outBuffer, st_size_t numSamples, st_volume_t vol_l, st_volume_t vol_r);
|
||||
|
||||
+ void printConvertType(const Common::String &name) {
|
||||
+ const Common::ConfigManager::Domain *activeDomain = ConfMan.getActiveDomain();
|
||||
+ if (activeDomain && ConfMan.getBool("print_rate")) {
|
||||
+ static st_rate_t previousInRate, previousOutRate;
|
||||
+ if (previousInRate != _inRate || previousOutRate != _outRate) {
|
||||
+ previousInRate = _inRate;
|
||||
+ previousOutRate = _outRate;
|
||||
+ atari_debug("RateConverter_Impl::%s[%s]: inRate %d Hz (%s) => outRate %d Hz (%s)",
|
||||
+ name.c_str(), activeDomain->getValOrDefault("gameid").c_str(),
|
||||
+ _inRate, inStereo ? "stereo" : "mono", _outRate, outStereo ? "stereo" : "mono");
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
public:
|
||||
RateConverter_Impl(st_rate_t inputRate, st_rate_t outputRate);
|
||||
virtual ~RateConverter_Impl() {}
|
||||
@@ -99,6 +115,8 @@ template<bool inStereo, bool outStereo, bool reverseStereo>
|
||||
int RateConverter_Impl<inStereo, outStereo, reverseStereo>::copyConvert(AudioStream &input, st_sample_t *outBuffer, st_size_t numSamples, st_volume_t volL, st_volume_t volR) {
|
||||
st_sample_t *outStart, *outEnd;
|
||||
|
||||
+ printConvertType("copyConvert");
|
||||
+
|
||||
outStart = outBuffer;
|
||||
outEnd = outBuffer + numSamples * (outStereo ? 2 : 1);
|
||||
|
||||
@@ -148,6 +166,8 @@ int RateConverter_Impl<inStereo, outStereo, reverseStereo>::simpleConvert(AudioS
|
||||
|
||||
st_sample_t *outStart, *outEnd;
|
||||
|
||||
+ printConvertType("simpleConvert");
|
||||
+
|
||||
outStart = outBuffer;
|
||||
outEnd = outBuffer + numSamples * (outStereo ? 2 : 1);
|
||||
|
||||
@@ -209,6 +229,8 @@ int RateConverter_Impl<inStereo, outStereo, reverseStereo>::interpolateConvert(A
|
||||
outStart = outBuffer;
|
||||
outEnd = outBuffer + numSamples * (outStereo ? 2 : 1);
|
||||
|
||||
+ printConvertType("interpolateConvert");
|
||||
+
|
||||
while (outBuffer < outEnd) {
|
||||
// Read enough input samples so that _outPosFrac < 0
|
||||
while ((frac_t)FRAC_ONE_LOW <= _outPosFrac) {
|
||||
268
backends/platform/atari/patches/tooltips.patch
Normal file
268
backends/platform/atari/patches/tooltips.patch
Normal file
@@ -0,0 +1,268 @@
|
||||
commit bc5168b5929bb7ce41c04aab54d71b88db432666
|
||||
Author: Miro Kropacek <miro.kropacek@gmail.com>
|
||||
Date: Sun Jun 4 14:50:49 2023 +0200
|
||||
|
||||
Don't merge: tooltips
|
||||
|
||||
diff --git a/gui/ThemeEngine.cpp b/gui/ThemeEngine.cpp
|
||||
index 44d6c9487ed..cce47f9efdd 100644
|
||||
--- a/gui/ThemeEngine.cpp
|
||||
+++ b/gui/ThemeEngine.cpp
|
||||
@@ -916,7 +916,7 @@ bool ThemeEngine::loadThemeXML(const Common::String &themeId) {
|
||||
/**********************************************************
|
||||
* Draw Date descriptors drawing functions
|
||||
*********************************************************/
|
||||
-void ThemeEngine::drawDD(DrawData type, const Common::Rect &r, uint32 dynamic, bool forceRestore) {
|
||||
+void ThemeEngine::drawDD(DrawData type, const Common::Rect &r, uint32 dynamic, bool forceRestore, Common::Rect *bgRect) {
|
||||
WidgetDrawData *drawData = _widgets[type];
|
||||
|
||||
if (!drawData)
|
||||
@@ -942,6 +942,9 @@ void ThemeEngine::drawDD(DrawData type, const Common::Rect &r, uint32 dynamic, b
|
||||
// Cull the elements not in the clip rect
|
||||
if (extendedRect.isEmpty()) {
|
||||
return;
|
||||
+ } else if (bgRect) {
|
||||
+ *bgRect = extendedRect;
|
||||
+ return;
|
||||
}
|
||||
|
||||
if (forceRestore || drawData->_layer == kDrawLayerBackground)
|
||||
@@ -1177,7 +1180,7 @@ void ThemeEngine::drawScrollbar(const Common::Rect &r, int sliderY, int sliderHe
|
||||
drawDD(scrollState == kScrollbarStateSlider ? kDDScrollbarHandleHover : kDDScrollbarHandleIdle, r2);
|
||||
}
|
||||
|
||||
-void ThemeEngine::drawDialogBackground(const Common::Rect &r, DialogBackground bgtype) {
|
||||
+void ThemeEngine::drawDialogBackground(const Common::Rect &r, DialogBackground bgtype, Common::Rect *bgRect) {
|
||||
if (!ready())
|
||||
return;
|
||||
|
||||
@@ -1195,7 +1198,7 @@ void ThemeEngine::drawDialogBackground(const Common::Rect &r, DialogBackground b
|
||||
break;
|
||||
|
||||
case kDialogBackgroundTooltip:
|
||||
- drawDD(kDDTooltipBackground, r);
|
||||
+ drawDD(kDDTooltipBackground, r, 0, false, bgRect);
|
||||
break;
|
||||
|
||||
case kDialogBackgroundDefault:
|
||||
diff --git a/gui/ThemeEngine.h b/gui/ThemeEngine.h
|
||||
index 940298eff25..a371c13dcd5 100644
|
||||
--- a/gui/ThemeEngine.h
|
||||
+++ b/gui/ThemeEngine.h
|
||||
@@ -53,6 +53,7 @@ class Dialog;
|
||||
class GuiObject;
|
||||
class ThemeEval;
|
||||
class ThemeParser;
|
||||
+class Tooltip;
|
||||
|
||||
/**
|
||||
* DrawData sets enumeration.
|
||||
@@ -212,6 +213,7 @@ protected:
|
||||
|
||||
friend class GUI::Dialog;
|
||||
friend class GUI::GuiObject;
|
||||
+ friend class GUI::Tooltip;
|
||||
|
||||
public:
|
||||
/// Vertical alignment of the text.
|
||||
@@ -492,7 +494,7 @@ public:
|
||||
|
||||
void drawLineSeparator(const Common::Rect &r);
|
||||
|
||||
- void drawDialogBackground(const Common::Rect &r, DialogBackground type);
|
||||
+ void drawDialogBackground(const Common::Rect &r, DialogBackground type, Common::Rect *bgRect = nullptr);
|
||||
|
||||
void drawText(const Common::Rect &r, const Common::U32String &str, WidgetStateInfo state = kStateEnabled,
|
||||
Graphics::TextAlign align = Graphics::kTextAlignCenter,
|
||||
@@ -712,7 +714,7 @@ protected:
|
||||
*
|
||||
* These functions are called from all the Widget drawing methods.
|
||||
*/
|
||||
- void drawDD(DrawData type, const Common::Rect &r, uint32 dynamic = 0, bool forceRestore = false);
|
||||
+ void drawDD(DrawData type, const Common::Rect &r, uint32 dynamic = 0, bool forceRestore = false, Common::Rect *bgRect = nullptr);
|
||||
void drawDDText(TextData type, TextColor color, const Common::Rect &r, const Common::U32String &text, bool restoreBg,
|
||||
bool elipsis, Graphics::TextAlign alignH = Graphics::kTextAlignLeft,
|
||||
TextAlignVertical alignV = kTextAlignVTop, int deltax = 0,
|
||||
diff --git a/gui/Tooltip.cpp b/gui/Tooltip.cpp
|
||||
index 79f0f9db91a..b08cc9939bc 100644
|
||||
--- a/gui/Tooltip.cpp
|
||||
+++ b/gui/Tooltip.cpp
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "gui/widget.h"
|
||||
#include "gui/dialog.h"
|
||||
#include "gui/gui-manager.h"
|
||||
+#include "graphics/VectorRenderer.h"
|
||||
|
||||
#include "gui/Tooltip.h"
|
||||
#include "gui/ThemeEval.h"
|
||||
@@ -31,7 +32,7 @@ namespace GUI {
|
||||
|
||||
|
||||
Tooltip::Tooltip() :
|
||||
- Dialog(-1, -1, -1, -1), _maxWidth(-1), _parent(nullptr), _xdelta(0), _ydelta(0), _xpadding(0), _ypadding(0) {
|
||||
+ Dialog(-1, -1, -1, -1), _maxWidth(-1), _parent(nullptr), _xdelta(0), _ydelta(0), _xpadding(0), _ypadding(0), _firstDraw(true) {
|
||||
|
||||
_backgroundType = GUI::ThemeEngine::kDialogBackgroundTooltip;
|
||||
}
|
||||
@@ -71,7 +72,40 @@ void Tooltip::drawDialog(DrawLayer layerToDraw) {
|
||||
int num = 0;
|
||||
int h = g_gui.theme()->getFontHeight(ThemeEngine::kFontStyleTooltip) + 2;
|
||||
|
||||
- Dialog::drawDialog(layerToDraw);
|
||||
+ // Dialog::drawDialog(layerToDraw)
|
||||
+ if (!isVisible())
|
||||
+ return;
|
||||
+
|
||||
+ g_gui.theme()->disableClipRect();
|
||||
+ g_gui.theme()->_layerToDraw = layerToDraw;
|
||||
+
|
||||
+ if (_firstDraw) {
|
||||
+ ThemeEngine *theme = g_gui.theme();
|
||||
+
|
||||
+ // store backgrounds from Backbuffer and Screen
|
||||
+ theme->drawDialogBackground(Common::Rect(_x, _y, _x + _w, _y + _h), _backgroundType, &_bgRect);
|
||||
+
|
||||
+ theme->drawToBackbuffer();
|
||||
+ _bgBackbufferSurf.create(_bgRect.width(), _bgRect.height(), theme->renderer()->getActiveSurface()->format);
|
||||
+ _bgBackbufferSurf.copyRectToSurface(*theme->renderer()->getActiveSurface(), 0, 0, _bgRect);
|
||||
+
|
||||
+ theme->drawToScreen();
|
||||
+ _bgScreenSurf.create(_bgRect.width(), _bgRect.height(), theme->renderer()->getActiveSurface()->format);
|
||||
+ _bgScreenSurf.copyRectToSurface(*theme->renderer()->getActiveSurface(), 0, 0, _bgRect);
|
||||
+
|
||||
+ theme->drawToBackbuffer();
|
||||
+ _firstDraw = false;
|
||||
+ }
|
||||
+
|
||||
+ g_gui.theme()->drawDialogBackground(Common::Rect(_x, _y, _x + _w, _y + _h), _backgroundType);
|
||||
+
|
||||
+ markWidgetsAsDirty();
|
||||
+
|
||||
+#ifdef LAYOUT_DEBUG_DIALOG
|
||||
+ return;
|
||||
+#endif
|
||||
+ drawWidgets();
|
||||
+ // end of Dialog::drawDialog(layerToDraw)
|
||||
|
||||
int16 textX = _x + 1 + _xpadding;
|
||||
if (g_gui.useRTL()) {
|
||||
@@ -98,4 +132,32 @@ void Tooltip::drawDialog(DrawLayer layerToDraw) {
|
||||
}
|
||||
}
|
||||
|
||||
+void Tooltip::open() {
|
||||
+ Dialog::open();
|
||||
+ g_gui._redrawStatus = GuiManager::kRedrawOpenTooltip;
|
||||
+}
|
||||
+
|
||||
+void Tooltip::close() {
|
||||
+ Dialog::close();
|
||||
+ g_gui._redrawStatus = GuiManager::kRedrawDisabled;
|
||||
+
|
||||
+ if (!_bgRect.isEmpty()) {
|
||||
+ ThemeEngine *theme = g_gui.theme();
|
||||
+
|
||||
+ theme->drawToBackbuffer();
|
||||
+ theme->renderer()->getActiveSurface()->copyRectToSurface(
|
||||
+ _bgBackbufferSurf, _bgRect.left, _bgRect.top, Common::Rect(_bgRect.width(), _bgRect.height()));
|
||||
+
|
||||
+ theme->drawToScreen();
|
||||
+ theme->renderer()->getActiveSurface()->copyRectToSurface(
|
||||
+ _bgScreenSurf, _bgRect.left, _bgRect.top, Common::Rect(_bgRect.width(), _bgRect.height()));
|
||||
+
|
||||
+ theme->addDirtyRect(_bgRect);
|
||||
+
|
||||
+ _bgRect = Common::Rect();
|
||||
+ _bgBackbufferSurf.free();
|
||||
+ _bgScreenSurf.free();
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
}
|
||||
diff --git a/gui/Tooltip.h b/gui/Tooltip.h
|
||||
index 2f188764ff3..40caa7a1be4 100644
|
||||
--- a/gui/Tooltip.h
|
||||
+++ b/gui/Tooltip.h
|
||||
@@ -23,7 +23,9 @@
|
||||
#define GUI_TOOLTIP_H
|
||||
|
||||
#include "common/keyboard.h"
|
||||
+#include "common/rect.h"
|
||||
#include "common/str-array.h"
|
||||
+#include "graphics/surface.h"
|
||||
#include "gui/dialog.h"
|
||||
|
||||
namespace GUI {
|
||||
@@ -43,6 +45,9 @@ public:
|
||||
|
||||
void receivedFocus(int x = -1, int y = -1) override {}
|
||||
protected:
|
||||
+ void open() override;
|
||||
+ void close() override;
|
||||
+
|
||||
void handleMouseDown(int x, int y, int button, int clickCount) override {
|
||||
close();
|
||||
_parent->handleMouseDown(x + (getAbsX() - _parent->getAbsX()), y + (getAbsY() - _parent->getAbsY()), button, clickCount);
|
||||
@@ -72,6 +77,11 @@ protected:
|
||||
int _xpadding, _ypadding;
|
||||
|
||||
Common::U32StringArray _wrappedLines;
|
||||
+
|
||||
+ bool _firstDraw;
|
||||
+ Common::Rect _bgRect;
|
||||
+ Graphics::Surface _bgBackbufferSurf;
|
||||
+ Graphics::Surface _bgScreenSurf;
|
||||
};
|
||||
|
||||
} // End of namespace GUI
|
||||
diff --git a/gui/gui-manager.cpp b/gui/gui-manager.cpp
|
||||
index 02de69aa7c9..da57fdb2973 100644
|
||||
--- a/gui/gui-manager.cpp
|
||||
+++ b/gui/gui-manager.cpp
|
||||
@@ -453,6 +453,9 @@ void GuiManager::redrawInternal() {
|
||||
_theme->applyScreenShading(shading);
|
||||
}
|
||||
|
||||
+ // fall through
|
||||
+
|
||||
+ case kRedrawOpenTooltip:
|
||||
// Finally, draw the top dialog background
|
||||
_dialogStack.top()->drawDialog(kDrawLayerBackground);
|
||||
|
||||
@@ -753,8 +756,6 @@ void GuiManager::closeTopDialog() {
|
||||
|
||||
if (_redrawStatus != kRedrawFull)
|
||||
_redrawStatus = kRedrawCloseDialog;
|
||||
-
|
||||
- redraw();
|
||||
}
|
||||
|
||||
void GuiManager::setupCursor() {
|
||||
diff --git a/gui/gui-manager.h b/gui/gui-manager.h
|
||||
index 0718f631c8b..e8b646ec46c 100644
|
||||
--- a/gui/gui-manager.h
|
||||
+++ b/gui/gui-manager.h
|
||||
@@ -62,6 +62,7 @@ enum {
|
||||
|
||||
class Dialog;
|
||||
class ThemeEval;
|
||||
+class Tooltip;
|
||||
class GuiObject;
|
||||
|
||||
#define g_gui (GUI::GuiManager::instance())
|
||||
@@ -82,6 +83,7 @@ typedef Common::FixedStack<Dialog *> DialogStack;
|
||||
*/
|
||||
class GuiManager : public Common::Singleton<GuiManager>, public CommandSender {
|
||||
friend class Dialog;
|
||||
+ friend class Tooltip;
|
||||
friend class Common::Singleton<SingletonBaseType>;
|
||||
GuiManager();
|
||||
~GuiManager() override;
|
||||
@@ -159,6 +161,7 @@ protected:
|
||||
enum RedrawStatus {
|
||||
kRedrawDisabled = 0,
|
||||
kRedrawOpenDialog,
|
||||
+ kRedrawOpenTooltip,
|
||||
kRedrawCloseDialog,
|
||||
kRedrawTopDialog,
|
||||
kRedrawFull
|
||||
535
backends/platform/atari/readme.txt
Normal file
535
backends/platform/atari/readme.txt
Normal file
@@ -0,0 +1,535 @@
|
||||
ScummVM 2026.1.1git
|
||||
=============
|
||||
|
||||
This is a port of ScummVM (https://www.scummvm.org), a program which allows you
|
||||
to run certain classic graphical adventure and role-playing games, provided you
|
||||
already have their data files.
|
||||
|
||||
You can find a full list with details on which games are supported and how well
|
||||
on the compatibility page: https://www.scummvm.org/compatibility.
|
||||
|
||||
|
||||
New port?
|
||||
---------
|
||||
|
||||
Keith Scroggins (aka KeithS) has been providing ScummVM (and many other) builds
|
||||
for the Atari community for an unbelievable 17 years. He put quite a lot of time
|
||||
into testing each release, updating ScummVM dependencies to their latest
|
||||
versions and even regularly upgrading his compiler toolchain to get the best
|
||||
possible performance.
|
||||
|
||||
However, ScummVM (and SDL to some extent) is a beast; it requires quite a lot
|
||||
of attention regarding where the cycles go, e.g. an additional screen refresh
|
||||
can easily drop the frame rate by half.
|
||||
|
||||
After I had seen how snappy NovaCoder's ScummVM on the Amiga is (who coded his
|
||||
own backend), I decided to check whether there was a way to get a better
|
||||
performing port on our platform. And there is!
|
||||
|
||||
I have managed to create a "native" Atari port talking directly to the
|
||||
hardware, skipping the SDL layer altogether, which is in some cases usable even
|
||||
on a plain 32 MHz Atari TT.
|
||||
|
||||
|
||||
Differences between the versions
|
||||
--------------------------------
|
||||
|
||||
After talking to Keith we have decided to provide three flavours of ScummVM.
|
||||
Please refer to https://docs.scummvm.org/en/v2026.1.1git/other_platforms/atari.html
|
||||
for more details (TBD).
|
||||
|
||||
Atari Full package
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Minimum hardware requirements: Atari Falcon with 4 + 64 MB RAM, 68040 CPU.
|
||||
|
||||
- Because there is limited horsepower available on our platform, features like
|
||||
16bpp graphics, software synthesisers, scalers, real-time software
|
||||
MP3/OGG/FLAC playback etc., are omitted. This saves CPU cycles, memory and
|
||||
disk space.
|
||||
|
||||
- Tailored video settings for the best possible performance and visual
|
||||
experience (Falcon RGB overscan, chunky modes with the SuperVidel, TT 640x480
|
||||
for the overlay, ...).
|
||||
|
||||
- Direct rendering and single/triple buffering support.
|
||||
|
||||
- Blitting routines optimised for 68040 / 68060 CPU.
|
||||
|
||||
- Custom (and optimal) surface drawing (especially for the cursor).
|
||||
|
||||
- Custom (hardware based) aspect ratio correction (!)
|
||||
|
||||
- Full support for the SuperVidel, including the SuperBlitter (!)
|
||||
|
||||
- External DSP clock support for playing back samples at PC frequencies
|
||||
(Falcon only). Dual clock input frequency supported as well (Steinberg's
|
||||
FDI).
|
||||
|
||||
- Support for PC keys (page up, page down, pause, F11/F12, ...) and mouse wheel
|
||||
(Eiffel/Aranym only).
|
||||
|
||||
- Native MIDI output (if present).
|
||||
|
||||
- Runs also in Hatari and ARAnyM but in case of ARAnyM don't forget to disable
|
||||
fVDI to enable Videl output.
|
||||
|
||||
- FreeMiNT + memory protection friendly.
|
||||
|
||||
Atari Lite package
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Minimum hardware requirements: Atari TT / Falcon with 4 + 32 MB RAM.
|
||||
|
||||
As a further optimisation step, a 030-only version of ScummVM is provided, aimed
|
||||
at less powerful TT and Falcon machines with the 68030 CPU. It further restricts
|
||||
features but also improves performance and reduces executable size.
|
||||
|
||||
- Compiled with -m68030 => 68030/68882-specific optimisations enabled.
|
||||
|
||||
- Disabled 040+/SuperVidel code => faster code path for blitting.
|
||||
|
||||
- Doesn't support hires (640x480) games => smaller executable size.
|
||||
|
||||
- Overlay is rendered in 16 colours => faster redraw.
|
||||
|
||||
- Overlay during gameplay has no game background => even faster redraw.
|
||||
|
||||
- Overlay doesn't support alternative themes => faster loading time.
|
||||
|
||||
- "STMIDI" driver is automatically enabled (i.e. MIDI emulation is never used
|
||||
but still allows playing speech/sfx samples and/or CD audio).
|
||||
|
||||
FireBee package
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Hardware requirements: MCF5475 Evaluation Board or FireBee.
|
||||
|
||||
This one is still built and provided by Keith.
|
||||
|
||||
- Based on the most recent SDL.
|
||||
|
||||
- Contains various optimisations discovered / implemented from the Atari
|
||||
backend.
|
||||
|
||||
- Works in GEM (in theory also in XBIOS but that seems to be still broken on
|
||||
FireBee).
|
||||
|
||||
- Support for all engines is included in the build; this does not mean all
|
||||
games work. For instance, support for OGG and MP3 audio is included but the
|
||||
system can not handle playback of compressed audio; there is not enough
|
||||
processing power for both gameplay and sound at the same time.
|
||||
|
||||
Scalers can be utilized to make the GEM window larger on the Firebee.
|
||||
Performance is best when not using AdLib sound; using STMIDI would be
|
||||
optimal, but untested as of yet (I have been unable to get MIDI to work on my
|
||||
FireBee).
|
||||
|
||||
- Removed features: FLAC, MPEG2, Network/Cloud Support, HQ Scalers.
|
||||
|
||||
|
||||
The rest of this document describes things specific to the Full / Lite package.
|
||||
For the FireBee (SDL) build please refer to generic ScummVM documentation.
|
||||
|
||||
|
||||
Platform-specific features outside the GUI
|
||||
------------------------------------------
|
||||
|
||||
Keyboard shortcut "CONTROL+ALT+a": immediate aspect ratio correction on/off
|
||||
toggle.
|
||||
|
||||
"output_rate" in scummvm.ini: sample rate for mixing. Allowed values depend on
|
||||
the hardware connected:
|
||||
- TT030: 50066, 25033, 12517, 6258 Hz
|
||||
- Falcon030: as TT030 (except 6258) plus 49170, 32780, 24585, 19668, 16390,
|
||||
12292, 9834, 8195 Hz
|
||||
- External 22.5792 MHz DSP clock: as Falcon030 plus 44100, 29400, 22050,
|
||||
17640, 14700, 11025, 8820, 7350 Hz
|
||||
- External 24.576 MHz DSP clock: as Falcon030 plus 48000, 32000, 24000,
|
||||
19200, 16000, 12000, 9600, 8000 Hz
|
||||
The lower the value, the faster the mixing but also worse quality. Default is
|
||||
24585/25033 Hz (16-bit, stereo). Please note you don't have to enter the value
|
||||
exactly, it will be rounded to the nearest sane value.
|
||||
|
||||
"output_channels" in scummvm.ini: mono (1) or stereo (2) mixing. Please note
|
||||
that Falcon doesn't allow mixing in 16-bit mono, so this will have no effect on
|
||||
this machine.
|
||||
|
||||
"print_rate" in scummvm.ini: used for optimising sample playback (where
|
||||
available). It prints input and output sample format as well as the name of the
|
||||
converter used. See below for details.
|
||||
|
||||
"audio_buffer_size" in scummvm.ini: number of samples to preload. Default is
|
||||
2048 which equals to about 83ms of audio lag and seems to be about right for
|
||||
most games on my CT60@66 MHz.
|
||||
|
||||
If you want to play with "audio_buffer_size", the rule of thumb is: (lag in ms)
|
||||
= (audio_buffer_size / output_rate) * 1000. But it's totally OK just to double
|
||||
the samples value to get rid of stuttering in a heavier game.
|
||||
|
||||
|
||||
Graphics modes
|
||||
--------------
|
||||
|
||||
This topic is more complex than it looks. ScummVM renders game graphics using
|
||||
rectangles and this port offers the following options to render them:
|
||||
|
||||
Direct rendering
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is direct writing of the pixels into the screen buffer. On SuperVidel it is
|
||||
done natively, on Videl a chunky to planar conversion takes place beforehand.
|
||||
|
||||
Pros:
|
||||
|
||||
- On SuperVidel this offers fastest possible rendering (especially in 640x480
|
||||
with a lot of small rectangle updates where the buffer copying drags
|
||||
performance down).
|
||||
|
||||
- On Videl this _may_ offer fastest possible rendering if the rendering
|
||||
pipeline isn't flooded with too many small rectangles (C2P setup isn't for
|
||||
free). However with fullscreen intro sequences this is a no-brainer.
|
||||
|
||||
Cons:
|
||||
|
||||
- Screen tearing in most cases.
|
||||
|
||||
- On Videl, this may not work properly if a game engine uses its own buffers
|
||||
instead of surfaces (which are aligned on a 16pix boundary). Another source
|
||||
of danger is if an engine draws directly to the screen surface. Fortunately,
|
||||
each game can have its own graphics mode set separately so for games which do
|
||||
not work properly one can still leave the default graphics mode set.
|
||||
|
||||
- On Videl, overlay background isn't rendered (the GUI code can't work with
|
||||
bitplanes).
|
||||
|
||||
SuperBlitter used: sometimes (when ScummVM allocates surface via its create()
|
||||
function; custom/small buffers originating in the engine code are still copied
|
||||
using the CPU).
|
||||
|
||||
Single buffering
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is very similar to the previous mode with the difference that the engine
|
||||
uses an intermediate buffer for storing the rectangles but yet it remembers
|
||||
which ones they were.
|
||||
|
||||
Pros:
|
||||
|
||||
- Second fastest possible rendering.
|
||||
|
||||
Cons:
|
||||
|
||||
- Screen tearing in most cases.
|
||||
|
||||
- If there are too many smaller rectangles, it can be less efficient than
|
||||
updating the whole buffer at once.
|
||||
|
||||
SuperBlitter used: yes, for rectangle blitting to screen and cursor
|
||||
restoration. Sometimes also for generic copying between buffers (see above).
|
||||
|
||||
Triple buffering
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is the "true" triple buffering as described in
|
||||
https://en.wikipedia.org/wiki/Multiple_buffering#Triple_buffering and not "swap
|
||||
chain" as described in https://en.wikipedia.org/wiki/Swap_chain. The latter
|
||||
would be slightly slower as three buffers would need to be updated instead of
|
||||
two.
|
||||
|
||||
Pros:
|
||||
|
||||
- No screen tearing.
|
||||
|
||||
- Best compromise between performance and visual experience.
|
||||
|
||||
- Works well with both higher and lower frame rates.
|
||||
|
||||
Cons:
|
||||
|
||||
- If there are too many smaller rectangles, it can be less efficient than
|
||||
single buffering.
|
||||
|
||||
- Slightly irregular frame rate (depends solely on the game's complexity).
|
||||
|
||||
- In case of extremely fast rendering, one or more frames are dropped in favour
|
||||
of showing only the most recent one.
|
||||
|
||||
SuperBlitter used: yes, for rectangle blitting to screen and cursor
|
||||
restoration. Sometimes also for generic copying between buffers (see above).
|
||||
|
||||
Triple buffering is the default mode.
|
||||
|
||||
|
||||
SuperVidel and SuperBlitter
|
||||
---------------------------
|
||||
|
||||
As mentioned, this port uses SuperVidel and its SuperBlitter heavily. That
|
||||
means that if the SuperVidel is detected, it does the following:
|
||||
|
||||
- Uses 8bpp chunky resolutions.
|
||||
|
||||
- Patches all surface addresses by OR'ing 0xA0000000, i.e. using SV RAM instead
|
||||
of slow ST RAM (and even instead of TT RAM for allowing pure SuperBlitter
|
||||
copying).
|
||||
|
||||
- When SuperVidel FW version >= 9 is detected, the async FIFO buffer is used
|
||||
instead of the slower sync blitting (where one has to wait for every
|
||||
rectangle blit to finish), this sometimes leads to nearly zero-cost rendering
|
||||
and makes a *huge* difference for 640x480 fullscreen updates.
|
||||
|
||||
|
||||
Aspect ratio correction
|
||||
-----------------------
|
||||
|
||||
Please refer to the official documentation about its usage. Normally ScummVM
|
||||
implements this functionality using yet another fullscreen transformation of
|
||||
320x200 surface into a 320x240 one (there is even a selection of algorithms for
|
||||
this task, varying in performance and quality).
|
||||
|
||||
Naturally, this would impose a terrible performance penalty on our backend so
|
||||
some cheating has been used:
|
||||
|
||||
- On RGB, the vertical refresh rate frequency is set to 60 Hz, creating an
|
||||
illusion of non-square pixels. Works best on CRT monitors.
|
||||
|
||||
- On VGA, the vertical refresh rate frequency is set to 70 Hz, with more or
|
||||
less the same effect as on RGB. Works best on CRT monitors.
|
||||
|
||||
- On SuperVidel, video output is modified in such way that the DVI/HDMI monitor
|
||||
receives a 320x200 image and if properly set/supported, it will automatically
|
||||
stretch the image to 320x240 (this is usually a setting called "picture
|
||||
expansion" or "picture stretch" -- make sure it isn't set to something like
|
||||
"1:1" or "dot by dot").
|
||||
|
||||
Yes, it's a hack. :) Owners of a CRT monitor can achieve the same effect by the
|
||||
analog knobs -- stretch and move the 320x200 picture unless black borders are
|
||||
no longer visible. This hack provides a more elegant and per-game
|
||||
functionality.
|
||||
|
||||
|
||||
Audio mixing
|
||||
------------
|
||||
|
||||
ScummVM works internally with 16-bit samples so on the TT a simple downsampling
|
||||
to 8-bit resolution is used. However, there is still one piece missing - an
|
||||
XBIOS emulator (so ScummVM doesn't have to access hardware directly). There are
|
||||
two options (both available from https://mikrosk.github.io/xbios): STFA and
|
||||
X-SOUND, any of these will do. Or executing ScummVM in EmuTOS which contains
|
||||
the same routines as X-SOUND.
|
||||
|
||||
|
||||
Performance considerations/pitfalls
|
||||
-----------------------------------
|
||||
|
||||
It's important to understand what affects performance on our limited platform to
|
||||
avoid unpleasant gaming experiences.
|
||||
|
||||
Game engines with unexpected performance hit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A typical example from this category is Gobliiins (and its sequels), some SCI
|
||||
engine games (Gabriel Knight, Larry 2/7, ...) or the Sherlock engine (The Case
|
||||
of the Rose Tattoo). At first it looks like our machine or Atari backend is
|
||||
doing something terribly wrong but the truth is that it is the engine itself
|
||||
which is doing a lot of redraws, sometimes even before reaching the backend.
|
||||
The only solution is to profile and fix those engines.
|
||||
|
||||
Too many fullscreen updates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Somewhat related to the previous point - sometimes the engine authors didn't
|
||||
realise the impact of every update on the overall performance and instead of
|
||||
updating only the rectangles that really had changed, they ask for a fullscreen
|
||||
update. Not a problem on a >1 GHz machine but very visible on Atari! Also, this
|
||||
is (by definition) the case of animated intros, especially those in 640x480.
|
||||
|
||||
MIDI vs. AdLib vs. sampled music
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It could seem that sampled music replay must be the most demanding one but on
|
||||
the contrary! Always choose a CD version of a game (with *.wav tracks) over any
|
||||
other version. With one exception: if you have a native MIDI device able to
|
||||
replay the given game's MIDI notes (using the STMIDI plugin).
|
||||
|
||||
MIDI emulation (synthesis) can easily eat as much as 50% of all used CPU time
|
||||
(on the CT60). By default, this port uses the MAME OPL emulation (which is said
|
||||
to be fastest but also least accurate) but some engines require the DOSBOX one
|
||||
which is even more demanding. By the way, you can put "FM_high_quality=true" or
|
||||
"FM_medium_quality=true" into scummvm.ini if you want to experiment with a
|
||||
better quality synthesis, otherwise the lowest quality will be used (applies
|
||||
for MAME OPL only).
|
||||
|
||||
On TT, in most cases it makes sense to use ScummVM only if you own a native
|
||||
MIDI synthesiser (like mt32-pi: https://github.com/dwhinham/mt32-pi). MIDI
|
||||
emulation is out of the question and downsampling to 8-bit resolution takes a
|
||||
good chunk of CPU time which could be utilised elsewhere. However, there are
|
||||
games which are fine with sampled music/speech even on a plain TT (e.g. Lands
|
||||
of Lore).
|
||||
|
||||
CD music slows everything down
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some games use separate audio *and* video streams (files). Even if the CPU is
|
||||
able to handle both, the bottleneck becomes ... disk access. This is visible in
|
||||
The Curse Of Monkey Island for example -- there's audible stuttering during the
|
||||
intro sequence (and during the game as well). Increasing "audio_buffer_size"
|
||||
makes the rendering literally crawl! Why? Because disk I/O is busy with loading
|
||||
even *more* sample data so there's less time for video loading and rendering.
|
||||
Try to put "musdisk1.bun" and "musdisk2.bun" into a ramdisk (i.e. symlink to
|
||||
u:/ram in FreeMiNT), you'll be pleasantly surprised with the performance boost
|
||||
gained.
|
||||
|
||||
Mute vs. "No music"
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Currently ScummVM requires each backend to mix samples, even though they may
|
||||
contain muted output (i.e. zeroes). This is because the progression of sample
|
||||
playback tells ScummVM how much time has passed in e.g. an animation.
|
||||
|
||||
"No music" means using the null audio plugin which prevents generating any MIDI
|
||||
music (and therefore avoiding the expensive synthesis emulation) but beware, it
|
||||
doesn't affect CD (*.wav) playback at all! Same applies for speech and sfx.
|
||||
|
||||
The least amount of cycles is spent when:
|
||||
- "No music" as "Preferred device": This prevents MIDI synthesis of any kind.
|
||||
- "Subtitles" as "Text and speech": This prevents any sampled speech to be
|
||||
mixed.
|
||||
- All external audio files are deleted (typically *.wav); that way the mixer
|
||||
won't have anything to mix. However beware, this is not allowed in every game!
|
||||
|
||||
Sample rate
|
||||
~~~~~~~~~~~
|
||||
|
||||
It's important to realise the impact the sample rate has on the given game. The
|
||||
most obvious setting is its value: the bigger, the more demanding audio mixing
|
||||
becomes. However, if you inspect many games' samples, you will notice that most
|
||||
of them (esp. the ones from the 80s/90s) use simple samples like mono 11025 Hz
|
||||
(sometimes even less).
|
||||
|
||||
Obviously, setting "output_channels" to "1" is the easiest improvement
|
||||
(unfortunately only on TT). Next best thing you can do is to buy an external
|
||||
DSP clock for your Falcon: nearly all games use sample frequencies which are
|
||||
multiples of 44100 Hz: 22050, 11025, ... so with the external clock there
|
||||
won't be the need to resample them.
|
||||
|
||||
There's one caveat, though: it is important whether your replay frequency is
|
||||
equal to, a multiple of, or other than the desired one. Let's consider 44100
|
||||
and 22050 frequencies as an example (also applies to all the other
|
||||
frequencies):
|
||||
|
||||
- If you set 44100 Hz and a game requests 44100 Hz => so called "copyConvert"
|
||||
method will be used (fastest).
|
||||
- If you set 22050 Hz and a game requests 44100 Hz => so called "simpleConvert"
|
||||
method will be used (skipping every second sample, second fastest).
|
||||
- If you set 44100 Hz and a game requests 22050 Hz => so called
|
||||
"interpolateConvert" method will be used (slowest!).
|
||||
- Any other combination: "interpolateConvert" (slowest).
|
||||
|
||||
So how do you know which frequency to set as "output_rate" ? This is where
|
||||
"print_rate" comes to rescue. Enabling this option in scummvm.ini will tell you
|
||||
for each game which sample converters are being used and for what input/values.
|
||||
So you can easily verify whether the given game's demands match your setting.
|
||||
|
||||
Unfortunately, currently per-game "output_rate" / "output_channels" is not
|
||||
possible but this may change in the future.
|
||||
|
||||
Slow GUI
|
||||
~~~~~~~~
|
||||
|
||||
Themes handling is quite slow - each theme must be depacked, each one contains
|
||||
quite a few XML files to parse and quite a few images to load/convert. That's
|
||||
the reason why the built-in one is used as default, it dramatically speeds up
|
||||
loading time. To speed things up in other cases, the full version is
|
||||
distributed with repackaged theme files with compression level zero.
|
||||
|
||||
|
||||
Changes to upstream
|
||||
-------------------
|
||||
|
||||
There are a few features that have been disabled or changed and are not possible
|
||||
/ plausible to merge into upstream:
|
||||
|
||||
- the aforementioned "print_rate" feature, too invasive for other platforms
|
||||
|
||||
- This port contains an implementation of much faster tooltips in the overlay.
|
||||
However, there is a minor rendering bug which sometimes corrupts the
|
||||
background. But since its impact is huge, I left it in.
|
||||
|
||||
|
||||
Known issues
|
||||
------------
|
||||
|
||||
- When run on TT, screen contains horizontal black lines. That is due to the
|
||||
fact that TT offers only 320x480 in 256 colours. Possibly fixable by a Timer
|
||||
B interrupt.
|
||||
|
||||
- Horizontal screen shaking doesn't work on TT because TT Shifter doesn't
|
||||
support fine scrolling. However it is "emulated" via vertical shaking.
|
||||
|
||||
- Aspect ratio correction has no effect on TT because it is not possible to
|
||||
alter its vertical screen refresh frequency.
|
||||
|
||||
- The talkie version of SOMI needs to be merged from two sources:
|
||||
- The DOS version (install.bat) to obtain file "monster.sou".
|
||||
- The FLAC version (install_flac.bat) to obtain folders "cd_music_flac" and
|
||||
"se_music_flac" (these *.flac files then have to be converted to *.wav
|
||||
manually).
|
||||
- Files "monkey.000" and "monkey.001" can be taken from either version.
|
||||
- Point the extra path to the folder with *.wav files (or copy its content
|
||||
where monkey.00? files are located).
|
||||
|
||||
- Following engines have been explicitly disabled:
|
||||
- Cine (2 games)
|
||||
- Incompatible with other engines / prone to freezes.
|
||||
- https://wiki.scummvm.org/index.php?title=Cine
|
||||
- Director (many games)
|
||||
- Huge game list slows detection for other games, and would require
|
||||
(currently missing) localization support.
|
||||
- Only small subset of games actually supported by upstream, but none of
|
||||
them detected on TOS 8+3 file system.
|
||||
- https://wiki.scummvm.org/index.php?title=Director
|
||||
- Hugo (3 games)
|
||||
- Uses (lot of) overlay dialogs which are problematic for Atari backend.
|
||||
- Engine GUI (for save/load/etc) does not support 8-bit screens.
|
||||
- https://wiki.scummvm.org/index.php?title=Hugo
|
||||
- Ultima (many games)
|
||||
- The only non-hires ultima engine is ultima1; see
|
||||
https://bugs.scummvm.org/ticket/14790
|
||||
- This prevents adding the 15 MB ultima.dat to the release archive.
|
||||
- https://wiki.scummvm.org/index.php?title=Ultima
|
||||
|
||||
- When using FreeMiNT, ScummVM requires a recent kernel (>= 2021), otherwise
|
||||
keyboard handling won't work properly.
|
||||
|
||||
- When using EmuTOS, ScummVM requires a recent release (>= 1.3), otherwise
|
||||
various screen- and sound-related issues may appear.
|
||||
|
||||
Future plans
|
||||
------------
|
||||
|
||||
- DSP-based sample mixer (WAV, FLAC, MP2).
|
||||
|
||||
- Avoid loading music/speech files (and thus slowing down everything) if muted.
|
||||
|
||||
- Cached audio/video streams (i.e. don't load only "audio_buffer_size" number
|
||||
of samples but cache, say, 1 second so disk i/o won't be so stressed).
|
||||
|
||||
- Using Thorsten Otto's sharedlibs: https://tho-otto.de/sharedlibs.php for game
|
||||
engine plugins to relieve the huge binary size.
|
||||
|
||||
- True audio CD support via MetaDOS API.
|
||||
|
||||
- OPL2LPT and Retrowave support (if I manage to purchase it somewhere).
|
||||
|
||||
|
||||
Closing words
|
||||
-------------
|
||||
|
||||
Many optimisations and improvements wouldn't be possible without the help of
|
||||
Eero Tamminen, so thank you for all the help with profiling in Hatari.
|
||||
|
||||
Miro Kropacek aka MiKRO
|
||||
Brisbane / Australia
|
||||
miro.kropacek@gmail.com
|
||||
http://mikro.atari.org
|
||||
535
backends/platform/atari/readme.txt.in
Normal file
535
backends/platform/atari/readme.txt.in
Normal file
@@ -0,0 +1,535 @@
|
||||
ScummVM @VERSION@
|
||||
=============
|
||||
|
||||
This is a port of ScummVM (https://www.scummvm.org), a program which allows you
|
||||
to run certain classic graphical adventure and role-playing games, provided you
|
||||
already have their data files.
|
||||
|
||||
You can find a full list with details on which games are supported and how well
|
||||
on the compatibility page: https://www.scummvm.org/compatibility.
|
||||
|
||||
|
||||
New port?
|
||||
---------
|
||||
|
||||
Keith Scroggins (aka KeithS) has been providing ScummVM (and many other) builds
|
||||
for the Atari community for an unbelievable 17 years. He put quite a lot of time
|
||||
into testing each release, updating ScummVM dependencies to their latest
|
||||
versions and even regularly upgrading his compiler toolchain to get the best
|
||||
possible performance.
|
||||
|
||||
However, ScummVM (and SDL to some extent) is a beast; it requires quite a lot
|
||||
of attention regarding where the cycles go, e.g. an additional screen refresh
|
||||
can easily drop the frame rate by half.
|
||||
|
||||
After I had seen how snappy NovaCoder's ScummVM on the Amiga is (who coded his
|
||||
own backend), I decided to check whether there was a way to get a better
|
||||
performing port on our platform. And there is!
|
||||
|
||||
I have managed to create a "native" Atari port talking directly to the
|
||||
hardware, skipping the SDL layer altogether, which is in some cases usable even
|
||||
on a plain 32 MHz Atari TT.
|
||||
|
||||
|
||||
Differences between the versions
|
||||
--------------------------------
|
||||
|
||||
After talking to Keith we have decided to provide three flavours of ScummVM.
|
||||
Please refer to https://docs.scummvm.org/en/v@VERSION@/other_platforms/atari.html
|
||||
for more details (TBD).
|
||||
|
||||
Atari Full package
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Minimum hardware requirements: Atari Falcon with 4 + 64 MB RAM, 68040 CPU.
|
||||
|
||||
- Because there is limited horsepower available on our platform, features like
|
||||
16bpp graphics, software synthesisers, scalers, real-time software
|
||||
MP3/OGG/FLAC playback etc., are omitted. This saves CPU cycles, memory and
|
||||
disk space.
|
||||
|
||||
- Tailored video settings for the best possible performance and visual
|
||||
experience (Falcon RGB overscan, chunky modes with the SuperVidel, TT 640x480
|
||||
for the overlay, ...).
|
||||
|
||||
- Direct rendering and single/triple buffering support.
|
||||
|
||||
- Blitting routines optimised for 68040 / 68060 CPU.
|
||||
|
||||
- Custom (and optimal) surface drawing (especially for the cursor).
|
||||
|
||||
- Custom (hardware based) aspect ratio correction (!)
|
||||
|
||||
- Full support for the SuperVidel, including the SuperBlitter (!)
|
||||
|
||||
- External DSP clock support for playing back samples at PC frequencies
|
||||
(Falcon only). Dual clock input frequency supported as well (Steinberg's
|
||||
FDI).
|
||||
|
||||
- Support for PC keys (page up, page down, pause, F11/F12, ...) and mouse wheel
|
||||
(Eiffel/Aranym only).
|
||||
|
||||
- Native MIDI output (if present).
|
||||
|
||||
- Runs also in Hatari and ARAnyM but in case of ARAnyM don't forget to disable
|
||||
fVDI to enable Videl output.
|
||||
|
||||
- FreeMiNT + memory protection friendly.
|
||||
|
||||
Atari Lite package
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Minimum hardware requirements: Atari TT / Falcon with 4 + 32 MB RAM.
|
||||
|
||||
As a further optimisation step, a 030-only version of ScummVM is provided, aimed
|
||||
at less powerful TT and Falcon machines with the 68030 CPU. It further restricts
|
||||
features but also improves performance and reduces executable size.
|
||||
|
||||
- Compiled with -m68030 => 68030/68882-specific optimisations enabled.
|
||||
|
||||
- Disabled 040+/SuperVidel code => faster code path for blitting.
|
||||
|
||||
- Doesn't support hires (640x480) games => smaller executable size.
|
||||
|
||||
- Overlay is rendered in 16 colours => faster redraw.
|
||||
|
||||
- Overlay during gameplay has no game background => even faster redraw.
|
||||
|
||||
- Overlay doesn't support alternative themes => faster loading time.
|
||||
|
||||
- "STMIDI" driver is automatically enabled (i.e. MIDI emulation is never used
|
||||
but still allows playing speech/sfx samples and/or CD audio).
|
||||
|
||||
FireBee package
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Hardware requirements: MCF5475 Evaluation Board or FireBee.
|
||||
|
||||
This one is still built and provided by Keith.
|
||||
|
||||
- Based on the most recent SDL.
|
||||
|
||||
- Contains various optimisations discovered / implemented from the Atari
|
||||
backend.
|
||||
|
||||
- Works in GEM (in theory also in XBIOS but that seems to be still broken on
|
||||
FireBee).
|
||||
|
||||
- Support for all engines is included in the build; this does not mean all
|
||||
games work. For instance, support for OGG and MP3 audio is included but the
|
||||
system can not handle playback of compressed audio; there is not enough
|
||||
processing power for both gameplay and sound at the same time.
|
||||
|
||||
Scalers can be utilized to make the GEM window larger on the Firebee.
|
||||
Performance is best when not using AdLib sound; using STMIDI would be
|
||||
optimal, but untested as of yet (I have been unable to get MIDI to work on my
|
||||
FireBee).
|
||||
|
||||
- Removed features: FLAC, MPEG2, Network/Cloud Support, HQ Scalers.
|
||||
|
||||
|
||||
The rest of this document describes things specific to the Full / Lite package.
|
||||
For the FireBee (SDL) build please refer to generic ScummVM documentation.
|
||||
|
||||
|
||||
Platform-specific features outside the GUI
|
||||
------------------------------------------
|
||||
|
||||
Keyboard shortcut "CONTROL+ALT+a": immediate aspect ratio correction on/off
|
||||
toggle.
|
||||
|
||||
"output_rate" in scummvm.ini: sample rate for mixing. Allowed values depend on
|
||||
the hardware connected:
|
||||
- TT030: 50066, 25033, 12517, 6258 Hz
|
||||
- Falcon030: as TT030 (except 6258) plus 49170, 32780, 24585, 19668, 16390,
|
||||
12292, 9834, 8195 Hz
|
||||
- External 22.5792 MHz DSP clock: as Falcon030 plus 44100, 29400, 22050,
|
||||
17640, 14700, 11025, 8820, 7350 Hz
|
||||
- External 24.576 MHz DSP clock: as Falcon030 plus 48000, 32000, 24000,
|
||||
19200, 16000, 12000, 9600, 8000 Hz
|
||||
The lower the value, the faster the mixing but also worse quality. Default is
|
||||
24585/25033 Hz (16-bit, stereo). Please note you don't have to enter the value
|
||||
exactly, it will be rounded to the nearest sane value.
|
||||
|
||||
"output_channels" in scummvm.ini: mono (1) or stereo (2) mixing. Please note
|
||||
that Falcon doesn't allow mixing in 16-bit mono, so this will have no effect on
|
||||
this machine.
|
||||
|
||||
"print_rate" in scummvm.ini: used for optimising sample playback (where
|
||||
available). It prints input and output sample format as well as the name of the
|
||||
converter used. See below for details.
|
||||
|
||||
"audio_buffer_size" in scummvm.ini: number of samples to preload. Default is
|
||||
2048 which equals to about 83ms of audio lag and seems to be about right for
|
||||
most games on my CT60@66 MHz.
|
||||
|
||||
If you want to play with "audio_buffer_size", the rule of thumb is: (lag in ms)
|
||||
= (audio_buffer_size / output_rate) * 1000. But it's totally OK just to double
|
||||
the samples value to get rid of stuttering in a heavier game.
|
||||
|
||||
|
||||
Graphics modes
|
||||
--------------
|
||||
|
||||
This topic is more complex than it looks. ScummVM renders game graphics using
|
||||
rectangles and this port offers the following options to render them:
|
||||
|
||||
Direct rendering
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is direct writing of the pixels into the screen buffer. On SuperVidel it is
|
||||
done natively, on Videl a chunky to planar conversion takes place beforehand.
|
||||
|
||||
Pros:
|
||||
|
||||
- On SuperVidel this offers fastest possible rendering (especially in 640x480
|
||||
with a lot of small rectangle updates where the buffer copying drags
|
||||
performance down).
|
||||
|
||||
- On Videl this _may_ offer fastest possible rendering if the rendering
|
||||
pipeline isn't flooded with too many small rectangles (C2P setup isn't for
|
||||
free). However with fullscreen intro sequences this is a no-brainer.
|
||||
|
||||
Cons:
|
||||
|
||||
- Screen tearing in most cases.
|
||||
|
||||
- On Videl, this may not work properly if a game engine uses its own buffers
|
||||
instead of surfaces (which are aligned on a 16pix boundary). Another source
|
||||
of danger is if an engine draws directly to the screen surface. Fortunately,
|
||||
each game can have its own graphics mode set separately so for games which do
|
||||
not work properly one can still leave the default graphics mode set.
|
||||
|
||||
- On Videl, overlay background isn't rendered (the GUI code can't work with
|
||||
bitplanes).
|
||||
|
||||
SuperBlitter used: sometimes (when ScummVM allocates surface via its create()
|
||||
function; custom/small buffers originating in the engine code are still copied
|
||||
using the CPU).
|
||||
|
||||
Single buffering
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is very similar to the previous mode with the difference that the engine
|
||||
uses an intermediate buffer for storing the rectangles but yet it remembers
|
||||
which ones they were.
|
||||
|
||||
Pros:
|
||||
|
||||
- Second fastest possible rendering.
|
||||
|
||||
Cons:
|
||||
|
||||
- Screen tearing in most cases.
|
||||
|
||||
- If there are too many smaller rectangles, it can be less efficient than
|
||||
updating the whole buffer at once.
|
||||
|
||||
SuperBlitter used: yes, for rectangle blitting to screen and cursor
|
||||
restoration. Sometimes also for generic copying between buffers (see above).
|
||||
|
||||
Triple buffering
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is the "true" triple buffering as described in
|
||||
https://en.wikipedia.org/wiki/Multiple_buffering#Triple_buffering and not "swap
|
||||
chain" as described in https://en.wikipedia.org/wiki/Swap_chain. The latter
|
||||
would be slightly slower as three buffers would need to be updated instead of
|
||||
two.
|
||||
|
||||
Pros:
|
||||
|
||||
- No screen tearing.
|
||||
|
||||
- Best compromise between performance and visual experience.
|
||||
|
||||
- Works well with both higher and lower frame rates.
|
||||
|
||||
Cons:
|
||||
|
||||
- If there are too many smaller rectangles, it can be less efficient than
|
||||
single buffering.
|
||||
|
||||
- Slightly irregular frame rate (depends solely on the game's complexity).
|
||||
|
||||
- In case of extremely fast rendering, one or more frames are dropped in favour
|
||||
of showing only the most recent one.
|
||||
|
||||
SuperBlitter used: yes, for rectangle blitting to screen and cursor
|
||||
restoration. Sometimes also for generic copying between buffers (see above).
|
||||
|
||||
Triple buffering is the default mode.
|
||||
|
||||
|
||||
SuperVidel and SuperBlitter
|
||||
---------------------------
|
||||
|
||||
As mentioned, this port uses SuperVidel and its SuperBlitter heavily. That
|
||||
means that if the SuperVidel is detected, it does the following:
|
||||
|
||||
- Uses 8bpp chunky resolutions.
|
||||
|
||||
- Patches all surface addresses by OR'ing 0xA0000000, i.e. using SV RAM instead
|
||||
of slow ST RAM (and even instead of TT RAM for allowing pure SuperBlitter
|
||||
copying).
|
||||
|
||||
- When SuperVidel FW version >= 9 is detected, the async FIFO buffer is used
|
||||
instead of the slower sync blitting (where one has to wait for every
|
||||
rectangle blit to finish), this sometimes leads to nearly zero-cost rendering
|
||||
and makes a *huge* difference for 640x480 fullscreen updates.
|
||||
|
||||
|
||||
Aspect ratio correction
|
||||
-----------------------
|
||||
|
||||
Please refer to the official documentation about its usage. Normally ScummVM
|
||||
implements this functionality using yet another fullscreen transformation of
|
||||
320x200 surface into a 320x240 one (there is even a selection of algorithms for
|
||||
this task, varying in performance and quality).
|
||||
|
||||
Naturally, this would impose a terrible performance penalty on our backend so
|
||||
some cheating has been used:
|
||||
|
||||
- On RGB, the vertical refresh rate frequency is set to 60 Hz, creating an
|
||||
illusion of non-square pixels. Works best on CRT monitors.
|
||||
|
||||
- On VGA, the vertical refresh rate frequency is set to 70 Hz, with more or
|
||||
less the same effect as on RGB. Works best on CRT monitors.
|
||||
|
||||
- On SuperVidel, video output is modified in such way that the DVI/HDMI monitor
|
||||
receives a 320x200 image and if properly set/supported, it will automatically
|
||||
stretch the image to 320x240 (this is usually a setting called "picture
|
||||
expansion" or "picture stretch" -- make sure it isn't set to something like
|
||||
"1:1" or "dot by dot").
|
||||
|
||||
Yes, it's a hack. :) Owners of a CRT monitor can achieve the same effect by the
|
||||
analog knobs -- stretch and move the 320x200 picture unless black borders are
|
||||
no longer visible. This hack provides a more elegant and per-game
|
||||
functionality.
|
||||
|
||||
|
||||
Audio mixing
|
||||
------------
|
||||
|
||||
ScummVM works internally with 16-bit samples so on the TT a simple downsampling
|
||||
to 8-bit resolution is used. However, there is still one piece missing - an
|
||||
XBIOS emulator (so ScummVM doesn't have to access hardware directly). There are
|
||||
two options (both available from https://mikrosk.github.io/xbios): STFA and
|
||||
X-SOUND, any of these will do. Or executing ScummVM in EmuTOS which contains
|
||||
the same routines as X-SOUND.
|
||||
|
||||
|
||||
Performance considerations/pitfalls
|
||||
-----------------------------------
|
||||
|
||||
It's important to understand what affects performance on our limited platform to
|
||||
avoid unpleasant gaming experiences.
|
||||
|
||||
Game engines with unexpected performance hit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A typical example from this category is Gobliiins (and its sequels), some SCI
|
||||
engine games (Gabriel Knight, Larry 2/7, ...) or the Sherlock engine (The Case
|
||||
of the Rose Tattoo). At first it looks like our machine or Atari backend is
|
||||
doing something terribly wrong but the truth is that it is the engine itself
|
||||
which is doing a lot of redraws, sometimes even before reaching the backend.
|
||||
The only solution is to profile and fix those engines.
|
||||
|
||||
Too many fullscreen updates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Somewhat related to the previous point - sometimes the engine authors didn't
|
||||
realise the impact of every update on the overall performance and instead of
|
||||
updating only the rectangles that really had changed, they ask for a fullscreen
|
||||
update. Not a problem on a >1 GHz machine but very visible on Atari! Also, this
|
||||
is (by definition) the case of animated intros, especially those in 640x480.
|
||||
|
||||
MIDI vs. AdLib vs. sampled music
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It could seem that sampled music replay must be the most demanding one but on
|
||||
the contrary! Always choose a CD version of a game (with *.wav tracks) over any
|
||||
other version. With one exception: if you have a native MIDI device able to
|
||||
replay the given game's MIDI notes (using the STMIDI plugin).
|
||||
|
||||
MIDI emulation (synthesis) can easily eat as much as 50% of all used CPU time
|
||||
(on the CT60). By default, this port uses the MAME OPL emulation (which is said
|
||||
to be fastest but also least accurate) but some engines require the DOSBOX one
|
||||
which is even more demanding. By the way, you can put "FM_high_quality=true" or
|
||||
"FM_medium_quality=true" into scummvm.ini if you want to experiment with a
|
||||
better quality synthesis, otherwise the lowest quality will be used (applies
|
||||
for MAME OPL only).
|
||||
|
||||
On TT, in most cases it makes sense to use ScummVM only if you own a native
|
||||
MIDI synthesiser (like mt32-pi: https://github.com/dwhinham/mt32-pi). MIDI
|
||||
emulation is out of the question and downsampling to 8-bit resolution takes a
|
||||
good chunk of CPU time which could be utilised elsewhere. However, there are
|
||||
games which are fine with sampled music/speech even on a plain TT (e.g. Lands
|
||||
of Lore).
|
||||
|
||||
CD music slows everything down
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some games use separate audio *and* video streams (files). Even if the CPU is
|
||||
able to handle both, the bottleneck becomes ... disk access. This is visible in
|
||||
The Curse Of Monkey Island for example -- there's audible stuttering during the
|
||||
intro sequence (and during the game as well). Increasing "audio_buffer_size"
|
||||
makes the rendering literally crawl! Why? Because disk I/O is busy with loading
|
||||
even *more* sample data so there's less time for video loading and rendering.
|
||||
Try to put "musdisk1.bun" and "musdisk2.bun" into a ramdisk (i.e. symlink to
|
||||
u:/ram in FreeMiNT), you'll be pleasantly surprised with the performance boost
|
||||
gained.
|
||||
|
||||
Mute vs. "No music"
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Currently ScummVM requires each backend to mix samples, even though they may
|
||||
contain muted output (i.e. zeroes). This is because the progression of sample
|
||||
playback tells ScummVM how much time has passed in e.g. an animation.
|
||||
|
||||
"No music" means using the null audio plugin which prevents generating any MIDI
|
||||
music (and therefore avoiding the expensive synthesis emulation) but beware, it
|
||||
doesn't affect CD (*.wav) playback at all! Same applies for speech and sfx.
|
||||
|
||||
The least amount of cycles is spent when:
|
||||
- "No music" as "Preferred device": This prevents MIDI synthesis of any kind.
|
||||
- "Subtitles" as "Text and speech": This prevents any sampled speech to be
|
||||
mixed.
|
||||
- All external audio files are deleted (typically *.wav); that way the mixer
|
||||
won't have anything to mix. However beware, this is not allowed in every game!
|
||||
|
||||
Sample rate
|
||||
~~~~~~~~~~~
|
||||
|
||||
It's important to realise the impact the sample rate has on the given game. The
|
||||
most obvious setting is its value: the bigger, the more demanding audio mixing
|
||||
becomes. However, if you inspect many games' samples, you will notice that most
|
||||
of them (esp. the ones from the 80s/90s) use simple samples like mono 11025 Hz
|
||||
(sometimes even less).
|
||||
|
||||
Obviously, setting "output_channels" to "1" is the easiest improvement
|
||||
(unfortunately only on TT). Next best thing you can do is to buy an external
|
||||
DSP clock for your Falcon: nearly all games use sample frequencies which are
|
||||
multiples of 44100 Hz: 22050, 11025, ... so with the external clock there
|
||||
won't be the need to resample them.
|
||||
|
||||
There's one caveat, though: it is important whether your replay frequency is
|
||||
equal to, a multiple of, or other than the desired one. Let's consider 44100
|
||||
and 22050 frequencies as an example (also applies to all the other
|
||||
frequencies):
|
||||
|
||||
- If you set 44100 Hz and a game requests 44100 Hz => so called "copyConvert"
|
||||
method will be used (fastest).
|
||||
- If you set 22050 Hz and a game requests 44100 Hz => so called "simpleConvert"
|
||||
method will be used (skipping every second sample, second fastest).
|
||||
- If you set 44100 Hz and a game requests 22050 Hz => so called
|
||||
"interpolateConvert" method will be used (slowest!).
|
||||
- Any other combination: "interpolateConvert" (slowest).
|
||||
|
||||
So how do you know which frequency to set as "output_rate" ? This is where
|
||||
"print_rate" comes to rescue. Enabling this option in scummvm.ini will tell you
|
||||
for each game which sample converters are being used and for what input/values.
|
||||
So you can easily verify whether the given game's demands match your setting.
|
||||
|
||||
Unfortunately, currently per-game "output_rate" / "output_channels" is not
|
||||
possible but this may change in the future.
|
||||
|
||||
Slow GUI
|
||||
~~~~~~~~
|
||||
|
||||
Themes handling is quite slow - each theme must be depacked, each one contains
|
||||
quite a few XML files to parse and quite a few images to load/convert. That's
|
||||
the reason why the built-in one is used as default, it dramatically speeds up
|
||||
loading time. To speed things up in other cases, the full version is
|
||||
distributed with repackaged theme files with compression level zero.
|
||||
|
||||
|
||||
Changes to upstream
|
||||
-------------------
|
||||
|
||||
There are a few features that have been disabled or changed and are not possible
|
||||
/ plausible to merge into upstream:
|
||||
|
||||
- the aforementioned "print_rate" feature, too invasive for other platforms
|
||||
|
||||
- This port contains an implementation of much faster tooltips in the overlay.
|
||||
However, there is a minor rendering bug which sometimes corrupts the
|
||||
background. But since its impact is huge, I left it in.
|
||||
|
||||
|
||||
Known issues
|
||||
------------
|
||||
|
||||
- When run on TT, screen contains horizontal black lines. That is due to the
|
||||
fact that TT offers only 320x480 in 256 colours. Possibly fixable by a Timer
|
||||
B interrupt.
|
||||
|
||||
- Horizontal screen shaking doesn't work on TT because TT Shifter doesn't
|
||||
support fine scrolling. However it is "emulated" via vertical shaking.
|
||||
|
||||
- Aspect ratio correction has no effect on TT because it is not possible to
|
||||
alter its vertical screen refresh frequency.
|
||||
|
||||
- The talkie version of SOMI needs to be merged from two sources:
|
||||
- The DOS version (install.bat) to obtain file "monster.sou".
|
||||
- The FLAC version (install_flac.bat) to obtain folders "cd_music_flac" and
|
||||
"se_music_flac" (these *.flac files then have to be converted to *.wav
|
||||
manually).
|
||||
- Files "monkey.000" and "monkey.001" can be taken from either version.
|
||||
- Point the extra path to the folder with *.wav files (or copy its content
|
||||
where monkey.00? files are located).
|
||||
|
||||
- Following engines have been explicitly disabled:
|
||||
- Cine (2 games)
|
||||
- Incompatible with other engines / prone to freezes.
|
||||
- https://wiki.scummvm.org/index.php?title=Cine
|
||||
- Director (many games)
|
||||
- Huge game list slows detection for other games, and would require
|
||||
(currently missing) localization support.
|
||||
- Only small subset of games actually supported by upstream, but none of
|
||||
them detected on TOS 8+3 file system.
|
||||
- https://wiki.scummvm.org/index.php?title=Director
|
||||
- Hugo (3 games)
|
||||
- Uses (lot of) overlay dialogs which are problematic for Atari backend.
|
||||
- Engine GUI (for save/load/etc) does not support 8-bit screens.
|
||||
- https://wiki.scummvm.org/index.php?title=Hugo
|
||||
- Ultima (many games)
|
||||
- The only non-hires ultima engine is ultima1; see
|
||||
https://bugs.scummvm.org/ticket/14790
|
||||
- This prevents adding the 15 MB ultima.dat to the release archive.
|
||||
- https://wiki.scummvm.org/index.php?title=Ultima
|
||||
|
||||
- When using FreeMiNT, ScummVM requires a recent kernel (>= 2021), otherwise
|
||||
keyboard handling won't work properly.
|
||||
|
||||
- When using EmuTOS, ScummVM requires a recent release (>= 1.3), otherwise
|
||||
various screen- and sound-related issues may appear.
|
||||
|
||||
Future plans
|
||||
------------
|
||||
|
||||
- DSP-based sample mixer (WAV, FLAC, MP2).
|
||||
|
||||
- Avoid loading music/speech files (and thus slowing down everything) if muted.
|
||||
|
||||
- Cached audio/video streams (i.e. don't load only "audio_buffer_size" number
|
||||
of samples but cache, say, 1 second so disk i/o won't be so stressed).
|
||||
|
||||
- Using Thorsten Otto's sharedlibs: https://tho-otto.de/sharedlibs.php for game
|
||||
engine plugins to relieve the huge binary size.
|
||||
|
||||
- True audio CD support via MetaDOS API.
|
||||
|
||||
- OPL2LPT and Retrowave support (if I manage to purchase it somewhere).
|
||||
|
||||
|
||||
Closing words
|
||||
-------------
|
||||
|
||||
Many optimisations and improvements wouldn't be possible without the help of
|
||||
Eero Tamminen, so thank you for all the help with profiling in Hatari.
|
||||
|
||||
Miro Kropacek aka MiKRO
|
||||
Brisbane / Australia
|
||||
miro.kropacek@gmail.com
|
||||
http://mikro.atari.org
|
||||
79
backends/platform/atari/symbols.h
Normal file
79
backends/platform/atari/symbols.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Atari a.out / ELF symbol handling by Thorsten Otto.
|
||||
// include in each of the .S files and put every global symbol into
|
||||
// SYM(<symbol name>).
|
||||
|
||||
#ifndef __USER_LABEL_PREFIX__
|
||||
#define __USER_LABEL_PREFIX__ _
|
||||
#endif
|
||||
|
||||
#ifndef __REGISTER_PREFIX__
|
||||
#define __REGISTER_PREFIX__
|
||||
#endif
|
||||
|
||||
#ifndef __IMMEDIATE_PREFIX__
|
||||
#define __IMMEDIATE_PREFIX__ #
|
||||
#endif
|
||||
|
||||
#define CONCAT1(a, b) CONCAT2(a, b)
|
||||
#define CONCAT2(a, b) a ## b
|
||||
|
||||
/* Use the right prefix for global labels. */
|
||||
|
||||
#define SYM(x) CONCAT1 (__USER_LABEL_PREFIX__, x)
|
||||
|
||||
#ifdef __ELF__
|
||||
#define FUNC(x) .type SYM(x),function
|
||||
#else
|
||||
/* The .proc pseudo-op is accepted, but ignored, by GAS. We could just
|
||||
define this to the empty string for non-ELF systems, but defining it
|
||||
to .proc means that the information is available to the assembler if
|
||||
the need arises. */
|
||||
#define FUNC(x) .proc
|
||||
#endif
|
||||
|
||||
#define REG(x) CONCAT1 (__REGISTER_PREFIX__, x)
|
||||
|
||||
#define IMM(x) CONCAT1 (__IMMEDIATE_PREFIX__, x)
|
||||
|
||||
#define d0 REG(d0)
|
||||
#define d1 REG(d1)
|
||||
#define d2 REG(d2)
|
||||
#define d3 REG(d3)
|
||||
#define d4 REG(d4)
|
||||
#define d5 REG(d5)
|
||||
#define d6 REG(d6)
|
||||
#define d7 REG(d7)
|
||||
#define a0 REG(a0)
|
||||
#define a1 REG(a1)
|
||||
#define a2 REG(a2)
|
||||
#define a3 REG(a3)
|
||||
#define a4 REG(a4)
|
||||
#define a5 REG(a5)
|
||||
#define a6 REG(a6)
|
||||
#define a7 REG(a7)
|
||||
#define fp REG(fp)
|
||||
#define sp REG(sp)
|
||||
#define pc REG(pc)
|
||||
|
||||
#define sr REG(sr)
|
||||
26
backends/platform/dc/DCLauncherDialog.h
Normal file
26
backends/platform/dc/DCLauncherDialog.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
class DCLauncherDialog {
|
||||
public:
|
||||
DCLauncherDialog() {}
|
||||
int runModal();
|
||||
};
|
||||
95
backends/platform/dc/Makefile
Normal file
95
backends/platform/dc/Makefile
Normal file
@@ -0,0 +1,95 @@
|
||||
|
||||
ronindir = /usr/local/ronin
|
||||
|
||||
DYNAMIC_MODULES = 1
|
||||
|
||||
srcdir = ../../..
|
||||
VPATH = $(srcdir)
|
||||
|
||||
CC = sh-elf-gcc -ml -m4-single-only
|
||||
CXX = sh-elf-g++ -ml -m4-single-only
|
||||
LD = $(CXX)
|
||||
CXXFLAGS= -O3 -Wno-multichar -funroll-loops -fschedule-insns2 -fomit-frame-pointer -fdelete-null-pointer-checks -fno-exceptions
|
||||
DEFINES = -D__DC__ -DNONSTANDARD_PORT -DUSE_MAD -DUSE_ZLIB -DDISABLE_DEFAULT_SAVEFILEMANAGER -DDISABLE_TEXT_CONSOLE -DDISABLE_COMMAND_LINE -DUSE_RGB_COLOR
|
||||
# For release builds:
|
||||
#DEFINES += -DNOSERIAL
|
||||
LDFLAGS = -Wl,-Ttext,0x8c010000 -nostartfiles $(ronindir)/lib/crt0.o
|
||||
INCLUDES= -I./ -I$(srcdir) -I$(ronindir)/include/ -I$(srcdir)/engines
|
||||
LIBS = -L$(ronindir)/lib -lmad -lronin -lz -lm
|
||||
# For release builds:
|
||||
#LIBS = -L$(ronindir)/lib -lmad -lronin-noserial -lz -lm
|
||||
EXECUTABLE = scummvm.elf
|
||||
DEPDIR = .deps
|
||||
CXX_UPDATE_DEP_FLAG = -Wp,-MMD,"$(*D)/$(DEPDIR)/$(*F).d",-MQ,"$@",-MP
|
||||
PLUGIN_PREFIX =
|
||||
PLUGIN_SUFFIX = .plg
|
||||
PLUGIN_EXTRA_DEPS = plugin.x plugin.syms scummvm.elf plugin_head.o
|
||||
PLUGIN_LDFLAGS = -nostartfiles -Wl,-q,-Tplugin.x,--just-symbols,scummvm.elf,--retain-symbols-file,plugin.syms -L$(ronindir)/lib plugin_head.o
|
||||
MKDIR = mkdir -p
|
||||
RM = rm -f
|
||||
RM_REC = rm -rf
|
||||
AR = sh-elf-ar cru
|
||||
RANLIB = sh-elf-ranlib
|
||||
USE_RGB_COLOR = true
|
||||
|
||||
ifdef DYNAMIC_MODULES
|
||||
DEFINES += -DDYNAMIC_MODULES
|
||||
PRE_OBJS_FLAGS = -Wl,--whole-archive
|
||||
POST_OBJS_FLAGS = -Wl,--no-whole-archive
|
||||
ENABLED=DYNAMIC_PLUGIN
|
||||
else
|
||||
ENABLED=STATIC_PLUGIN
|
||||
endif
|
||||
|
||||
ENABLE_SCUMM = $(ENABLED)
|
||||
# Not meaningful anymore (bug #6008)
|
||||
#ENABLE_SCUMM_7_8 = $(ENABLED)
|
||||
ENABLE_HE = $(ENABLED)
|
||||
ENABLE_AGI = $(ENABLED)
|
||||
ENABLE_AGOS = $(ENABLED)
|
||||
ENABLE_CINE = $(ENABLED)
|
||||
ENABLE_CRUISE = $(ENABLED)
|
||||
ENABLE_DRASCULA = $(ENABLED)
|
||||
ENABLE_GOB = $(ENABLED)
|
||||
ENABLE_GROOVIE = $(ENABLED)
|
||||
ENABLE_IHNM = $(ENABLED)
|
||||
ENABLE_KYRA = $(ENABLED)
|
||||
ENABLE_LURE = $(ENABLED)
|
||||
ENABLE_M4 = $(ENABLED)
|
||||
ENABLE_MADE = $(ENABLED)
|
||||
ENABLE_PARALLACTION = $(ENABLED)
|
||||
ENABLE_QUEEN = $(ENABLED)
|
||||
ENABLE_SAGA = $(ENABLED)
|
||||
ENABLE_SKY = $(ENABLED)
|
||||
ENABLE_SWORD1 = $(ENABLED)
|
||||
ENABLE_SWORD2 = $(ENABLED)
|
||||
ENABLE_TOUCHE = $(ENABLED)
|
||||
ENABLE_TUCKER = $(ENABLED)
|
||||
|
||||
OBJS := dcmain.o time.o display.o audio.o input.o selector.o icon.o \
|
||||
label.o vmsave.o softkbd.o dcloader.o cache.o dc-fs.o plugins.o \
|
||||
dcutils.o
|
||||
|
||||
MODULE_DIRS += ./
|
||||
|
||||
BACKEND := dc
|
||||
|
||||
include $(srcdir)/Makefile.common
|
||||
|
||||
scummvm.bin : scummvm.elf
|
||||
sh-elf-objcopy -S -R .stack -O binary $< $@
|
||||
|
||||
SCUMMVM.BIN : scummvm.bin
|
||||
scramble $< $@
|
||||
|
||||
plugin_dist : plugins
|
||||
for p in $(PLUGINS); do \
|
||||
t="`basename \"$$p\" | LC_CTYPE=C tr '[:lower:]' '[:upper:]'`"; \
|
||||
sh-elf-strip -g -o "$$t" "$$p"; \
|
||||
./check_plugin_symbols "$$t"; \
|
||||
done
|
||||
|
||||
dist : SCUMMVM.BIN plugin_dist
|
||||
|
||||
spotless : distclean
|
||||
$(RM) SCUMMVM.BIN scummvm.bin *.PLG
|
||||
25
backends/platform/dc/README
Normal file
25
backends/platform/dc/README
Normal file
@@ -0,0 +1,25 @@
|
||||
Compiling ScummVM for SEGA Dreamcast
|
||||
====================================
|
||||
|
||||
If you want to compile ScummVM for your Dreamcast,
|
||||
you'll need the following:
|
||||
|
||||
* gcc-4.6.3 configured as a cross-compiler for `sh-elf'
|
||||
|
||||
* binutils-2.18 configured likewise
|
||||
|
||||
* newlib for sh-elf : <URL:http://mc.pp.se/dc/files/newlib-1.19.0.tar.gz>
|
||||
|
||||
* libronin-0.7 : <URL:http://peter.bortas.org/scumm/libronin-0.7.tar.gz>
|
||||
|
||||
* libmad : <URL:http://mc.pp.se/dc/files/libmad-0.15.1b.tar.gz>
|
||||
|
||||
* GNU make
|
||||
|
||||
|
||||
Edit the Makefile to contain the path to libronin if you installed it
|
||||
somewhere other than /usr/local/ronin, then run `make dist', and you
|
||||
should get a scrambled binary SCUMMVM.BIN and some plugins *.PLG.
|
||||
|
||||
For serial/IP upload, remove the "DYNAMIC_MODULES" line and just run `make',
|
||||
to get a static binary with the name `scummvm.elf'.
|
||||
75
backends/platform/dc/audio.cpp
Normal file
75
backends/platform/dc/audio.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <common/scummsys.h>
|
||||
#include "engines/engine.h"
|
||||
#include "audio/mixer_intern.h"
|
||||
#include "dc.h"
|
||||
|
||||
EXTERN_C void *memcpy4s(void *s1, const void *s2, unsigned int n);
|
||||
|
||||
uint OSystem_Dreamcast::initSound()
|
||||
{
|
||||
stop_sound();
|
||||
do_sound_command(CMD_SET_FREQ_EXP(FREQ_22050_EXP));
|
||||
do_sound_command(CMD_SET_STEREO(1));
|
||||
do_sound_command(CMD_SET_BUFFER(SOUND_BUFFER_SHIFT));
|
||||
return read_sound_int(&SOUNDSTATUS->freq);
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::checkSound()
|
||||
{
|
||||
int n;
|
||||
int curr_ring_buffer_samples;
|
||||
|
||||
if (!_mixer)
|
||||
return;
|
||||
|
||||
if (read_sound_int(&SOUNDSTATUS->mode) != MODE_PLAY)
|
||||
start_sound();
|
||||
|
||||
curr_ring_buffer_samples = read_sound_int(&SOUNDSTATUS->ring_length);
|
||||
|
||||
n = read_sound_int(&SOUNDSTATUS->samplepos);
|
||||
|
||||
if ((n-=fillpos)<0)
|
||||
n += curr_ring_buffer_samples;
|
||||
|
||||
n = ADJUST_BUFFER_SIZE(n-10);
|
||||
|
||||
if (n<100)
|
||||
return;
|
||||
|
||||
_mixer->mixCallback((byte *)temp_sound_buffer,
|
||||
2*SAMPLES_TO_BYTES(n));
|
||||
|
||||
if (fillpos+n > curr_ring_buffer_samples) {
|
||||
int r = curr_ring_buffer_samples - fillpos;
|
||||
memcpy4s(RING_BUF+fillpos, temp_sound_buffer, SAMPLES_TO_BYTES(r));
|
||||
fillpos = 0;
|
||||
n -= r;
|
||||
memcpy4s(RING_BUF, temp_sound_buffer+r, SAMPLES_TO_BYTES(n));
|
||||
} else {
|
||||
memcpy4s(RING_BUF+fillpos, temp_sound_buffer, SAMPLES_TO_BYTES(n));
|
||||
}
|
||||
if ((fillpos += n) >= curr_ring_buffer_samples)
|
||||
fillpos = 0;
|
||||
}
|
||||
37
backends/platform/dc/cache.S
Normal file
37
backends/platform/dc/cache.S
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
.globl _flush_instruction_cache
|
||||
|
||||
.align 2
|
||||
|
||||
! Flush the SH instruction cache
|
||||
|
||||
_flush_instruction_cache:
|
||||
mova fcc,r0
|
||||
mov.l p2_mask,r1
|
||||
or r1,r0
|
||||
jmp @r0
|
||||
nop
|
||||
nop
|
||||
fcc:
|
||||
mov.l ccr_addr,r0
|
||||
mov.l ccr_data,r1
|
||||
mov.l r1,@r0
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
rts
|
||||
nop
|
||||
|
||||
.align 2
|
||||
|
||||
p2_mask:
|
||||
.long 0xa0000000
|
||||
ccr_addr:
|
||||
.long 0xff00001c
|
||||
ccr_data:
|
||||
.word 0x0905
|
||||
15
backends/platform/dc/check_plugin_symbols
Normal file
15
backends/platform/dc/check_plugin_symbols
Normal file
@@ -0,0 +1,15 @@
|
||||
#! /bin/sh
|
||||
case "$0" in
|
||||
*/*) dir=`dirname "$0"`/;;
|
||||
*) dir="";;
|
||||
esac
|
||||
exec < "$dir"plugin.syms
|
||||
while read sym; do
|
||||
if sh-elf-nm "$1" | grep >/dev/null " $sym"'$'; then
|
||||
:
|
||||
else
|
||||
echo >&2 "ERROR: Symbol $sym missing from $1"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
172
backends/platform/dc/dc-fs.cpp
Normal file
172
backends/platform/dc/dc-fs.cpp
Normal file
@@ -0,0 +1,172 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#define FORBIDDEN_SYMBOL_ALLOW_ALL
|
||||
|
||||
#include "dc.h"
|
||||
#include "backends/fs/abstract-fs.h"
|
||||
#include "backends/fs/stdiostream.h"
|
||||
|
||||
#include <ronin/cdfs.h>
|
||||
#include <stdio.h>
|
||||
#define usleep usleep_unistd
|
||||
#include <unistd.h>
|
||||
#undef usleep
|
||||
|
||||
/**
|
||||
* Implementation of the ScummVM file system API based on Ronin.
|
||||
*
|
||||
* Parts of this class are documented in the base interface class, AbstractFSNode.
|
||||
*/
|
||||
class RoninCDFileNode : public AbstractFSNode {
|
||||
protected:
|
||||
Common::String _path;
|
||||
|
||||
public:
|
||||
RoninCDFileNode(const Common::String &path) : _path(path) {}
|
||||
|
||||
bool exists() const override { return true; }
|
||||
Common::String getName() const override { return lastPathComponent(_path, '/'); }
|
||||
Common::U32String getDisplayName() const override { return getName(); }
|
||||
Common::String getPath() const override { return _path; }
|
||||
bool isDirectory() const override { return false; }
|
||||
bool isReadable() const override { return true; }
|
||||
bool isWritable() const override { return false; }
|
||||
|
||||
AbstractFSNode *getChild(const Common::String &n) const override { return NULL; }
|
||||
bool getChildren(AbstractFSList &list, ListMode mode, bool hidden) const override { return false; }
|
||||
AbstractFSNode *getParent() const override;
|
||||
|
||||
Common::SeekableReadStream *createReadStream() override;
|
||||
Common::SeekableWriteStream *createWriteStream(bool atomic) override { return 0; }
|
||||
bool createDirectory() override { return false; }
|
||||
|
||||
static AbstractFSNode *makeFileNodePath(const Common::String &path);
|
||||
};
|
||||
|
||||
/* A directory */
|
||||
class RoninCDDirectoryNode final : public RoninCDFileNode {
|
||||
public:
|
||||
RoninCDDirectoryNode(const Common::String &path) : RoninCDFileNode(path) {}
|
||||
|
||||
bool isDirectory() const override { return true; }
|
||||
AbstractFSNode *getChild(const Common::String &n) const override;
|
||||
bool getChildren(AbstractFSList &list, ListMode mode, bool hidden) const override;
|
||||
Common::SeekableReadStream *createReadStream() override { return 0; }
|
||||
bool createDirectory() override { return true; }
|
||||
};
|
||||
|
||||
/* A file/directory which does not exist */
|
||||
class RoninCDNonexistingNode final : public RoninCDFileNode {
|
||||
public:
|
||||
RoninCDNonexistingNode(const Common::String &path) : RoninCDFileNode(path) {}
|
||||
|
||||
bool exists() const override { return false; }
|
||||
bool isReadable() const override { return false; }
|
||||
Common::SeekableReadStream *createReadStream() override { return 0; }
|
||||
};
|
||||
|
||||
AbstractFSNode *RoninCDFileNode::makeFileNodePath(const Common::String &path) {
|
||||
assert(path.size() > 0);
|
||||
|
||||
int fd;
|
||||
|
||||
if ((fd = open(path.c_str(), O_RDONLY)) >= 0) {
|
||||
close(fd);
|
||||
return new RoninCDFileNode(path);
|
||||
} else if ((fd = open(path.c_str(), O_DIR|O_RDONLY)) >= 0) {
|
||||
close(fd);
|
||||
return new RoninCDDirectoryNode(path);
|
||||
} else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
AbstractFSNode *RoninCDDirectoryNode::getChild(const Common::String &n) const {
|
||||
Common::String newPath(_path);
|
||||
if (_path.lastChar() != '/')
|
||||
newPath += '/';
|
||||
newPath += n;
|
||||
|
||||
return makeFileNodePath(newPath);
|
||||
}
|
||||
|
||||
bool RoninCDDirectoryNode::getChildren(AbstractFSList &myList, ListMode mode, bool hidden) const {
|
||||
|
||||
DIR *dirp = opendir(_path.c_str());
|
||||
struct dirent *dp;
|
||||
|
||||
if (dirp == NULL)
|
||||
return false;
|
||||
|
||||
// ... loop over dir entries using readdir
|
||||
while ((dp = readdir(dirp)) != NULL) {
|
||||
Common::String newPath(_path);
|
||||
if (newPath.lastChar() != '/')
|
||||
newPath += '/';
|
||||
newPath += dp->d_name;
|
||||
|
||||
if (dp->d_size < 0) {
|
||||
// Honor the chosen mode
|
||||
if (mode == Common::FSNode::kListFilesOnly)
|
||||
continue;
|
||||
|
||||
myList.push_back(new RoninCDDirectoryNode(newPath));
|
||||
} else {
|
||||
// Honor the chosen mode
|
||||
if (mode == Common::FSNode::kListDirectoriesOnly)
|
||||
continue;
|
||||
|
||||
myList.push_back(new RoninCDFileNode(newPath));
|
||||
}
|
||||
}
|
||||
closedir(dirp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
AbstractFSNode *RoninCDFileNode::getParent() const {
|
||||
if (_path == "/")
|
||||
return 0;
|
||||
|
||||
const char *start = _path.c_str();
|
||||
const char *end = lastPathComponent(_path, '/');
|
||||
|
||||
return new RoninCDDirectoryNode(Common::String(start, end - start));
|
||||
}
|
||||
|
||||
|
||||
Common::SeekableReadStream *RoninCDFileNode::createReadStream() {
|
||||
return StdioStream::makeFromPath(getPath().c_str(), StdioStream::WriteMode_Read);
|
||||
}
|
||||
|
||||
AbstractFSNode *OSystem_Dreamcast::makeRootFileNode() const {
|
||||
return new RoninCDDirectoryNode("/");
|
||||
}
|
||||
|
||||
AbstractFSNode *OSystem_Dreamcast::makeCurrentDirectoryFileNode() const {
|
||||
return makeRootFileNode();
|
||||
}
|
||||
|
||||
AbstractFSNode *OSystem_Dreamcast::makeFileNodePath(const Common::String &path) const {
|
||||
AbstractFSNode *node = RoninCDFileNode::makeFileNodePath(path);
|
||||
return (node ? node : new RoninCDNonexistingNode(path));
|
||||
}
|
||||
254
backends/platform/dc/dc.h
Normal file
254
backends/platform/dc/dc.h
Normal file
@@ -0,0 +1,254 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "backends/base-backend.h"
|
||||
#include <graphics/surface.h>
|
||||
#include <graphics/paletteman.h>
|
||||
#include <ronin/soundcommon.h>
|
||||
#include "backends/timer/default/default-timer.h"
|
||||
#include "backends/audiocd/default/default-audiocd.h"
|
||||
#include "backends/fs/fs-factory.h"
|
||||
#include "audio/mixer_intern.h"
|
||||
#include "common/language.h"
|
||||
#include "common/platform.h"
|
||||
#ifdef DYNAMIC_MODULES
|
||||
#include "backends/plugins/dynamic-plugin.h"
|
||||
#endif
|
||||
|
||||
#define NUM_BUFFERS 4
|
||||
#define SOUND_BUFFER_SHIFT 3
|
||||
|
||||
class Interactive
|
||||
{
|
||||
public:
|
||||
virtual int key(int k, byte &shiftFlags) = 0;
|
||||
virtual void mouse(int x, int y) = 0;
|
||||
virtual ~Interactive() = 0;
|
||||
};
|
||||
inline Interactive::~Interactive() { }
|
||||
|
||||
#include "softkbd.h"
|
||||
|
||||
class DCHardware {
|
||||
private:
|
||||
static void dc_init_hardware();
|
||||
protected:
|
||||
DCHardware() { dc_init_hardware(); }
|
||||
};
|
||||
|
||||
class DCCDManager : public DefaultAudioCDManager {
|
||||
public:
|
||||
// Poll cdrom status
|
||||
// Returns true if cd audio is playing
|
||||
bool isPlaying() const override;
|
||||
|
||||
// Play cdrom audio track
|
||||
bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false,
|
||||
Audio::Mixer::SoundType soundType = Audio::Mixer::kMusicSoundType) override;
|
||||
|
||||
// Stop cdrom audio track
|
||||
void stop() override;
|
||||
};
|
||||
|
||||
class OSystem_Dreamcast : private DCHardware, public EventsBaseBackend, public PaletteManager, public FilesystemFactory
|
||||
#ifdef DYNAMIC_MODULES
|
||||
, public FilePluginProvider
|
||||
#endif
|
||||
{
|
||||
|
||||
public:
|
||||
OSystem_Dreamcast();
|
||||
|
||||
virtual void initBackend();
|
||||
|
||||
// Determine whether the backend supports the specified feature.
|
||||
bool hasFeature(Feature f);
|
||||
|
||||
// En-/disable the specified feature.
|
||||
void setFeatureState(Feature f, bool enable);
|
||||
|
||||
// Query the state of the specified feature.
|
||||
bool getFeatureState(Feature f);
|
||||
|
||||
// Set colors of the palette
|
||||
PaletteManager *getPaletteManager() { return this; }
|
||||
protected:
|
||||
// PaletteManager API
|
||||
void setPalette(const byte *colors, uint start, uint num);
|
||||
void grabPalette(byte *colors, uint start, uint num) const;
|
||||
|
||||
public:
|
||||
|
||||
// Determine the pixel format currently in use for screen rendering.
|
||||
Graphics::PixelFormat getScreenFormat() const;
|
||||
|
||||
// Returns a list of all pixel formats supported by the backend.
|
||||
Common::List<Graphics::PixelFormat> getSupportedFormats() const;
|
||||
|
||||
// Set the size of the video bitmap.
|
||||
// Typically, 320x200
|
||||
void initSize(uint w, uint h, const Graphics::PixelFormat *format);
|
||||
int16 getHeight() { return _screen_h; }
|
||||
int16 getWidth() { return _screen_w; }
|
||||
|
||||
// Draw a bitmap to screen.
|
||||
// The screen will not be updated to reflect the new bitmap
|
||||
void copyRectToScreen(const void *buf, int pitch, int x, int y, int w, int h);
|
||||
|
||||
virtual Graphics::Surface *lockScreen();
|
||||
virtual void unlockScreen();
|
||||
|
||||
// Update the dirty areas of the screen
|
||||
void updateScreen();
|
||||
|
||||
// Either show or hide the mouse cursor
|
||||
bool showMouse(bool visible);
|
||||
|
||||
// Move ("warp") the mouse cursor to the specified position.
|
||||
void warpMouse(int x, int y);
|
||||
|
||||
// Set the bitmap that's used when drawing the cursor.
|
||||
void setMouseCursor(const void *buf, uint w, uint h, int hotspot_x, int hotspot_y, uint32 keycolor, bool dontScale, const Graphics::PixelFormat *format, const byte *mask);
|
||||
|
||||
// Replace the specified range of cursor the palette with new colors.
|
||||
void setCursorPalette(const byte *colors, uint start, uint num);
|
||||
|
||||
// Shaking is used in SCUMM. Set current shake position.
|
||||
void setShakePos(int shake_x_pos, int shake_y_pos);
|
||||
|
||||
// Get the number of milliseconds since the program was started.
|
||||
uint32 getMillis(bool skipRecord = false);
|
||||
|
||||
// Delay for a specified amount of milliseconds
|
||||
void delayMillis(uint msecs);
|
||||
|
||||
// Get the current time and date. Correspond to time()+localtime().
|
||||
void getTimeAndDate(TimeDate &td, bool skipRecord = false) const;
|
||||
|
||||
// Get the next event.
|
||||
// Returns true if an event was retrieved.
|
||||
bool pollEvent(Common::Event &event);
|
||||
|
||||
// Quit
|
||||
void quit();
|
||||
|
||||
// Overlay
|
||||
int16 getOverlayHeight() const;
|
||||
int16 getOverlayWidth() const;
|
||||
bool isOverlayVisible() const { return _overlay_visible; }
|
||||
void showOverlay(bool inGUI);
|
||||
void hideOverlay();
|
||||
void clearOverlay();
|
||||
void grabOverlay(Graphics::Surface &surface);
|
||||
void copyRectToOverlay(const void *buf, int pitch, int x, int y, int w, int h);
|
||||
virtual Graphics::PixelFormat getOverlayFormat() const { return Graphics::PixelFormat(2, 4, 4, 4, 4, 8, 4, 0, 12); }
|
||||
|
||||
// Mutex handling
|
||||
Common::MutexInternal *createMutex();
|
||||
|
||||
// Set a window caption or any other comparable status display to the
|
||||
// given value.
|
||||
void setWindowCaption(const Common::U32String &caption);
|
||||
|
||||
// Modulatized backend
|
||||
Audio::Mixer *getMixer() { return _mixer; }
|
||||
|
||||
// Extra SoftKbd support
|
||||
void mouseToSoftKbd(int x, int y, int &rx, int &ry) const;
|
||||
|
||||
// Filesystem
|
||||
AbstractFSNode *makeRootFileNode() const override;
|
||||
AbstractFSNode *makeCurrentDirectoryFileNode() const override;
|
||||
AbstractFSNode *makeFileNodePath(const Common::String &path) const override;
|
||||
|
||||
private:
|
||||
|
||||
Audio::MixerImpl *_mixer;
|
||||
SoftKeyboard _softkbd;
|
||||
|
||||
int _ms_cur_x, _ms_cur_y, _ms_cur_w, _ms_cur_h, _ms_old_x, _ms_old_y;
|
||||
int _ms_hotspot_x, _ms_hotspot_y, _ms_visible, _devpoll, _last_screen_refresh;
|
||||
int _current_shake_x_pos, _current_shake_y_pos, _screen_w, _screen_h;
|
||||
int _overlay_x, _overlay_y;
|
||||
unsigned char *_ms_buf;
|
||||
uint32 _ms_keycolor;
|
||||
bool _overlay_visible, _overlay_dirty, _screen_dirty;
|
||||
int _screen_buffer, _overlay_buffer, _mouse_buffer;
|
||||
bool _aspect_stretch, _softkbd_on, _enable_cursor_palette;
|
||||
bool _overlay_in_gui;
|
||||
float _overlay_fade, _xscale, _yscale, _top_offset;
|
||||
int _softkbd_motion;
|
||||
|
||||
unsigned char *screen;
|
||||
unsigned short *mouse;
|
||||
unsigned short *overlay;
|
||||
void *screen_tx[NUM_BUFFERS];
|
||||
void *mouse_tx[NUM_BUFFERS];
|
||||
void *ovl_tx[NUM_BUFFERS];
|
||||
unsigned short palette[256], cursor_palette[256];
|
||||
|
||||
Graphics::Surface _framebuffer;
|
||||
int _screenFormat, _mouseFormat;
|
||||
|
||||
int temp_sound_buffer[RING_BUFFER_SAMPLES>>SOUND_BUFFER_SHIFT];
|
||||
|
||||
uint initSound();
|
||||
void checkSound();
|
||||
|
||||
void updateScreenTextures(void);
|
||||
void updateScreenPolygons(void);
|
||||
void maybeRefreshScreen(void);
|
||||
void drawMouse(int xdraw, int ydraw, int w, int h,
|
||||
unsigned char *buf, bool visible);
|
||||
|
||||
void setScaling();
|
||||
|
||||
|
||||
Common::SaveFileManager *createSavefileManager();
|
||||
|
||||
Common::SeekableReadStream *createConfigReadStream();
|
||||
Common::WriteStream *createConfigWriteStream();
|
||||
|
||||
void logMessage(LogMessageType::Type type, const char *message);
|
||||
Common::String getSystemLanguage() const;
|
||||
|
||||
#ifdef DYNAMIC_MODULES
|
||||
class DCPlugin;
|
||||
|
||||
protected:
|
||||
Plugin* createPlugin(const Common::FSNode &node) const;
|
||||
bool isPluginFilename(const Common::FSNode &node) const;
|
||||
void addCustomDirectories(Common::FSList &dirs) const;
|
||||
public:
|
||||
PluginList getPlugins();
|
||||
private:
|
||||
const char *pluginCustomDirectory;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
extern int handleInput(struct mapledev *pad,
|
||||
int &mouse_x, int &mouse_y,
|
||||
byte &shiftFlags, Interactive *inter = NULL);
|
||||
extern bool selectGame(char *&, char *&, char *&, Common::Language &, Common::Platform &, class Icon &);
|
||||
#ifdef DYNAMIC_MODULES
|
||||
extern bool selectPluginDir(Common::String &selection, const Common::FSNode &base);
|
||||
#endif
|
||||
446
backends/platform/dc/dcloader.cpp
Normal file
446
backends/platform/dc/dcloader.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ronin/ronin.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "dcloader.h"
|
||||
|
||||
#include <cxxabi.h>
|
||||
|
||||
#ifdef DL_DEBUG
|
||||
#define DBG(x) reportf x
|
||||
#else
|
||||
#define DBG(x) do{}while(0)
|
||||
#endif
|
||||
|
||||
|
||||
/* ELF stuff */
|
||||
|
||||
typedef unsigned short Elf32_Half, Elf32_Section;
|
||||
typedef unsigned long Elf32_Word, Elf32_Addr, Elf32_Off;
|
||||
typedef signed long Elf32_Sword;
|
||||
typedef Elf32_Half Elf32_Versym;
|
||||
|
||||
#define EI_NIDENT (16)
|
||||
#define ELFMAG "\177ELF\1\1"
|
||||
#define SELFMAG 6
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
|
||||
Elf32_Half e_type; /* Object file type */
|
||||
Elf32_Half e_machine; /* Architecture */
|
||||
Elf32_Word e_version; /* Object file version */
|
||||
Elf32_Addr e_entry; /* Entry point virtual address */
|
||||
Elf32_Off e_phoff; /* Program header table file offset */
|
||||
Elf32_Off e_shoff; /* Section header table file offset */
|
||||
Elf32_Word e_flags; /* Processor-specific flags */
|
||||
Elf32_Half e_ehsize; /* ELF header size in bytes */
|
||||
Elf32_Half e_phentsize; /* Program header table entry size */
|
||||
Elf32_Half e_phnum; /* Program header table entry count */
|
||||
Elf32_Half e_shentsize; /* Section header table entry size */
|
||||
Elf32_Half e_shnum; /* Section header table entry count */
|
||||
Elf32_Half e_shstrndx; /* Section header string table index */
|
||||
} Elf32_Ehdr;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Elf32_Word p_type; /* Segment type */
|
||||
Elf32_Off p_offset; /* Segment file offset */
|
||||
Elf32_Addr p_vaddr; /* Segment virtual address */
|
||||
Elf32_Addr p_paddr; /* Segment physical address */
|
||||
Elf32_Word p_filesz; /* Segment size in file */
|
||||
Elf32_Word p_memsz; /* Segment size in memory */
|
||||
Elf32_Word p_flags; /* Segment flags */
|
||||
Elf32_Word p_align; /* Segment alignment */
|
||||
} Elf32_Phdr;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Elf32_Word sh_name; /* Section name (string tbl index) */
|
||||
Elf32_Word sh_type; /* Section type */
|
||||
Elf32_Word sh_flags; /* Section flags */
|
||||
Elf32_Addr sh_addr; /* Section virtual addr at execution */
|
||||
Elf32_Off sh_offset; /* Section file offset */
|
||||
Elf32_Word sh_size; /* Section size in bytes */
|
||||
Elf32_Word sh_link; /* Link to another section */
|
||||
Elf32_Word sh_info; /* Additional section information */
|
||||
Elf32_Word sh_addralign; /* Section alignment */
|
||||
Elf32_Word sh_entsize; /* Entry size if section holds table */
|
||||
} Elf32_Shdr;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Elf32_Word st_name; /* Symbol name (string tbl index) */
|
||||
Elf32_Addr st_value; /* Symbol value */
|
||||
Elf32_Word st_size; /* Symbol size */
|
||||
unsigned char st_info; /* Symbol type and binding */
|
||||
unsigned char st_other; /* Symbol visibility */
|
||||
Elf32_Section st_shndx; /* Section index */
|
||||
} Elf32_Sym;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
Elf32_Addr r_offset; /* Address */
|
||||
Elf32_Word r_info; /* Relocation type and symbol index */
|
||||
Elf32_Sword r_addend; /* Addend */
|
||||
} Elf32_Rela;
|
||||
|
||||
|
||||
|
||||
extern "C" void flush_instruction_cache();
|
||||
|
||||
static void purge_copyback()
|
||||
{
|
||||
int i;
|
||||
for (i=0; i!=(1<<14); i+=(1<<5))
|
||||
*(volatile unsigned int *)(0xf4000000+i) &= ~3;
|
||||
}
|
||||
|
||||
|
||||
void DLObject::seterror(const char *fmt, ...)
|
||||
{
|
||||
if (errbuf) {
|
||||
va_list va;
|
||||
va_start(va, fmt);
|
||||
vsnprintf(errbuf, MAXDLERRLEN, fmt, va);
|
||||
va_end(va);
|
||||
}
|
||||
}
|
||||
|
||||
void DLObject::discard_symtab()
|
||||
{
|
||||
free(symtab);
|
||||
free(strtab);
|
||||
symtab = NULL;
|
||||
strtab = NULL;
|
||||
symbol_cnt = 0;
|
||||
}
|
||||
|
||||
void DLObject::unload()
|
||||
{
|
||||
discard_symtab();
|
||||
free(segment);
|
||||
segment = NULL;
|
||||
}
|
||||
|
||||
bool DLObject::relocate(int fd, unsigned long offset, unsigned long size)
|
||||
{
|
||||
Elf32_Rela *rela;
|
||||
|
||||
if (!(rela = (Elf32_Rela *)malloc(size))) {
|
||||
seterror("Out of memory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lseek(fd, offset, SEEK_SET)<0 ||
|
||||
read(fd, rela, size) != (ssize_t)size) {
|
||||
seterror("Relocation table load failed.");
|
||||
free(rela);
|
||||
return false;
|
||||
}
|
||||
|
||||
int cnt = size / sizeof(*rela);
|
||||
for (int i=0; i<cnt; i++) {
|
||||
|
||||
Elf32_Sym *sym = (Elf32_Sym *)(void *)(((char *)symtab)+(rela[i].r_info>>4));
|
||||
|
||||
void *target = ((char *)segment)+rela[i].r_offset;
|
||||
|
||||
switch(rela[i].r_info & 0xf) {
|
||||
case 1: /* DIR32 */
|
||||
if (sym->st_shndx < 0xff00)
|
||||
*(unsigned long *)target += (unsigned long)segment;
|
||||
break;
|
||||
default:
|
||||
seterror("Unknown relocation type %d.", rela[i].r_info & 0xf);
|
||||
free(rela);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
free(rela);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool DLObject::load(int fd)
|
||||
{
|
||||
Elf32_Ehdr ehdr;
|
||||
Elf32_Phdr phdr;
|
||||
Elf32_Shdr *shdr;
|
||||
int symtab_sect = -1;
|
||||
|
||||
if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr) ||
|
||||
memcmp(ehdr.e_ident, ELFMAG, SELFMAG) ||
|
||||
ehdr.e_type != 2 || ehdr.e_machine != 42 ||
|
||||
ehdr.e_phentsize < sizeof(phdr) || ehdr.e_shentsize != sizeof(*shdr) ||
|
||||
ehdr.e_phnum != 1) {
|
||||
seterror("Invalid file type.");
|
||||
return false;
|
||||
}
|
||||
|
||||
DBG(("phoff = %d, phentsz = %d, phnum = %d\n",
|
||||
ehdr.e_phoff, ehdr.e_phentsize, ehdr.e_phnum));
|
||||
|
||||
if (lseek(fd, ehdr.e_phoff, SEEK_SET)<0 ||
|
||||
read(fd, &phdr, sizeof(phdr)) != sizeof(phdr)) {
|
||||
seterror("Program header load failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (phdr.p_type != 1 || phdr.p_vaddr != 0 || phdr.p_paddr != 0 ||
|
||||
phdr.p_filesz > phdr.p_memsz) {
|
||||
seterror("Invalid program header.");
|
||||
return false;
|
||||
}
|
||||
|
||||
DBG(("offs = %d, filesz = %d, memsz = %d, align = %d\n",
|
||||
phdr.p_offset, phdr.p_filesz, phdr.p_memsz, phdr.p_align));
|
||||
|
||||
if (!(segment = memalign(phdr.p_align, phdr.p_memsz))) {
|
||||
seterror("Out of memory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
DBG(("segment @ %p\n", segment));
|
||||
|
||||
if (phdr.p_memsz > phdr.p_filesz)
|
||||
memset(((char *)segment) + phdr.p_filesz, 0, phdr.p_memsz - phdr.p_filesz);
|
||||
|
||||
if (lseek(fd, phdr.p_offset, SEEK_SET)<0 ||
|
||||
read(fd, segment, phdr.p_filesz) != (ssize_t)phdr.p_filesz) {
|
||||
seterror("Segment load failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
DBG(("shoff = %d, shentsz = %d, shnum = %d\n",
|
||||
ehdr.e_shoff, ehdr.e_shentsize, ehdr.e_shnum));
|
||||
|
||||
if (!(shdr = (Elf32_Shdr *)malloc(ehdr.e_shnum * sizeof(*shdr)))) {
|
||||
seterror("Out of memory.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lseek(fd, ehdr.e_shoff, SEEK_SET)<0 ||
|
||||
read(fd, shdr, ehdr.e_shnum * sizeof(*shdr)) !=
|
||||
(ssize_t)(ehdr.e_shnum * sizeof(*shdr))) {
|
||||
seterror("Section headers load failed.");
|
||||
free(shdr);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i=0; i<ehdr.e_shnum; i++) {
|
||||
DBG(("Section %d: type = %d, size = %d, entsize = %d, link = %d\n",
|
||||
i, shdr[i].sh_type, shdr[i].sh_size, shdr[i].sh_entsize, shdr[i].sh_link));
|
||||
if (shdr[i].sh_type == 2 && shdr[i].sh_entsize == sizeof(Elf32_Sym) &&
|
||||
shdr[i].sh_link < ehdr.e_shnum && shdr[shdr[i].sh_link].sh_type == 3 &&
|
||||
symtab_sect < 0)
|
||||
symtab_sect = i;
|
||||
}
|
||||
|
||||
if (symtab_sect < 0) {
|
||||
seterror("No symbol table.");
|
||||
free(shdr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(symtab = malloc(shdr[symtab_sect].sh_size))) {
|
||||
seterror("Out of memory.");
|
||||
free(shdr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lseek(fd, shdr[symtab_sect].sh_offset, SEEK_SET)<0 ||
|
||||
read(fd, symtab, shdr[symtab_sect].sh_size) !=
|
||||
(ssize_t)shdr[symtab_sect].sh_size){
|
||||
seterror("Symbol table load failed.");
|
||||
free(shdr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(strtab = (char *)malloc(shdr[shdr[symtab_sect].sh_link].sh_size))) {
|
||||
seterror("Out of memory.");
|
||||
free(shdr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (lseek(fd, shdr[shdr[symtab_sect].sh_link].sh_offset, SEEK_SET)<0 ||
|
||||
read(fd, strtab, shdr[shdr[symtab_sect].sh_link].sh_size) !=
|
||||
(ssize_t)shdr[shdr[symtab_sect].sh_link].sh_size){
|
||||
seterror("Symbol table strings load failed.");
|
||||
free(shdr);
|
||||
return false;
|
||||
}
|
||||
|
||||
symbol_cnt = shdr[symtab_sect].sh_size / sizeof(Elf32_Sym);
|
||||
DBG(("Loaded %d symbols.\n", symbol_cnt));
|
||||
|
||||
Elf32_Sym *s = (Elf32_Sym *)symtab;
|
||||
for (int c = symbol_cnt; c--; s++)
|
||||
if (s->st_shndx < 0xff00)
|
||||
s->st_value += (Elf32_Addr)segment;
|
||||
|
||||
for (int i=0; i<ehdr.e_shnum; i++)
|
||||
if (shdr[i].sh_type == 4 && shdr[i].sh_entsize == sizeof(Elf32_Rela) &&
|
||||
(int)shdr[i].sh_link == symtab_sect && shdr[i].sh_info < ehdr.e_shnum &&
|
||||
(shdr[shdr[i].sh_info].sh_flags & 2))
|
||||
if (!relocate(fd, shdr[i].sh_offset, shdr[i].sh_size)) {
|
||||
free(shdr);
|
||||
return false;
|
||||
}
|
||||
|
||||
free(shdr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DLObject::open(const char *path)
|
||||
{
|
||||
int fd;
|
||||
void *ctors_start, *ctors_end;
|
||||
|
||||
DBG(("open(\"%s\")\n", path));
|
||||
|
||||
if ((fd = ::open(path, O_RDONLY))<0) {
|
||||
seterror("%s not found.", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!load(fd)) {
|
||||
::close(fd);
|
||||
unload();
|
||||
return false;
|
||||
}
|
||||
|
||||
::close(fd);
|
||||
|
||||
int oldmask = getimask();
|
||||
setimask(15);
|
||||
purge_copyback();
|
||||
flush_instruction_cache();
|
||||
setimask(oldmask);
|
||||
|
||||
ctors_start = symbol("__plugin_ctors");
|
||||
ctors_end = symbol("__plugin_ctors_end");
|
||||
dtors_start = symbol("__plugin_dtors");
|
||||
dtors_end = symbol("__plugin_dtors_end");
|
||||
dso_handle = symbol("__dso_handle");
|
||||
|
||||
if (ctors_start == NULL || ctors_end == NULL || dtors_start == NULL ||
|
||||
dtors_end == NULL) {
|
||||
seterror("Missing ctors/dtors.");
|
||||
dtors_start = dtors_end = NULL;
|
||||
unload();
|
||||
return false;
|
||||
}
|
||||
|
||||
DBG(("Calling constructors.\n"));
|
||||
for (void (**f)(void) = (void (**)(void))ctors_start; f != ctors_end; f++)
|
||||
(**f)();
|
||||
|
||||
DBG(("%s opened ok.\n", path));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DLObject::close()
|
||||
{
|
||||
if (dso_handle != NULL) {
|
||||
__cxxabiv1::__cxa_finalize(dso_handle);
|
||||
dso_handle = NULL;
|
||||
}
|
||||
if (dtors_start != NULL && dtors_end != NULL)
|
||||
for (void (**f)(void) = (void (**)(void))dtors_start; f != dtors_end; f++)
|
||||
(**f)();
|
||||
dtors_start = dtors_end = NULL;
|
||||
unload();
|
||||
return true;
|
||||
}
|
||||
|
||||
void *DLObject::symbol(const char *name)
|
||||
{
|
||||
DBG(("symbol(\"%s\")\n", name));
|
||||
|
||||
if (symtab == NULL || strtab == NULL || symbol_cnt < 1) {
|
||||
seterror("No symbol table loaded.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Elf32_Sym *s = (Elf32_Sym *)symtab;
|
||||
for (int c = symbol_cnt; c--; s++)
|
||||
if ((s->st_info>>4 == 1 || s->st_info>>4 == 2) &&
|
||||
strtab[s->st_name] == '_' && !strcmp(name, strtab+s->st_name+1)) {
|
||||
DBG(("=> %p\n", (void *)s->st_value));
|
||||
return (void *)s->st_value;
|
||||
}
|
||||
|
||||
seterror("Symbol \"%s\" not found.", name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
static char dlerr[MAXDLERRLEN];
|
||||
|
||||
void *dlopen(const char *filename, int flags)
|
||||
{
|
||||
DLObject *obj = new DLObject(dlerr);
|
||||
if (obj->open(filename))
|
||||
return (void *)obj;
|
||||
delete obj;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int dlclose(void *handle)
|
||||
{
|
||||
DLObject *obj = (DLObject *)handle;
|
||||
if (obj == NULL) {
|
||||
Common::strcpy_s(dlerr, "Handle is NULL.");
|
||||
return -1;
|
||||
}
|
||||
if (obj->close()) {
|
||||
delete obj;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void *dlsym(void *handle, const char *symbol)
|
||||
{
|
||||
if (handle == NULL) {
|
||||
Common::strcpy_s(dlerr, "Handle is NULL.");
|
||||
return NULL;
|
||||
}
|
||||
return ((DLObject *)handle)->symbol(symbol);
|
||||
}
|
||||
|
||||
const char *dlerror()
|
||||
{
|
||||
return dlerr;
|
||||
}
|
||||
|
||||
void dlforgetsyms(void *handle)
|
||||
{
|
||||
if (handle != NULL)
|
||||
((DLObject *)handle)->discard_symtab();
|
||||
}
|
||||
63
backends/platform/dc/dcloader.h
Normal file
63
backends/platform/dc/dcloader.h
Normal file
@@ -0,0 +1,63 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DC_DCLOADER_H
|
||||
#define DC_DCLOADER_H
|
||||
|
||||
#include "dc.h"
|
||||
|
||||
#define MAXDLERRLEN 80
|
||||
|
||||
class DLObject {
|
||||
private:
|
||||
char *errbuf; /* For error messages, at least MAXDLERRLEN in size */
|
||||
|
||||
void *segment, *symtab;
|
||||
char *strtab;
|
||||
int symbol_cnt;
|
||||
void *dtors_start, *dtors_end, *dso_handle;
|
||||
|
||||
void seterror(const char *fmt, ...);
|
||||
void unload();
|
||||
bool relocate(int fd, unsigned long offset, unsigned long size);
|
||||
bool load(int fd);
|
||||
|
||||
public:
|
||||
bool open(const char *path);
|
||||
bool close();
|
||||
void *symbol(const char *name);
|
||||
void discard_symtab();
|
||||
|
||||
DLObject(char *_errbuf = NULL) : errbuf(_errbuf), segment(NULL),symtab(NULL),
|
||||
strtab(NULL), symbol_cnt(0), dtors_start(NULL), dtors_end(NULL) {}
|
||||
};
|
||||
|
||||
#define RTLD_LAZY 0
|
||||
|
||||
extern "C" {
|
||||
void *dlopen(const char *filename, int flags);
|
||||
int dlclose(void *handle);
|
||||
void *dlsym(void *handle, const char *symbol);
|
||||
const char *dlerror();
|
||||
void dlforgetsyms(void *handle);
|
||||
}
|
||||
|
||||
#endif
|
||||
315
backends/platform/dc/dcmain.cpp
Normal file
315
backends/platform/dc/dcmain.cpp
Normal file
@@ -0,0 +1,315 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#define FORBIDDEN_SYMBOL_ALLOW_ALL
|
||||
|
||||
#include <common/scummsys.h>
|
||||
#include <engines/engine.h>
|
||||
#include <base/main.h>
|
||||
#include <base/plugins.h>
|
||||
#include "dc.h"
|
||||
#include "dcutils.h"
|
||||
#include "icon.h"
|
||||
#include "DCLauncherDialog.h"
|
||||
#include "backends/mutex/null/null-mutex.h"
|
||||
#include <common/config-manager.h>
|
||||
#include <common/memstream.h>
|
||||
#include <common/endian.h>
|
||||
|
||||
#include "audio/mixer_intern.h"
|
||||
|
||||
|
||||
Icon icon;
|
||||
char gGameName[32];
|
||||
|
||||
|
||||
OSystem_Dreamcast::OSystem_Dreamcast()
|
||||
: _devpoll(0), screen(NULL), mouse(NULL), overlay(NULL), _softkbd(this),
|
||||
_ms_buf(NULL), _mixer(NULL),
|
||||
_current_shake_x_pos(0), _current_shake_y_pos(0), _aspect_stretch(false), _softkbd_on(false),
|
||||
_softkbd_motion(0), _enable_cursor_palette(false), _overlay_in_gui(false), _screenFormat(0)
|
||||
{
|
||||
memset(screen_tx, 0, sizeof(screen_tx));
|
||||
memset(mouse_tx, 0, sizeof(mouse_tx));
|
||||
memset(ovl_tx, 0, sizeof(ovl_tx));
|
||||
_fsFactory = this;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::initBackend()
|
||||
{
|
||||
ConfMan.setInt("autosave_period", 0);
|
||||
_savefileManager = createSavefileManager();
|
||||
_timerManager = new DefaultTimerManager();
|
||||
|
||||
uint sampleRate = initSound();
|
||||
_mixer = new Audio::MixerImpl(sampleRate);
|
||||
_mixer->setReady(true);
|
||||
|
||||
_audiocdManager = new DCCDManager();
|
||||
|
||||
EventsBaseBackend::initBackend();
|
||||
}
|
||||
|
||||
|
||||
/* CD Audio */
|
||||
static bool find_track(int track, int &first_sec, int &last_sec)
|
||||
{
|
||||
struct TOC *toc = cdfs_gettoc();
|
||||
if (!toc)
|
||||
return false;
|
||||
int i, first, last;
|
||||
first = TOC_TRACK(toc->first);
|
||||
last = TOC_TRACK(toc->last);
|
||||
if (first < 1 || last > 99 || first > last)
|
||||
return false;
|
||||
for (i=first; i<=last; i++)
|
||||
if (!(TOC_CTRL(toc->entry[i-1])&4)) {
|
||||
if (track==1) {
|
||||
first_sec = TOC_LBA(toc->entry[i-1]);
|
||||
last_sec = TOC_LBA(toc->entry[i]);
|
||||
return true;
|
||||
} else
|
||||
--track;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DCCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate,
|
||||
Audio::Mixer::SoundType soundType) {
|
||||
// Prefer emulation
|
||||
if (DefaultAudioCDManager::play(track, numLoops, startFrame, duration, onlyEmulate, soundType))
|
||||
return true;
|
||||
|
||||
// If we're playing now return here
|
||||
if (isPlaying()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we should only play emulated tracks stop here.
|
||||
if (onlyEmulate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int firstSec, lastSec;
|
||||
#if 1
|
||||
if (numLoops)
|
||||
--numLoops;
|
||||
#endif
|
||||
|
||||
if (numLoops > 14)
|
||||
numLoops = 14;
|
||||
else if (numLoops < 0)
|
||||
numLoops = 15; // infinity
|
||||
|
||||
if (!find_track(track, firstSec, lastSec))
|
||||
return false;
|
||||
|
||||
if (duration)
|
||||
lastSec = firstSec + startFrame + duration;
|
||||
|
||||
firstSec += startFrame;
|
||||
play_cdda_sectors(firstSec, lastSec, numLoops);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DCCDManager::stop() {
|
||||
DefaultAudioCDManager::stop();
|
||||
stop_cdda();
|
||||
}
|
||||
|
||||
bool DCCDManager::isPlaying() const {
|
||||
if (DefaultAudioCDManager::isPlaying())
|
||||
return true;
|
||||
|
||||
return getCdState() == 3;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::setWindowCaption(const Common::U32String &caption)
|
||||
{
|
||||
Common::strlcpy(gGameName, caption.encode(Common::kISO8859_1).c_str(), 32);
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::quit() {
|
||||
(*(void(**)(int))0x8c0000e0)(0);
|
||||
}
|
||||
|
||||
/* Mutex handling */
|
||||
Common::MutexInternal *OSystem_Dreamcast::createMutex()
|
||||
{
|
||||
return new NullMutexInternal();
|
||||
}
|
||||
|
||||
/* Features */
|
||||
bool OSystem_Dreamcast::hasFeature(Feature f)
|
||||
{
|
||||
switch(f) {
|
||||
case kFeatureAspectRatioCorrection:
|
||||
case kFeatureVirtualKeyboard:
|
||||
case kFeatureOverlaySupportsAlpha:
|
||||
case kFeatureCursorPalette:
|
||||
case kFeatureCursorAlpha:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::setFeatureState(Feature f, bool enable)
|
||||
{
|
||||
switch(f) {
|
||||
case kFeatureAspectRatioCorrection:
|
||||
_aspect_stretch = enable;
|
||||
if (screen)
|
||||
setScaling();
|
||||
break;
|
||||
case kFeatureVirtualKeyboard:
|
||||
_softkbd_on = enable;
|
||||
break;
|
||||
case kFeatureCursorPalette:
|
||||
_enable_cursor_palette = enable;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool OSystem_Dreamcast::getFeatureState(Feature f)
|
||||
{
|
||||
switch(f) {
|
||||
case kFeatureAspectRatioCorrection:
|
||||
return _aspect_stretch;
|
||||
case kFeatureVirtualKeyboard:
|
||||
return _softkbd_on;
|
||||
case kFeatureCursorPalette:
|
||||
return _enable_cursor_palette;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::getTimeAndDate(TimeDate &td, bool skipRecord) const {
|
||||
time_t curTime;
|
||||
time(&curTime);
|
||||
struct tm t = *localtime(&curTime);
|
||||
td.tm_sec = t.tm_sec;
|
||||
td.tm_min = t.tm_min;
|
||||
td.tm_hour = t.tm_hour;
|
||||
td.tm_mday = t.tm_mday;
|
||||
td.tm_mon = t.tm_mon;
|
||||
td.tm_year = t.tm_year;
|
||||
td.tm_wday = t.tm_wday;
|
||||
}
|
||||
|
||||
Common::SeekableReadStream *OSystem_Dreamcast::createConfigReadStream() {
|
||||
Common::FSNode file("/scummvm.ini");
|
||||
Common::SeekableReadStream *s = file.createReadStream();
|
||||
return s? s : new Common::MemoryReadStream((const byte *)"", 0);
|
||||
}
|
||||
|
||||
Common::WriteStream *OSystem_Dreamcast::createConfigWriteStream() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::logMessage(LogMessageType::Type type, const char *message) {
|
||||
#ifndef NOSERIAL
|
||||
report(message);
|
||||
#endif
|
||||
}
|
||||
|
||||
Common::String OSystem_Dreamcast::getSystemLanguage() const {
|
||||
static const char *languages[] = {
|
||||
"ja_JP",
|
||||
"en_US",
|
||||
"de_DE",
|
||||
"fr_FR",
|
||||
"es_ES",
|
||||
"it_IT"
|
||||
};
|
||||
int l = DC_Flash::get_locale_setting();
|
||||
if (l<0 || ((unsigned)l)>=sizeof(languages)/sizeof(languages[0]))
|
||||
l = 1;
|
||||
return Common::String(languages[l]);
|
||||
}
|
||||
|
||||
|
||||
void DCHardware::dc_init_hardware()
|
||||
{
|
||||
#ifndef NOSERIAL
|
||||
serial_init(57600);
|
||||
usleep(2000000);
|
||||
printf("Serial OK\r\n");
|
||||
#endif
|
||||
|
||||
cdfs_init();
|
||||
maple_init();
|
||||
dc_setup_ta();
|
||||
init_arm();
|
||||
}
|
||||
|
||||
static OSystem_Dreamcast osys_dc;
|
||||
|
||||
int main()
|
||||
{
|
||||
static const char *argv[] = { "scummvm", NULL, };
|
||||
static int argc = 1;
|
||||
|
||||
g_system = &osys_dc;
|
||||
|
||||
#ifdef DYNAMIC_MODULES
|
||||
PluginManager::instance().addPluginProvider(&osys_dc);
|
||||
#endif
|
||||
|
||||
scummvm_main(argc, argv);
|
||||
|
||||
g_system->quit();
|
||||
}
|
||||
|
||||
int DCLauncherDialog::runModal()
|
||||
{
|
||||
char *engineId = NULL, *gameId = NULL, *dir = NULL;
|
||||
Common::Language language = Common::UNK_LANG;
|
||||
Common::Platform platform = Common::kPlatformUnknown;
|
||||
|
||||
if (!selectGame(engineId, gameId, dir, language, platform, icon))
|
||||
g_system->quit();
|
||||
|
||||
// Set the game path.
|
||||
ConfMan.addGameDomain(gameId);
|
||||
ConfMan.set("engineid", engineId, gameId);
|
||||
ConfMan.set("gameid", gameId, gameId);
|
||||
|
||||
if (dir != NULL)
|
||||
ConfMan.setPath("path", dir, gameId);
|
||||
|
||||
// Set the game language.
|
||||
if (language != Common::UNK_LANG)
|
||||
ConfMan.set("language", Common::getLanguageCode(language), gameId);
|
||||
|
||||
// Set the game platform.
|
||||
if (platform != Common::kPlatformUnknown)
|
||||
ConfMan.set("platform", Common::getPlatformCode(platform), gameId);
|
||||
|
||||
// Set the target.
|
||||
ConfMan.setActiveDomain(gameId);
|
||||
|
||||
return 0;
|
||||
}
|
||||
250
backends/platform/dc/dcutils.cpp
Normal file
250
backends/platform/dc/dcutils.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#define FORBIDDEN_SYMBOL_EXCEPTION_chdir
|
||||
|
||||
#include "dc.h"
|
||||
#include "dcutils.h"
|
||||
#include <ronin/gddrive.h>
|
||||
|
||||
|
||||
int getCdState()
|
||||
{
|
||||
unsigned int param[4];
|
||||
gdGdcGetDrvStat(param);
|
||||
return param[0];
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
int dummy_cdfs_get_volume_id(char *, unsigned int) {
|
||||
return -1;
|
||||
}
|
||||
int cdfs_get_volume_id(char *, unsigned int) __attribute__ ((weak, alias ("dummy_cdfs_get_volume_id")));
|
||||
}
|
||||
|
||||
DiscLabel::DiscLabel() {
|
||||
if (cdfs_get_volume_id(buf, 32) < 0)
|
||||
memset(buf, '*', 32);
|
||||
}
|
||||
|
||||
bool DiscLabel::operator==(const DiscLabel &other) const {
|
||||
return !memcmp(buf, other.buf, 32);
|
||||
}
|
||||
|
||||
void DiscLabel::get(char *p) const {
|
||||
memcpy(p, buf, 32);
|
||||
p[32] = 0;
|
||||
}
|
||||
|
||||
|
||||
void draw_solid_quad(float x1, float y1, float x2, float y2,
|
||||
int c0, int c1, int c2, int c3)
|
||||
{
|
||||
struct polygon_list mypoly;
|
||||
struct packed_colour_vertex_list myvertex;
|
||||
|
||||
mypoly.cmd =
|
||||
TA_CMD_POLYGON|TA_CMD_POLYGON_TYPE_OPAQUE|TA_CMD_POLYGON_SUBLIST|
|
||||
TA_CMD_POLYGON_STRIPLENGTH_2|TA_CMD_POLYGON_PACKED_COLOUR|
|
||||
TA_CMD_POLYGON_GOURAUD_SHADING;
|
||||
mypoly.mode1 = TA_POLYMODE1_Z_ALWAYS|TA_POLYMODE1_NO_Z_UPDATE;
|
||||
mypoly.mode2 =
|
||||
TA_POLYMODE2_BLEND_SRC|TA_POLYMODE2_FOG_DISABLED;
|
||||
mypoly.texture = 0;
|
||||
|
||||
mypoly.red = mypoly.green = mypoly.blue = mypoly.alpha = 0;
|
||||
|
||||
ta_commit_list(&mypoly);
|
||||
|
||||
myvertex.cmd = TA_CMD_VERTEX;
|
||||
myvertex.ocolour = 0;
|
||||
myvertex.z = 0.5;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = 0.0;
|
||||
|
||||
myvertex.colour = c0;
|
||||
myvertex.x = x1;
|
||||
myvertex.y = y1;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.colour = c1;
|
||||
myvertex.x = x2;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.colour = c2;
|
||||
myvertex.x = x1;
|
||||
myvertex.y = y2;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.colour = c3;
|
||||
myvertex.x = x2;
|
||||
myvertex.cmd |= TA_CMD_VERTEX_EOS;
|
||||
ta_commit_list(&myvertex);
|
||||
}
|
||||
|
||||
void draw_trans_quad(float x1, float y1, float x2, float y2,
|
||||
int c0, int c1, int c2, int c3)
|
||||
{
|
||||
struct polygon_list mypoly;
|
||||
struct packed_colour_vertex_list myvertex;
|
||||
|
||||
mypoly.cmd =
|
||||
TA_CMD_POLYGON|TA_CMD_POLYGON_TYPE_TRANSPARENT|TA_CMD_POLYGON_SUBLIST|
|
||||
TA_CMD_POLYGON_STRIPLENGTH_2|TA_CMD_POLYGON_PACKED_COLOUR|
|
||||
TA_CMD_POLYGON_GOURAUD_SHADING;
|
||||
mypoly.mode1 = TA_POLYMODE1_Z_ALWAYS|TA_POLYMODE1_NO_Z_UPDATE;
|
||||
mypoly.mode2 =
|
||||
TA_POLYMODE2_BLEND_SRC_ALPHA|TA_POLYMODE2_BLEND_DST_INVALPHA|
|
||||
TA_POLYMODE2_FOG_DISABLED|TA_POLYMODE2_ENABLE_ALPHA;
|
||||
mypoly.texture = 0;
|
||||
|
||||
mypoly.red = mypoly.green = mypoly.blue = mypoly.alpha = 0;
|
||||
|
||||
ta_commit_list(&mypoly);
|
||||
|
||||
myvertex.cmd = TA_CMD_VERTEX;
|
||||
myvertex.ocolour = 0;
|
||||
myvertex.z = 0.5;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = 0.0;
|
||||
|
||||
myvertex.colour = c0;
|
||||
myvertex.x = x1;
|
||||
myvertex.y = y1;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.colour = c1;
|
||||
myvertex.x = x2;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.colour = c2;
|
||||
myvertex.x = x1;
|
||||
myvertex.y = y2;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.colour = c3;
|
||||
myvertex.x = x2;
|
||||
myvertex.cmd |= TA_CMD_VERTEX_EOS;
|
||||
ta_commit_list(&myvertex);
|
||||
}
|
||||
|
||||
DiscSwap::DiscSwap(const char *label, unsigned int argb_) : argb(argb_) {
|
||||
x = 320 - 7 * strlen(label);
|
||||
lab.create_texture(label);
|
||||
}
|
||||
|
||||
void DiscSwap::run()
|
||||
{
|
||||
int wasopen = 0;
|
||||
for (;;) {
|
||||
int s = getCdState();
|
||||
if (s >= 6)
|
||||
wasopen = 1;
|
||||
if (s > 0 && s < 6 && wasopen) {
|
||||
cdfs_reinit();
|
||||
chdir("/"); // Expect this one to fail with ERR_DISKCHG
|
||||
chdir("/"); // but this one to succeed
|
||||
return;
|
||||
}
|
||||
|
||||
ta_begin_frame();
|
||||
background();
|
||||
ta_commit_end();
|
||||
lab.draw(x, 200.0, argb);
|
||||
ta_commit_frame();
|
||||
|
||||
interact();
|
||||
}
|
||||
}
|
||||
|
||||
namespace DC_Flash {
|
||||
|
||||
static int syscall_info_flash(int sect, int *info)
|
||||
{
|
||||
return (*(int (**)(int, void*, int, int))0x8c0000b8)(sect,info,0,0);
|
||||
}
|
||||
|
||||
static int syscall_read_flash(int offs, void *buf, int cnt)
|
||||
{
|
||||
return (*(int (**)(int, void*, int, int))0x8c0000b8)(offs,buf,cnt,1);
|
||||
}
|
||||
|
||||
static int flash_crc(const char *buf, int size)
|
||||
{
|
||||
int i, c, n = -1;
|
||||
for(i=0; i<size; i++) {
|
||||
n ^= (buf[i]<<8);
|
||||
for(c=0; c<8; c++)
|
||||
if(n & 0x8000)
|
||||
n = (n << 1) ^ 4129;
|
||||
else
|
||||
n <<= 1;
|
||||
}
|
||||
return (unsigned short)~n;
|
||||
}
|
||||
|
||||
int flash_read_sector(int partition, int sec, unsigned char *dst)
|
||||
{
|
||||
int s, r, n, b, bmb, got=0;
|
||||
int info[2];
|
||||
char buf[64];
|
||||
char bm[64];
|
||||
|
||||
if((r = syscall_info_flash(partition, info))<0)
|
||||
return r;
|
||||
|
||||
if((r = syscall_read_flash(info[0], buf, 64))<0)
|
||||
return r;
|
||||
|
||||
if(memcmp(buf, "KATANA_FLASH", 12) ||
|
||||
buf[16] != partition || buf[17] != 0)
|
||||
return -2;
|
||||
|
||||
n = (info[1]>>6)-1-((info[1] + 0x7fff)>>15);
|
||||
bmb = n+1;
|
||||
for(b = 0; b < n; b++) {
|
||||
if(!(b&511)) {
|
||||
if((r = syscall_read_flash(info[0] + (bmb++ << 6), bm, 64))<0)
|
||||
return r;
|
||||
}
|
||||
if(!(bm[(b>>3)&63] & (0x80>>(b&7)))) {
|
||||
if((r = syscall_read_flash(info[0] + ((b+1) << 6), buf, 64))<0)
|
||||
return r;
|
||||
else if((s=READ_LE_UINT16(buf+0)) == sec &&
|
||||
flash_crc(buf, 62) == READ_LE_UINT16(buf+62)) {
|
||||
memcpy(dst+(s-sec)*60, buf+2, 60);
|
||||
got=1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return got;
|
||||
}
|
||||
|
||||
int get_locale_setting()
|
||||
{
|
||||
unsigned char data[60];
|
||||
if (flash_read_sector(2,5,data) == 1)
|
||||
return data[5];
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // End of namespace DC_Flash
|
||||
75
backends/platform/dc/dcutils.h
Normal file
75
backends/platform/dc/dcutils.h
Normal file
@@ -0,0 +1,75 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DC_DCUTILS_H
|
||||
#define DC_DCUTILS_H
|
||||
|
||||
extern int getCdState();
|
||||
|
||||
class TextureStack {
|
||||
private:
|
||||
void *mark;
|
||||
public:
|
||||
TextureStack() {
|
||||
ta_sync();
|
||||
mark = ta_txmark();
|
||||
}
|
||||
~TextureStack() {
|
||||
ta_sync();
|
||||
ta_txrelease(mark);
|
||||
}
|
||||
};
|
||||
|
||||
class DiscLabel {
|
||||
private:
|
||||
char buf[32];
|
||||
public:
|
||||
DiscLabel();
|
||||
bool operator==(const DiscLabel &other) const;
|
||||
void get(char *p) const;
|
||||
};
|
||||
|
||||
class DiscSwap : TextureStack {
|
||||
private:
|
||||
unsigned int argb;
|
||||
float x;
|
||||
Label lab;
|
||||
protected:
|
||||
virtual void background() {}
|
||||
virtual void interact() {}
|
||||
public:
|
||||
DiscSwap(const char *label, unsigned int argb);
|
||||
virtual ~DiscSwap() {}
|
||||
void run();
|
||||
};
|
||||
|
||||
extern void draw_solid_quad(float x1, float y1, float x2, float y2,
|
||||
int c0, int c1, int c2, int c3);
|
||||
extern void draw_trans_quad(float x1, float y1, float x2, float y2,
|
||||
int c0, int c1, int c2, int c3);
|
||||
|
||||
namespace DC_Flash {
|
||||
int flash_read_sector(int partition, int sec, unsigned char *dst);
|
||||
int get_locale_setting();
|
||||
} // End of namespace DC_Flash
|
||||
|
||||
|
||||
#endif /* DC_DCUTILS_H */
|
||||
66
backends/platform/dc/deficon.h
Normal file
66
backends/platform/dc/deficon.h
Normal file
@@ -0,0 +1,66 @@
|
||||
static const unsigned char scummvm_icon[] = {
|
||||
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x20, 0x20, 0x10, 0x00, 0x01, 0x00,
|
||||
0x04, 0x00, 0xe8, 0x02, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x28, 0x00,
|
||||
0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x01, 0x00,
|
||||
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x12, 0x0b,
|
||||
0x00, 0x00, 0x12, 0x0b, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x00,
|
||||
0x00, 0x00, 0x33, 0x33, 0x33, 0x00, 0x33, 0x55, 0x22, 0x00, 0x33, 0x33,
|
||||
0x33, 0x00, 0x33, 0x99, 0x00, 0x00, 0x33, 0xcc, 0x00, 0x00, 0x33, 0x33,
|
||||
0x33, 0x00, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
0x65, 0x52, 0x52, 0x56, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x22, 0x00, 0x00, 0x00, 0x00, 0x22, 0x56, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x20, 0x00, 0x00, 0x00, 0x10, 0x01,
|
||||
0x00, 0x02, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x62, 0x00, 0x01,
|
||||
0x11, 0x31, 0x11, 0x11, 0x10, 0x00, 0x25, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x00, 0x01, 0x13, 0x34, 0x44, 0x44, 0x43, 0x31, 0x00, 0x00, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x60, 0x00, 0x11, 0x33, 0x44, 0x44, 0x44, 0x44,
|
||||
0x43, 0x31, 0x00, 0x26, 0x66, 0x66, 0x66, 0x66, 0x20, 0x01, 0x34, 0x44,
|
||||
0x44, 0x44, 0x44, 0x44, 0x44, 0x43, 0x00, 0x05, 0x66, 0x66, 0x66, 0x66,
|
||||
0x00, 0x13, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x43, 0x30, 0x02,
|
||||
0x66, 0x66, 0x66, 0x66, 0x00, 0x14, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
|
||||
0x44, 0x44, 0x41, 0x00, 0x66, 0x66, 0x66, 0x66, 0x20, 0x14, 0x44, 0x44,
|
||||
0x43, 0x31, 0x13, 0x44, 0x44, 0x44, 0x41, 0x00, 0x66, 0x66, 0x66, 0x66,
|
||||
0x50, 0x01, 0x44, 0x43, 0x10, 0x00, 0x03, 0x44, 0x44, 0x44, 0x41, 0x00,
|
||||
0x66, 0x66, 0x66, 0x66, 0x65, 0x00, 0x14, 0x30, 0x00, 0x00, 0x13, 0x44,
|
||||
0x44, 0x44, 0x43, 0x00, 0x56, 0x66, 0x66, 0x66, 0x66, 0x50, 0x01, 0x10,
|
||||
0x00, 0x03, 0x34, 0x44, 0x44, 0x44, 0x43, 0x00, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x65, 0x00, 0x00, 0x11, 0x34, 0x44, 0x44, 0x44, 0x44, 0x41, 0x02,
|
||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x01, 0x13, 0x44, 0x44, 0x44,
|
||||
0x44, 0x44, 0x40, 0x05, 0x66, 0x66, 0x66, 0x66, 0x66, 0x62, 0x00, 0x13,
|
||||
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x30, 0x06, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x50, 0x01, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x43, 0x00, 0x56,
|
||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x13, 0x44, 0x44, 0x44, 0x44, 0x44,
|
||||
0x44, 0x40, 0x02, 0x66, 0x66, 0x66, 0x66, 0x66, 0x65, 0x00, 0x34, 0x44,
|
||||
0x44, 0x44, 0x44, 0x44, 0x43, 0x00, 0x26, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
0x62, 0x01, 0x44, 0x44, 0x44, 0x44, 0x44, 0x43, 0x10, 0x00, 0x05, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x60, 0x01, 0x44, 0x44, 0x44, 0x44, 0x43, 0x10,
|
||||
0x01, 0x10, 0x02, 0x66, 0x66, 0x66, 0x66, 0x66, 0x60, 0x01, 0x44, 0x44,
|
||||
0x44, 0x44, 0x30, 0x00, 0x01, 0x31, 0x00, 0x26, 0x66, 0x66, 0x66, 0x66,
|
||||
0x60, 0x01, 0x44, 0x44, 0x44, 0x43, 0x00, 0x01, 0x14, 0x44, 0x10, 0x05,
|
||||
0x66, 0x66, 0x66, 0x66, 0x60, 0x01, 0x44, 0x44, 0x44, 0x43, 0x31, 0x34,
|
||||
0x44, 0x44, 0x30, 0x02, 0x66, 0x66, 0x66, 0x66, 0x62, 0x01, 0x44, 0x44,
|
||||
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x41, 0x00, 0x66, 0x66, 0x66, 0x66,
|
||||
0x65, 0x00, 0x14, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x40, 0x02,
|
||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x20, 0x03, 0x44, 0x44, 0x44, 0x44, 0x44,
|
||||
0x44, 0x43, 0x00, 0x06, 0x66, 0x66, 0x66, 0x66, 0x66, 0x50, 0x01, 0x34,
|
||||
0x44, 0x44, 0x44, 0x44, 0x43, 0x00, 0x02, 0x56, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x65, 0x00, 0x01, 0x34, 0x44, 0x44, 0x33, 0x10, 0x00, 0x26, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x05, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x52,
|
||||
0x00, 0x00, 0x00, 0x00, 0x25, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x65, 0x55, 0x55, 0x56, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x66, 0xff, 0xf8, 0x1f, 0xff, 0xff, 0xc0, 0x01, 0xff, 0xff, 0x00,
|
||||
0x00, 0xff, 0xfe, 0x00, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x3f, 0xf8, 0x00,
|
||||
0x00, 0x1f, 0xf0, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x0f, 0xf0, 0x00,
|
||||
0x00, 0x0f, 0xf0, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x0f, 0xf8, 0x00,
|
||||
0x00, 0x07, 0xfc, 0x00, 0x00, 0x0f, 0xfe, 0x00, 0x00, 0x0f, 0xff, 0x00,
|
||||
0x00, 0x0f, 0xfe, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x1f, 0xfc, 0x00,
|
||||
0x00, 0x3f, 0xf8, 0x00, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x3f, 0xf8, 0x00,
|
||||
0x00, 0x3f, 0xf8, 0x00, 0x00, 0x1f, 0xf8, 0x00, 0x00, 0x0f, 0xf8, 0x00,
|
||||
0x00, 0x0f, 0xf8, 0x00, 0x00, 0x0f, 0xf8, 0x00, 0x00, 0x0f, 0xfc, 0x00,
|
||||
0x00, 0x1f, 0xfc, 0x00, 0x00, 0x1f, 0xfe, 0x00, 0x00, 0x7f, 0xff, 0x00,
|
||||
0x00, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xf8, 0x1f, 0xff
|
||||
};
|
||||
713
backends/platform/dc/display.cpp
Normal file
713
backends/platform/dc/display.cpp
Normal file
@@ -0,0 +1,713 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#define RONIN_TIMER_ACCESS
|
||||
|
||||
#include "common/scummsys.h"
|
||||
#include "graphics/blit.h"
|
||||
#include "graphics/surface.h"
|
||||
#include "dc.h"
|
||||
|
||||
#define SCREEN_W 640
|
||||
#define SCREEN_H 480
|
||||
#define MOUSE_W 128
|
||||
#define MOUSE_H 128
|
||||
|
||||
#define OVL_W 320
|
||||
#define OVL_H 200
|
||||
#define OVL_TXSTRIDE 512
|
||||
|
||||
#define LEFT_OFFSET (_xscale*_current_shake_x_pos)
|
||||
#define TOP_OFFSET (_top_offset+_yscale*_current_shake_y_pos)
|
||||
|
||||
static const struct {
|
||||
Graphics::PixelFormat pixelFormat;
|
||||
unsigned int textureFormat;
|
||||
operator const Graphics::PixelFormat&() const { return pixelFormat; }
|
||||
} screenFormats[] = {
|
||||
/* Note: These are ordered by _increasing_ preference, so that
|
||||
CLUT8 appears at index 0. getSupportedFormats() will return
|
||||
them in reversed order. */
|
||||
{ Graphics::PixelFormat::createFormatCLUT8(), TA_TEXTUREMODE_ARGB1555 },
|
||||
{ Graphics::PixelFormat(2,4,4,4,4,8,4,0,12), TA_TEXTUREMODE_ARGB4444 },
|
||||
{ Graphics::PixelFormat(2,5,5,5,1,10,5,0,15), TA_TEXTUREMODE_ARGB1555 },
|
||||
{ Graphics::PixelFormat(2,5,6,5,0,11,5,0,0), TA_TEXTUREMODE_RGB565 },
|
||||
};
|
||||
#define NUM_FORMATS (sizeof(screenFormats)/sizeof(screenFormats[0]))
|
||||
|
||||
|
||||
#define QACR0 (*(volatile unsigned int *)(void *)0xff000038)
|
||||
#define QACR1 (*(volatile unsigned int *)(void *)0xff00003c)
|
||||
|
||||
|
||||
#define COPYPIXEL(n) do { \
|
||||
unsigned short _tmp = pal[*s++]; \
|
||||
d[n] = _tmp|(pal[*s++]<<16); \
|
||||
} while (0)
|
||||
|
||||
static void texture_memcpy64_pal(void *dest, void *src, int cnt, unsigned short *pal)
|
||||
{
|
||||
unsigned char *s = (unsigned char *)src;
|
||||
unsigned int *d = (unsigned int *)(void *)
|
||||
(0xe0000000 | (((unsigned long)dest) & 0x03ffffc0));
|
||||
QACR0 = ((0xa4000000>>26)<<2)&0x1c;
|
||||
QACR1 = ((0xa4000000>>26)<<2)&0x1c;
|
||||
while (cnt--) {
|
||||
COPYPIXEL(0);
|
||||
COPYPIXEL(1);
|
||||
COPYPIXEL(2);
|
||||
COPYPIXEL(3);
|
||||
asm("pref @%0" : : "r" (s+4*16));
|
||||
COPYPIXEL(4);
|
||||
COPYPIXEL(5);
|
||||
COPYPIXEL(6);
|
||||
COPYPIXEL(7);
|
||||
asm("pref @%0" : : "r" (d));
|
||||
d += 8;
|
||||
COPYPIXEL(0);
|
||||
COPYPIXEL(1);
|
||||
COPYPIXEL(2);
|
||||
COPYPIXEL(3);
|
||||
asm("pref @%0" : : "r" (s+4*16));
|
||||
COPYPIXEL(4);
|
||||
COPYPIXEL(5);
|
||||
COPYPIXEL(6);
|
||||
COPYPIXEL(7);
|
||||
asm("pref @%0" : : "r" (d));
|
||||
d += 8;
|
||||
}
|
||||
}
|
||||
|
||||
static void texture_memcpy64(void *dest, void *src, int cnt)
|
||||
{
|
||||
unsigned int *s = (unsigned int *)src;
|
||||
unsigned int *d = (unsigned int *)(void *)
|
||||
(0xe0000000 | (((unsigned long)dest) & 0x03ffffc0));
|
||||
QACR0 = ((0xa4000000>>26)<<2)&0x1c;
|
||||
QACR1 = ((0xa4000000>>26)<<2)&0x1c;
|
||||
while (cnt--) {
|
||||
d[0] = *s++;
|
||||
d[1] = *s++;
|
||||
d[2] = *s++;
|
||||
d[3] = *s++;
|
||||
asm("pref @%0" : : "r" (s+16));
|
||||
d[4] = *s++;
|
||||
d[5] = *s++;
|
||||
d[6] = *s++;
|
||||
d[7] = *s++;
|
||||
asm("pref @%0" : : "r" (d));
|
||||
d += 8;
|
||||
d[0] = *s++;
|
||||
d[1] = *s++;
|
||||
d[2] = *s++;
|
||||
d[3] = *s++;
|
||||
asm("pref @%0" : : "r" (s+16));
|
||||
d[4] = *s++;
|
||||
d[5] = *s++;
|
||||
d[6] = *s++;
|
||||
d[7] = *s++;
|
||||
asm("pref @%0" : : "r" (d));
|
||||
d += 8;
|
||||
}
|
||||
}
|
||||
|
||||
void commit_dummy_transpoly()
|
||||
{
|
||||
struct polygon_list mypoly;
|
||||
|
||||
mypoly.cmd =
|
||||
TA_CMD_POLYGON|TA_CMD_POLYGON_TYPE_TRANSPARENT|TA_CMD_POLYGON_SUBLIST|
|
||||
TA_CMD_POLYGON_STRIPLENGTH_2|TA_CMD_POLYGON_PACKED_COLOUR;
|
||||
mypoly.mode1 = TA_POLYMODE1_Z_ALWAYS|TA_POLYMODE1_NO_Z_UPDATE;
|
||||
mypoly.mode2 =
|
||||
TA_POLYMODE2_BLEND_SRC_ALPHA|TA_POLYMODE2_BLEND_DST_INVALPHA|
|
||||
TA_POLYMODE2_FOG_DISABLED|TA_POLYMODE2_ENABLE_ALPHA;
|
||||
mypoly.texture = 0;
|
||||
mypoly.red = mypoly.green = mypoly.blue = mypoly.alpha = 0;
|
||||
ta_commit_list(&mypoly);
|
||||
}
|
||||
|
||||
|
||||
void OSystem_Dreamcast::setPalette(const byte *colors, uint start, uint num)
|
||||
{
|
||||
unsigned short *dst = palette + start;
|
||||
if (num>0)
|
||||
while ( num-- ) {
|
||||
*dst++ = ((colors[0]<<7)&0x7c00)|
|
||||
((colors[1]<<2)&0x03e0)|
|
||||
((colors[2]>>3)&0x001f);
|
||||
colors += 3;
|
||||
}
|
||||
_screen_dirty = true;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::setCursorPalette(const byte *colors, uint start, uint num)
|
||||
{
|
||||
unsigned short *dst = cursor_palette + start;
|
||||
if (num>0)
|
||||
while ( num-- ) {
|
||||
*dst++ = ((colors[0]<<7)&0x7c00)|
|
||||
((colors[1]<<2)&0x03e0)|
|
||||
((colors[2]>>3)&0x001f);
|
||||
colors += 3;
|
||||
}
|
||||
_enable_cursor_palette = true;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::grabPalette(byte *colors, uint start, uint num) const
|
||||
{
|
||||
const unsigned short *src = palette + start;
|
||||
if (num>0)
|
||||
while ( num-- ) {
|
||||
unsigned short p = *src++;
|
||||
colors[0] = ((p&0x7c00)>>7)|((p&0x7000)>>12);
|
||||
colors[1] = ((p&0x03e0)>>2)|((p&0x0380)>>7);
|
||||
colors[2] = ((p&0x001f)<<3)|((p&0x001c)>>2);
|
||||
colors += 3;
|
||||
}
|
||||
}
|
||||
|
||||
Graphics::PixelFormat OSystem_Dreamcast::getScreenFormat() const
|
||||
{
|
||||
return screenFormats[_screenFormat];
|
||||
}
|
||||
|
||||
Common::List<Graphics::PixelFormat> OSystem_Dreamcast::getSupportedFormats() const
|
||||
{
|
||||
Common::List<Graphics::PixelFormat> list;
|
||||
unsigned i;
|
||||
for (i=0; i<NUM_FORMATS; i++)
|
||||
list.push_front(screenFormats[i]);
|
||||
return list;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::setScaling()
|
||||
{
|
||||
if (_screen_w > 400) {
|
||||
_xscale = _yscale = 1.0;
|
||||
_top_offset = (SCREEN_H-_screen_h)>>1;
|
||||
} else if (_aspect_stretch && _screen_w == 320 && _screen_h == 200) {
|
||||
_xscale = SCREEN_W/320.0;
|
||||
_yscale = SCREEN_H/200.0;
|
||||
_top_offset = 0;
|
||||
} else {
|
||||
_xscale = _yscale = 2.0;
|
||||
_top_offset = (SCREEN_H>>1)-_screen_h;
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::initSize(uint w, uint h, const Graphics::PixelFormat *format)
|
||||
{
|
||||
assert(w <= SCREEN_W && h <= SCREEN_H);
|
||||
|
||||
int i = 0;
|
||||
if (format != NULL)
|
||||
for (i=NUM_FORMATS-1; i>0; --i)
|
||||
if (*format == screenFormats[i])
|
||||
break;
|
||||
_screenFormat = i;
|
||||
|
||||
_overlay_visible = false;
|
||||
_overlay_fade = 0.0;
|
||||
_screen_w = w;
|
||||
_screen_h = h;
|
||||
_overlay_x = (w-OVL_W)/2;
|
||||
_overlay_y = (h-OVL_H)/2;
|
||||
if (_overlay_x<0) _overlay_x = 0;
|
||||
if (_overlay_y<0) _overlay_y = 0;
|
||||
setScaling();
|
||||
ta_sync();
|
||||
if (!screen)
|
||||
screen = new unsigned char[SCREEN_W*SCREEN_H*2];
|
||||
if (!overlay)
|
||||
overlay = new unsigned short[OVL_W*OVL_H];
|
||||
for (i=0; i<NUM_BUFFERS; i++)
|
||||
if (!screen_tx[i])
|
||||
screen_tx[i] = ta_txalloc(SCREEN_W*SCREEN_H*2);
|
||||
for (i=0; i<NUM_BUFFERS; i++)
|
||||
if (!mouse_tx[i])
|
||||
mouse_tx[i] = ta_txalloc(MOUSE_W*MOUSE_H*2);
|
||||
for (i=0; i<NUM_BUFFERS; i++)
|
||||
if (!ovl_tx[i])
|
||||
ovl_tx[i] = ta_txalloc(OVL_TXSTRIDE*OVL_H*2);
|
||||
_screen_buffer = 0;
|
||||
_mouse_buffer = 0;
|
||||
_overlay_buffer = 0;
|
||||
_screen_dirty = true;
|
||||
_overlay_dirty = true;
|
||||
*(volatile unsigned int *)(0xa05f80e4) = SCREEN_W/32; //stride
|
||||
// dc_reset_screen(0, 0);
|
||||
memset(screen, 0, SCREEN_W*SCREEN_H*2);
|
||||
memset(overlay, 0, OVL_W*OVL_H*sizeof(unsigned short));
|
||||
|
||||
_devpoll = Timer();
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::copyRectToScreen(const void *buf, int pitch, int x, int y,
|
||||
int w, int h)
|
||||
{
|
||||
if (w<1 || h<1)
|
||||
return;
|
||||
if (_screenFormat != 0) {
|
||||
x<<=1; w<<=1;
|
||||
}
|
||||
unsigned char *dst = screen + y*SCREEN_W*2 + x;
|
||||
const byte *src = (const byte *)buf;
|
||||
do {
|
||||
memcpy(dst, src, w);
|
||||
dst += SCREEN_W*2;
|
||||
src += pitch;
|
||||
} while (--h);
|
||||
_screen_dirty = true;
|
||||
}
|
||||
|
||||
bool OSystem_Dreamcast::showMouse(bool visible)
|
||||
{
|
||||
bool last = _ms_visible;
|
||||
_ms_visible = visible;
|
||||
|
||||
return last;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::warpMouse(int x, int y)
|
||||
{
|
||||
_ms_cur_x = x;
|
||||
_ms_cur_y = y;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::setMouseCursor(const void *buf, uint w, uint h,
|
||||
int hotspot_x, int hotspot_y,
|
||||
uint32 keycolor, bool dontScale, const Graphics::PixelFormat *format, const byte *mask) {
|
||||
if (mask)
|
||||
warning("OSystem_Dreamcast::setMouseCursor: Masks are not supported");
|
||||
|
||||
_ms_cur_w = w;
|
||||
_ms_cur_h = h;
|
||||
|
||||
_ms_hotspot_x = hotspot_x;
|
||||
_ms_hotspot_y = hotspot_y;
|
||||
|
||||
_ms_keycolor = keycolor;
|
||||
|
||||
int i = 0;
|
||||
if (format != NULL)
|
||||
for (i=NUM_FORMATS-1; i>0; --i)
|
||||
if (*format == screenFormats[i])
|
||||
break;
|
||||
_mouseFormat = i;
|
||||
|
||||
free(_ms_buf);
|
||||
|
||||
if (_mouseFormat != 0)
|
||||
w <<= 1;
|
||||
|
||||
_ms_buf = (byte *)malloc(w * h);
|
||||
memcpy(_ms_buf, buf, w * h);
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::setShakePos(int shake_x_pos, int shake_y_pos)
|
||||
{
|
||||
_current_shake_x_pos = shake_x_pos;
|
||||
_current_shake_y_pos = shake_y_pos;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::updateScreenTextures(void)
|
||||
{
|
||||
if (_screen_dirty) {
|
||||
|
||||
_screen_buffer++;
|
||||
_screen_buffer &= NUM_BUFFERS-1;
|
||||
|
||||
unsigned short *dst = (unsigned short *)screen_tx[_screen_buffer];
|
||||
unsigned char *src = screen;
|
||||
|
||||
// while ((*((volatile unsigned int *)(void *)0xa05f810c) & 0x3ff) != 200);
|
||||
// *((volatile unsigned int *)(void *)0xa05f8040) = 0xff0000;
|
||||
|
||||
if (_screenFormat == 0)
|
||||
for ( int y = 0; y<_screen_h; y++ )
|
||||
{
|
||||
texture_memcpy64_pal( dst, src, _screen_w>>5, palette );
|
||||
src += SCREEN_W*2;
|
||||
dst += SCREEN_W;
|
||||
}
|
||||
else
|
||||
for ( int y = 0; y<_screen_h; y++ )
|
||||
{
|
||||
texture_memcpy64( dst, src, _screen_w>>5 );
|
||||
src += SCREEN_W*2;
|
||||
dst += SCREEN_W;
|
||||
}
|
||||
|
||||
_screen_dirty = false;
|
||||
}
|
||||
|
||||
if ( _overlay_visible && _overlay_dirty ) {
|
||||
|
||||
_overlay_buffer++;
|
||||
_overlay_buffer &= NUM_BUFFERS-1;
|
||||
|
||||
unsigned short *dst = (unsigned short *)ovl_tx[_overlay_buffer];
|
||||
unsigned short *src = overlay;
|
||||
|
||||
for ( int y = 0; y<OVL_H; y++ )
|
||||
{
|
||||
texture_memcpy64( dst, src, OVL_W>>5 );
|
||||
src += OVL_W;
|
||||
dst += OVL_TXSTRIDE;
|
||||
}
|
||||
|
||||
_overlay_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::updateScreenPolygons(void)
|
||||
{
|
||||
struct polygon_list mypoly;
|
||||
struct packed_colour_vertex_list myvertex;
|
||||
|
||||
// *((volatile unsigned int *)(void *)0xa05f8040) = 0x00ff00;
|
||||
|
||||
mypoly.cmd =
|
||||
TA_CMD_POLYGON|TA_CMD_POLYGON_TYPE_OPAQUE|TA_CMD_POLYGON_SUBLIST|
|
||||
TA_CMD_POLYGON_STRIPLENGTH_2|TA_CMD_POLYGON_PACKED_COLOUR|TA_CMD_POLYGON_TEXTURED;
|
||||
mypoly.mode1 = TA_POLYMODE1_Z_ALWAYS|TA_POLYMODE1_NO_Z_UPDATE;
|
||||
mypoly.mode2 =
|
||||
TA_POLYMODE2_BLEND_SRC|TA_POLYMODE2_FOG_DISABLED|TA_POLYMODE2_TEXTURE_REPLACE|
|
||||
TA_POLYMODE2_U_SIZE_1024|TA_POLYMODE2_V_SIZE_1024;
|
||||
mypoly.texture = screenFormats[_screenFormat].textureFormat|
|
||||
TA_TEXTUREMODE_NON_TWIDDLED|TA_TEXTUREMODE_STRIDE|
|
||||
TA_TEXTUREMODE_ADDRESS(screen_tx[_screen_buffer]);
|
||||
|
||||
mypoly.red = mypoly.green = mypoly.blue = mypoly.alpha = 0;
|
||||
|
||||
ta_begin_frame();
|
||||
// *((volatile unsigned int *)(void *)0xa05f8040) = 0x0000ff;
|
||||
ta_commit_list(&mypoly);
|
||||
|
||||
myvertex.cmd = TA_CMD_VERTEX;
|
||||
myvertex.ocolour = 0;
|
||||
myvertex.colour = 0;
|
||||
myvertex.z = 0.5;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = 0.0;
|
||||
|
||||
myvertex.x = LEFT_OFFSET;
|
||||
myvertex.y = TOP_OFFSET;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x = _screen_w*_xscale;
|
||||
myvertex.u = _screen_w*(1/1024.0);
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x = 0.0;
|
||||
myvertex.y += _screen_h*_yscale;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = _screen_h*(1/1024.0);
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x = _screen_w*_xscale;
|
||||
myvertex.u = _screen_w*(1/1024.0);
|
||||
myvertex.cmd |= TA_CMD_VERTEX_EOS;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
ta_commit_end();
|
||||
|
||||
if (_overlay_visible) {
|
||||
if (_overlay_fade < 1.0)
|
||||
_overlay_fade += 0.125;
|
||||
} else {
|
||||
if (_overlay_fade > 0)
|
||||
_overlay_fade -= 0.125;
|
||||
}
|
||||
|
||||
if (_overlay_fade > 0.0) {
|
||||
|
||||
mypoly.cmd =
|
||||
TA_CMD_POLYGON|TA_CMD_POLYGON_TYPE_TRANSPARENT|TA_CMD_POLYGON_SUBLIST|
|
||||
TA_CMD_POLYGON_STRIPLENGTH_2|TA_CMD_POLYGON_PACKED_COLOUR|TA_CMD_POLYGON_TEXTURED;
|
||||
mypoly.mode1 = TA_POLYMODE1_Z_ALWAYS|TA_POLYMODE1_NO_Z_UPDATE;
|
||||
mypoly.mode2 =
|
||||
TA_POLYMODE2_BLEND_SRC/*_ALPHA*/|TA_POLYMODE2_BLEND_DST_INVALPHA|
|
||||
TA_POLYMODE2_ENABLE_ALPHA|
|
||||
TA_POLYMODE2_FOG_DISABLED|TA_POLYMODE2_TEXTURE_MODULATE_ALPHA|
|
||||
TA_POLYMODE2_U_SIZE_512|TA_POLYMODE2_V_SIZE_512;
|
||||
mypoly.texture = TA_TEXTUREMODE_ARGB4444|TA_TEXTUREMODE_NON_TWIDDLED|
|
||||
TA_TEXTUREMODE_ADDRESS(ovl_tx[_overlay_buffer]);
|
||||
|
||||
mypoly.red = mypoly.green = mypoly.blue = mypoly.alpha = 0.0;
|
||||
|
||||
ta_commit_list(&mypoly);
|
||||
|
||||
myvertex.cmd = TA_CMD_VERTEX;
|
||||
myvertex.ocolour = 0;
|
||||
myvertex.colour = 0xffffff|(((int)(255*_overlay_fade))<<24);
|
||||
|
||||
myvertex.z = 0.5;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = 0.0;
|
||||
|
||||
myvertex.x = _overlay_x*_xscale+LEFT_OFFSET;
|
||||
myvertex.y = _overlay_y*_yscale+TOP_OFFSET;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x += OVL_W*_xscale;
|
||||
myvertex.u = OVL_W*(1.0/512.0);
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x = _overlay_x*_xscale;
|
||||
myvertex.y += OVL_H*_yscale;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = OVL_H*(1.0/512.0);
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x += OVL_W*_xscale;
|
||||
myvertex.u = OVL_W*(1.0/512.0);
|
||||
myvertex.cmd |= TA_CMD_VERTEX_EOS;
|
||||
ta_commit_list(&myvertex);
|
||||
}
|
||||
|
||||
if (_softkbd_on)
|
||||
if (_softkbd_motion < 120)
|
||||
_softkbd_motion += 10;
|
||||
else
|
||||
;
|
||||
else
|
||||
if (_softkbd_motion > 0)
|
||||
_softkbd_motion -= 10;
|
||||
|
||||
if (_softkbd_motion)
|
||||
_softkbd.draw(330.0*sin(0.013*_softkbd_motion) - 320.0, 200.0,
|
||||
120-_softkbd_motion);
|
||||
|
||||
// *((volatile unsigned int *)(void *)0xa05f8040) = 0xffff00;
|
||||
drawMouse(_ms_cur_x, _ms_cur_y, _ms_cur_w, _ms_cur_h, _ms_buf, _ms_visible);
|
||||
// *((volatile unsigned int *)(void *)0xa05f8040) = 0xff00ff;
|
||||
ta_commit_frame();
|
||||
|
||||
// *((volatile unsigned int *)(void *)0xa05f8040) = 0x0;
|
||||
|
||||
_last_screen_refresh = Timer();
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::updateScreen(void)
|
||||
{
|
||||
updateScreenTextures();
|
||||
updateScreenPolygons();
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::maybeRefreshScreen(void)
|
||||
{
|
||||
unsigned int t = Timer();
|
||||
if((int)(t-_last_screen_refresh) > USEC_TO_TIMER(30000))
|
||||
updateScreenPolygons();
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::drawMouse(int xdraw, int ydraw, int w, int h,
|
||||
unsigned char *buf, bool visible)
|
||||
{
|
||||
if (!visible || buf == NULL || !w || !h || w>MOUSE_W || h>MOUSE_H) {
|
||||
commit_dummy_transpoly();
|
||||
return;
|
||||
}
|
||||
|
||||
struct polygon_list mypoly;
|
||||
struct packed_colour_vertex_list myvertex;
|
||||
|
||||
_mouse_buffer++;
|
||||
_mouse_buffer &= NUM_BUFFERS-1;
|
||||
|
||||
unsigned short *dst = (unsigned short *)mouse_tx[_mouse_buffer];
|
||||
unsigned int texturemode = screenFormats[_mouseFormat].textureFormat;
|
||||
|
||||
if (_mouseFormat == 0) {
|
||||
unsigned short *pal = _enable_cursor_palette? cursor_palette : palette;
|
||||
for (int y=0; y<h; y++) {
|
||||
int x;
|
||||
for (x=0; x<w; x++)
|
||||
if (*buf == _ms_keycolor) {
|
||||
*dst++ = 0;
|
||||
buf++;
|
||||
} else
|
||||
*dst++ = pal[*buf++]|0x8000;
|
||||
dst += MOUSE_W-x;
|
||||
}
|
||||
} else if(texturemode == TA_TEXTUREMODE_RGB565 &&
|
||||
_ms_keycolor<=0xffff) {
|
||||
/* Special handling when doing colorkey on RGB565; we need
|
||||
to convert to ARGB1555 to get an alpha channel... */
|
||||
texturemode = TA_TEXTUREMODE_ARGB1555;
|
||||
unsigned short *bufs = (unsigned short *)buf;
|
||||
for (int y=0; y<h; y++) {
|
||||
int x;
|
||||
for (x=0; x<w; x++)
|
||||
if (*bufs == _ms_keycolor) {
|
||||
*dst++ = 0;
|
||||
bufs++;
|
||||
} else {
|
||||
unsigned short p = *bufs++;
|
||||
*dst++ = (p&0x1f)|((p&0xffc0)>>1)|0x8000;
|
||||
}
|
||||
dst += MOUSE_W-x;
|
||||
}
|
||||
} else {
|
||||
unsigned short *bufs = (unsigned short *)buf;
|
||||
for (int y=0; y<h; y++) {
|
||||
int x;
|
||||
for (x=0; x<w; x++)
|
||||
if (*bufs == _ms_keycolor) {
|
||||
*dst++ = 0;
|
||||
bufs++;
|
||||
} else
|
||||
*dst++ = *bufs++;
|
||||
dst += MOUSE_W-x;
|
||||
}
|
||||
}
|
||||
|
||||
mypoly.cmd =
|
||||
TA_CMD_POLYGON|TA_CMD_POLYGON_TYPE_TRANSPARENT|TA_CMD_POLYGON_SUBLIST|
|
||||
TA_CMD_POLYGON_STRIPLENGTH_2|TA_CMD_POLYGON_PACKED_COLOUR|TA_CMD_POLYGON_TEXTURED;
|
||||
mypoly.mode1 = TA_POLYMODE1_Z_ALWAYS|TA_POLYMODE1_NO_Z_UPDATE;
|
||||
mypoly.mode2 =
|
||||
TA_POLYMODE2_BLEND_SRC_ALPHA|TA_POLYMODE2_BLEND_DST_INVALPHA|
|
||||
TA_POLYMODE2_FOG_DISABLED|TA_POLYMODE2_TEXTURE_REPLACE|
|
||||
TA_POLYMODE2_U_SIZE_128|TA_POLYMODE2_V_SIZE_128;
|
||||
mypoly.texture = texturemode|TA_TEXTUREMODE_NON_TWIDDLED|
|
||||
TA_TEXTUREMODE_ADDRESS(mouse_tx[_mouse_buffer]);
|
||||
|
||||
mypoly.red = mypoly.green = mypoly.blue = mypoly.alpha = 0;
|
||||
|
||||
ta_commit_list(&mypoly);
|
||||
|
||||
myvertex.cmd = TA_CMD_VERTEX;
|
||||
myvertex.ocolour = 0;
|
||||
myvertex.colour = 0xffff00;
|
||||
myvertex.z = 0.25;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = 0.0;
|
||||
|
||||
myvertex.x = (xdraw-_ms_hotspot_x)*_xscale + LEFT_OFFSET;
|
||||
myvertex.y = (ydraw-_ms_hotspot_y)*_yscale + TOP_OFFSET;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x += w*_xscale;
|
||||
myvertex.u = w*(1.0/MOUSE_W);
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x -= w*_xscale;
|
||||
myvertex.y += h*_yscale;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = h*(1.0/MOUSE_H);
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x += w*_xscale;
|
||||
myvertex.u = w*(1.0/MOUSE_W);
|
||||
myvertex.cmd |= TA_CMD_VERTEX_EOS;
|
||||
ta_commit_list(&myvertex);
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::mouseToSoftKbd(int x, int y, int &rx, int &ry) const
|
||||
{
|
||||
if (_softkbd_motion) {
|
||||
rx = (int)(x*_xscale - (330.0*sin(0.013*_softkbd_motion) + LEFT_OFFSET - 320.0));
|
||||
ry = (int)(y*_yscale + TOP_OFFSET - 200.0);
|
||||
} else {
|
||||
rx = -1;
|
||||
ry = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void OSystem_Dreamcast::showOverlay(bool inGUI)
|
||||
{
|
||||
_overlay_in_gui = inGUI;
|
||||
_overlay_visible = true;
|
||||
clearOverlay();
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::hideOverlay()
|
||||
{
|
||||
_overlay_in_gui = false;
|
||||
_overlay_visible = false;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::clearOverlay()
|
||||
{
|
||||
if (!_overlay_visible)
|
||||
return;
|
||||
|
||||
memset(overlay, 0, OVL_W*OVL_H*sizeof(unsigned short));
|
||||
|
||||
_overlay_dirty = true;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::grabOverlay(Graphics::Surface &surface)
|
||||
{
|
||||
assert(surface.w >= OVL_W);
|
||||
assert(surface.h >= OVL_H);
|
||||
assert(surface.format.bytesPerPixel == sizeof(unsigned short));
|
||||
|
||||
byte *src = (byte *)overlay;
|
||||
byte *dst = (byte *)surface.getPixels();
|
||||
Graphics::copyBlit(dst, src, surface.pitch, OVL_W * sizeof(unsigned short),
|
||||
OVL_W, OVL_H, sizeof(unsigned short));
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::copyRectToOverlay(const void *buf, int pitch,
|
||||
int x, int y, int w, int h)
|
||||
{
|
||||
if (w<1 || h<1)
|
||||
return;
|
||||
unsigned short *dst = overlay + y*OVL_W + x;
|
||||
const unsigned char *src = (const unsigned char *)buf;
|
||||
do {
|
||||
memcpy(dst, src, w*sizeof(int16));
|
||||
dst += OVL_W;
|
||||
src += pitch;
|
||||
} while (--h);
|
||||
_overlay_dirty = true;
|
||||
}
|
||||
|
||||
Graphics::Surface *OSystem_Dreamcast::lockScreen()
|
||||
{
|
||||
if (!screen)
|
||||
return 0;
|
||||
|
||||
_framebuffer.init(_screen_w, _screen_h, SCREEN_W*2, screen, screenFormats[_screenFormat]);
|
||||
|
||||
return &_framebuffer;
|
||||
}
|
||||
|
||||
void OSystem_Dreamcast::unlockScreen()
|
||||
{
|
||||
// Force screen update
|
||||
_screen_dirty = true;
|
||||
}
|
||||
|
||||
int16 OSystem_Dreamcast::getOverlayHeight() const
|
||||
{
|
||||
return OVL_H;
|
||||
}
|
||||
|
||||
int16 OSystem_Dreamcast::getOverlayWidth() const
|
||||
{
|
||||
return OVL_W;
|
||||
}
|
||||
46
backends/platform/dc/dreamcast.mk
Normal file
46
backends/platform/dc/dreamcast.mk
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
CC := $(CXX)
|
||||
ASFLAGS := $(CXXFLAGS)
|
||||
|
||||
dist : SCUMMVM.BIN IP.BIN plugin_dist
|
||||
|
||||
clean : dcclean
|
||||
|
||||
plugin_dist : plugins
|
||||
@[ -z "$(PLUGINS)" ] || for p in $(or $(PLUGINS),none); do \
|
||||
t="`basename \"$$p\" | LC_CTYPE=C tr '[:lower:]' '[:upper:]'`"; \
|
||||
if /usr/bin/test "$$p" -ot "$$t"; then :; else \
|
||||
echo sh-elf-strip -g -o "$$t" "$$p"; \
|
||||
sh-elf-strip -g -o "$$t" "$$p"; \
|
||||
$(srcdir)/backends/platform/dc/check_plugin_symbols "$$t"; \
|
||||
fi;\
|
||||
done
|
||||
|
||||
SCUMMVM.BIN : scummvm.bin
|
||||
scramble $< $@
|
||||
|
||||
scummvm.bin : scummvm.elf
|
||||
sh-elf-objcopy -S -R .stack -O binary $< $@
|
||||
|
||||
IP.BIN : ip.txt
|
||||
makeip $< $@
|
||||
|
||||
ip.txt : $(srcdir)/backends/platform/dc/ip.txt.in
|
||||
if [ x"$(VER_EXTRA)" = xgit ]; then \
|
||||
ver="GIT"; \
|
||||
else ver="V$(VERSION)"; fi; \
|
||||
if expr "$$ver" : V...... >/dev/null; then \
|
||||
ver="$$(printf "V%02d%02d%d" $$(($(VER_MAJOR) - 2000)) $(VER_MINOR) $(VER_PATCH))"; fi; \
|
||||
sed -e 's/[@]VERSION[@]/'"$$ver"/ -e 's/[@]DATE[@]/$(shell date '+%Y%m%d')/' < $< > $@
|
||||
|
||||
|
||||
dcdist : dist
|
||||
mkdir -p dcdist/scummvm
|
||||
cp scummvm.elf SCUMMVM.BIN IP.BIN *.PLG dcdist/scummvm/
|
||||
|
||||
dcclean :
|
||||
$(RM) backends/platform/dc/plugin_head.o
|
||||
$(RM) scummvm.bin SCUMMVM.BIN ip.txt IP.BIN *.PLG
|
||||
$(RM_REC) dcdist
|
||||
|
||||
.PHONY: dcclean
|
||||
237
backends/platform/dc/icon.cpp
Normal file
237
backends/platform/dc/icon.cpp
Normal file
@@ -0,0 +1,237 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <ronin/ronin.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "icon.h"
|
||||
|
||||
void Icon::create_vmicon(void *buffer)
|
||||
{
|
||||
unsigned short *pal = (unsigned short *)buffer;
|
||||
unsigned char *pix = ((unsigned char *)buffer)+32;
|
||||
|
||||
for (int n = 0; n<16; n++) {
|
||||
int p = palette[n];
|
||||
pal[n] =
|
||||
((p>>16)&0xf000)|
|
||||
((p>>12)&0x0f00)|
|
||||
((p>> 8)&0x00f0)|
|
||||
((p>> 4)&0x000f);
|
||||
}
|
||||
|
||||
for (int line = 0; line < 32; line++) {
|
||||
memcpy(pix, &bitmap[32/2*(31-line)], 32/2);
|
||||
pix += 32/2;
|
||||
}
|
||||
}
|
||||
|
||||
void Icon::create_texture()
|
||||
{
|
||||
static unsigned char tt[16] = { 0, 1, 4, 5, 16, 17, 20, 21,
|
||||
64, 65, 68, 69, 80, 81, 84, 85 };
|
||||
unsigned short *tex = (unsigned short *)ta_txalloc(512);
|
||||
unsigned short *linebase;
|
||||
unsigned char *src = bitmap+sizeof(bitmap)-17;
|
||||
for (int y=0; y<16; y++) {
|
||||
linebase = tex + (tt[y]<<1);
|
||||
for (int x=0; x<16; x++, --src)
|
||||
linebase[tt[x]] = src[16]|(src[0]<<8);
|
||||
src -= 16;
|
||||
}
|
||||
texture = tex;
|
||||
}
|
||||
|
||||
void Icon::setPalette(int pal)
|
||||
{
|
||||
unsigned int (*hwpal)[64][16] = (unsigned int (*)[64][16])0xa05f9000;
|
||||
for (int n = 0; n<16; n++)
|
||||
(*hwpal)[pal][n] = palette[n];
|
||||
}
|
||||
|
||||
void Icon::draw(float x1, float y1, float x2, float y2, int pal,
|
||||
unsigned int argb)
|
||||
{
|
||||
struct polygon_list mypoly;
|
||||
struct packed_colour_vertex_list myvertex;
|
||||
|
||||
mypoly.cmd =
|
||||
TA_CMD_POLYGON|TA_CMD_POLYGON_TYPE_TRANSPARENT|TA_CMD_POLYGON_SUBLIST|
|
||||
TA_CMD_POLYGON_STRIPLENGTH_2|TA_CMD_POLYGON_PACKED_COLOUR|TA_CMD_POLYGON_TEXTURED;
|
||||
mypoly.mode1 = TA_POLYMODE1_Z_ALWAYS|TA_POLYMODE1_NO_Z_UPDATE;
|
||||
mypoly.mode2 =
|
||||
TA_POLYMODE2_BLEND_SRC_ALPHA|TA_POLYMODE2_BLEND_DST_INVALPHA|
|
||||
TA_POLYMODE2_FOG_DISABLED|TA_POLYMODE2_ENABLE_ALPHA|
|
||||
TA_POLYMODE2_TEXTURE_MODULATE_ALPHA|TA_POLYMODE2_U_SIZE_32|
|
||||
TA_POLYMODE2_V_SIZE_32;
|
||||
mypoly.texture = TA_TEXTUREMODE_CLUT4|TA_TEXTUREMODE_CLUTBANK4(pal)|
|
||||
TA_TEXTUREMODE_ADDRESS(texture);
|
||||
|
||||
mypoly.red = mypoly.green = mypoly.blue = mypoly.alpha = 0;
|
||||
|
||||
ta_commit_list(&mypoly);
|
||||
|
||||
myvertex.cmd = TA_CMD_VERTEX;
|
||||
myvertex.ocolour = 0;
|
||||
myvertex.colour = argb;
|
||||
myvertex.z = 0.5;
|
||||
myvertex.u = 0.0;
|
||||
myvertex.v = 1.0;
|
||||
|
||||
myvertex.x = x1;
|
||||
myvertex.y = y1;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x = x2;
|
||||
myvertex.v = 0.0;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x = x1;
|
||||
myvertex.y = y2;
|
||||
myvertex.u = 1.0;
|
||||
myvertex.v = 1.0;
|
||||
ta_commit_list(&myvertex);
|
||||
|
||||
myvertex.x = x2;
|
||||
myvertex.v = 0.0;
|
||||
myvertex.cmd |= TA_CMD_VERTEX_EOS;
|
||||
ta_commit_list(&myvertex);
|
||||
}
|
||||
|
||||
int Icon::find_unused_pixel(const unsigned char *mask)
|
||||
{
|
||||
int use[16];
|
||||
memset(use, 0, sizeof(use));
|
||||
unsigned char *p = bitmap;
|
||||
for (int n=0; n<32*32/2/4; n++) {
|
||||
unsigned char mbits = ~*mask++;
|
||||
for (int i=0; i<4; i++) {
|
||||
unsigned char pix = *p++;
|
||||
if(mbits & 64)
|
||||
use[pix&0xf]++;
|
||||
if(mbits & 128)
|
||||
use[pix>>4]++;
|
||||
mbits <<= 2;
|
||||
}
|
||||
}
|
||||
for (int i=0; i<16; i++)
|
||||
if (!use[i])
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool Icon::load_image2(const void *data, int len)
|
||||
{
|
||||
struct {
|
||||
int size, w, h;
|
||||
short pla, bitcnt;
|
||||
int comp, sizeimg, xres, yres, used, imp;
|
||||
} hdr;
|
||||
if (len < 40)
|
||||
return false;
|
||||
memcpy(&hdr, data, 40);
|
||||
if (hdr.size != 40 || /* hdr.sizeimg<=0 || */ hdr.w<0 || hdr.h<0 ||
|
||||
hdr.bitcnt<0 || hdr.used<0)
|
||||
return false;
|
||||
if (!hdr.used)
|
||||
hdr.used = 1<<hdr.bitcnt;
|
||||
hdr.h >>= 1;
|
||||
/* Fix incorrect sizeimg (The Dig) */
|
||||
if (hdr.sizeimg < ((hdr.w*hdr.h*(1+hdr.bitcnt)+7)>>3))
|
||||
hdr.sizeimg = ((hdr.w*hdr.h*(1+hdr.bitcnt)+7)>>3);
|
||||
if (hdr.size + (hdr.used<<2) + hdr.sizeimg > len /* ||
|
||||
hdr.sizeimg < ((hdr.w*hdr.h*(1+hdr.bitcnt)+7)>>3) */)
|
||||
return false;
|
||||
if (hdr.w != 32 || hdr.h != 32 || hdr.bitcnt != 4 || hdr.used > 16)
|
||||
return false;
|
||||
memcpy(palette, ((const char *)data)+hdr.size, hdr.used<<2);
|
||||
memcpy(bitmap, ((const char *)data)+hdr.size+(hdr.used<<2), 32*32/2);
|
||||
for (int i=0; i<16; i++)
|
||||
palette[i] |= 0xff000000;
|
||||
for (int i=hdr.used; i<16; i++)
|
||||
palette[i] = 0;
|
||||
const unsigned char *mask =
|
||||
((const unsigned char *)data)+hdr.size+(hdr.used<<2)+32*32/2;
|
||||
int unused = find_unused_pixel(mask);
|
||||
if (unused >= 0) {
|
||||
unsigned char *pix = bitmap;
|
||||
for (int y=0; y<32; y++)
|
||||
for (int x=0; x<32/8; x++) {
|
||||
unsigned char mbits = *mask++;
|
||||
for (int z=0; z<4; z++) {
|
||||
unsigned char pbits = *pix;
|
||||
if (mbits & 64) pbits = (pbits & ~0xf) | unused;
|
||||
if (mbits & 128) pbits = (pbits & 0xf) | (unused << 4);
|
||||
*pix++ = pbits;
|
||||
mbits <<= 2;
|
||||
}
|
||||
}
|
||||
palette[unused] = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Icon::load_image1(const void *data, int len, int offs)
|
||||
{
|
||||
struct {
|
||||
char w, h, colors, rsrv;
|
||||
short pla, bitcnt;
|
||||
int bytes, offs;
|
||||
} hdr;
|
||||
if (len < offs+16)
|
||||
return false;
|
||||
memcpy(&hdr, ((const char *)data)+offs, 16);
|
||||
if (hdr.bytes > 0 && hdr.offs >= 0 && hdr.offs+hdr.bytes <= len)
|
||||
return load_image2(((const char *)data)+hdr.offs, hdr.bytes);
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Icon::load(const void *data, int len, int offs)
|
||||
{
|
||||
struct { short rsrv, type, cnt; } hdr;
|
||||
memset(bitmap, 0, sizeof(bitmap));
|
||||
memset(palette, 0, sizeof(palette));
|
||||
texture = NULL;
|
||||
if (len < offs+6)
|
||||
return false;
|
||||
memcpy(&hdr, ((const char *)data)+offs, 6);
|
||||
if (hdr.type != 1 || hdr.cnt < 1 || offs+6+(hdr.cnt<<4) > len)
|
||||
return false;
|
||||
for (int i=0; i<hdr.cnt; i++)
|
||||
if (load_image1(data, len, offs+6+(i<<4)))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Icon::load(const char *filename)
|
||||
{
|
||||
char buf[2048];
|
||||
int fd;
|
||||
if ((fd = open(filename, O_RDONLY))>=0) {
|
||||
int sz;
|
||||
sz = read(fd, buf, sizeof(buf));
|
||||
close(fd);
|
||||
if (sz>0)
|
||||
return load(buf, sz);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
46
backends/platform/dc/icon.h
Normal file
46
backends/platform/dc/icon.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/* ScummVM - Graphic Adventure Engine
|
||||
*
|
||||
* ScummVM is the legal property of its developers, whose names
|
||||
* are too numerous to list here. Please refer to the COPYRIGHT
|
||||
* file distributed with this source distribution.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 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 General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef DC_ICON_H
|
||||
#define DC_ICON_H
|
||||
|
||||
class Icon
|
||||
{
|
||||
private:
|
||||
unsigned char bitmap[32*32/2];
|
||||
unsigned int palette[16];
|
||||
void *texture;
|
||||
|
||||
int find_unused_pixel(const unsigned char *);
|
||||
bool load_image1(const void *data, int len, int offs);
|
||||
bool load_image2(const void *data, int len);
|
||||
|
||||
public:
|
||||
bool load(const void *data, int len, int offs = 0);
|
||||
bool load(const char *filename);
|
||||
void create_texture();
|
||||
void setPalette(int pal);
|
||||
void draw(float x1, float y1, float x2, float y2, int pal,
|
||||
unsigned argb = 0xffffffff);
|
||||
void create_vmicon(void *buffer);
|
||||
};
|
||||
|
||||
#endif /* DC_ICON_H */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user