Initial commit

This commit is contained in:
2026-02-02 04:50:13 +01:00
commit 5b11698731
22592 changed files with 7677434 additions and 0 deletions

View 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

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

View 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(&currentCommit, 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View 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

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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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);
}

View 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

View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View 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

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

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

View 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);
}
}
}

View 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);
}
}
}
}

View File

@@ -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>
* &lt;Keyboard
* android:keyWidth="%10p"
* android:keyHeight="50px"
* android:horizontalGap="2px"
* android:verticalGap="2px" &gt;
* &lt;Row android:keyWidth="32px" &gt;
* &lt;Key android:keyLabel="A" /&gt;
* ...
* &lt;/Row&gt;
* ...
* &lt;/Keyboard&gt;
* </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

View File

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

View File

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

View File

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

View 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));
}
}

View 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);
}
}

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

View File

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

View File

@@ -0,0 +1,5 @@
package org.scummvm.scummvm;
public interface OnKeyboardVisibilityListener {
void onVisibilityChanged(boolean visible);
}

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

View 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");
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View 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] == '/';
}
}
}

View File

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

View File

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

View 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);
}
}
}

File diff suppressed because it is too large Load Diff

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

View 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_

File diff suppressed because it is too large Load Diff

View 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);
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

View 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);
}

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

View 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

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

View 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

View 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

View 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

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

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

View 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

View 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'.

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

View 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

View 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

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

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

View 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

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

View 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

View 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 */

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

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

View 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

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

View 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