1
0

Compare commits

...

223 Commits

Author SHA1 Message Date
7b59fcb4f1 fix dockerfile due recent submodule git and reduce README - release 2.0.6 2024-06-05 16:04:47 -04:00
a261b4edc0 game as submodule for easy instalation and distribution..
*  minetest developers were so stupid or what? why not a simple
 submodule from git? so easy!
* do as:
    * `sed s#.*games/minetest#!games/minetest#g .gitignore`
    * `git add .gitignore && git commit -m "submodule step 1"`
    * `git submodule add -b stable-5.2 https://codeberg.org/minenux/minetest-game-minetest game/minetest`
    * `git add --all && git commit --amend -m "ubmodule step 2`
    * `sed s#.*games/minetest#games/minetest#g .gitignore`
    * `git add --all && git commit --amend -m "ubmodule step 3 final`
* update CI for gitlab to use the submodule and setup the game also
2024-06-05 15:07:21 -04:00
60a4940be8 MAC portage porfile add .. limited tested 2024-06-05 14:55:08 -04:00
94262d3bc1 fix dockerfile, tune up and setup proper dockerfile with all features
* you can build from dockerfile with `docker build -t mc .`
* later run with `docker run -p 40000:40000/udp -p 40000:40000/tcp -i -t mc`
* The docker by default copy the game from minenux and runs 40000 port
* fixed https://codeberg.org/minenux/minetest-engine-multicraft2/issues/52
  cos already provides linux support with all features (but by docker, puach)
* closes https://github.com/MultiCraft/MultiCraft/issues/59
2024-06-04 17:52:05 -04:00
532c187d83 Set event receiver to null before the receiver object is deleted
* backported bbcee42264
2024-06-03 11:23:48 -04:00
17cb07a366 formspec_escape stability improvement
* formspec_escape causes server crash, when text argument is a number.
  the `tostring(text)` prevents this
* backported https://github.com/MultiCraft/MultiCraft/pull/87
2024-06-03 09:41:55 -04:00
ce35eb4122 dont show the register confirmation, try to connect as fast as possible
* this is a security risk, but also a easy step.. the easy of use
  is a key to create a anarchy environment where people must learn
  to take care by itselft
2024-06-03 09:26:57 -04:00
d9bdbbd307 status string inverted logic fixed about status
* related to commit 90573105519953847be513068c1f05f1a1771d7c
2024-06-03 00:47:44 -04:00
5db6593abd fix bad implementation, try to use changed variable in constant function variable
* fix error: passing const string as this argument discards qualifiers
  for server list identification id
* related commit 90573105519953847be513068c1f05f1a1771d7c
2024-06-03 00:12:59 -04:00
35ea452ff5 fix bad implementation, try to redefine constant variable
* fix error: passing const string as this argument discards qualifiers
  for server list identification id
* related commit 90573105519953847be513068c1f05f1a1771d7c
2024-06-02 23:37:43 -04:00
3820aa20eb fix wrong access definition q_settings
* fix error: request for member 'empty' cos is not string
2024-06-02 22:51:26 -04:00
c047d31989 Disable builtin hunger and use default minetest.do_item_eat if
* adapted the PR https://github.com/MultiCraft/MultiCraft/pull/172
  do not remove but disable by default any refernce and only use it
  if enable explicit
* adapted commit 0a54481b29
2024-06-02 22:09:49 -04:00
cd18cab6d3 fake the user agent to the fuking server list retrieval
* closes https://codeberg.org/minenux/minetest-engine-multicraft2/issues/46
2024-06-02 21:38:01 -04:00
9057310551 use the id to send version as minetest if minetest id is choosen
* this need commit c8c06eaae54c395dc8ff76e402e31d0a51e20906
2024-06-02 21:24:40 -04:00
c94e9cdb9a remove stupid advertise about older servers 2024-06-02 21:16:09 -04:00
c8c06eaae5 Switch to MultiCraft server list an option only
* new settings to send identification
* new settings to make it optinal
* posibility to send information as need as minetest older
2024-06-02 21:07:06 -04:00
Maksym H
59b2c7be48 Version 2.0.6-release but not yet! 2024-06-02 19:31:28 -04:00
9a733f881b Add tab keybind 2024-06-02 06:40:02 -04:00
fd02603860 Knockback: fix rare "Invalid float vector dimension range" crash 2024-06-02 06:25:05 -04:00
ee85c709c9 Fixed crash on startup in event receiver 2024-06-02 06:21:28 -04:00
f20c739c48 unhide list of mods in server list
* close https://codeberg.org/minenux/minetest-engine-multicraft2/issues/45
2024-06-02 05:59:42 -04:00
f2f8f2c92e unhide list of players that was hide by stupid monte48
* close https://codeberg.org/minenux/minetest-engine-multicraft2/issues/48
* related commit 2aa0400bd2a1c205de50ce46e1758f09e1b0c306
2024-06-02 05:53:43 -04:00
luk3yx
f8bbed282e Don't use file download timeout for fetching ContentDB packages 2024-06-02 05:39:29 -04:00
7a9b79565b allow "-" on mods name for allowed chars 2024-06-02 05:22:07 -04:00
8eb09a5aa1 Add CSM restriction flag to block third party CSMs (#162) 2024-06-02 05:13:31 -04:00
bc3f43705c Handle translation files 2024-06-02 05:11:35 -04:00
6b22510752 Minor changes update gradle 2024-06-02 05:10:09 -04:00
8d4b921749 Initialise some variables in v7p becouse C++ standart
* the mapgen v7p has its own settings, to customise use v7 normal
2024-06-02 05:03:22 -04:00
67bd44f9fb Update the VCPKG version in Actions 2024-06-02 05:02:16 -04:00
Maksym H
1eb6b1dcb0 Disable desynchronize_mapblock_texture_animation by default 2024-06-02 05:00:47 -04:00
Deve
062f2e3613 Remove unused function that fails to build with new C++ 2024-06-02 04:58:34 -04:00
luk3yx
0eb18780e4 Add "hide_game = true" option to game.conf (#152) 2024-06-02 04:57:52 -04:00
Bektur
1538e5bc99 Android: update Kotlin part (#150) 2024-06-02 04:56:31 -04:00
a6f200ff21 Disable stereo mode support but dont over stereo mode on desktop
* Disable stereo mode support
* Fix stereo mode on desktop
* NOTE> this seems needs up to date irrlicht ?
2024-06-02 04:38:41 -04:00
f0d4e9bede Apple: minor update, change copyright XD 2024-06-02 04:37:32 -04:00
64ddc4065a Improve InfoText positioning on the screen 2024-06-02 04:22:52 -04:00
bf186d81b1 Avoid a crash in drawMeshNode() after reading out of array
* backported from b201316aed
2024-06-02 04:20:34 -04:00
Deve
f783bdb170 Fixed possible crash when close event is received during media loading 2024-06-02 04:19:38 -04:00
Maksym H
80d583e219 Android: update deps 2024-06-02 04:19:25 -04:00
d78e41a0f0 Fix compiler warnings in srp.cpp 2024-06-02 04:15:54 -04:00
005debcd8a adapt and set proper Fix curl deprecation warnings
* now curls still can be 7.19 and if 7.56.0 is detected then we used multipart
* commit 99209cd5d7
2024-06-02 04:12:41 -04:00
b7ad037647 update CI - specify 32bit for older debians build test 2024-06-02 01:17:51 -04:00
5ee92bc1ee backport "Few minor C++ fixes" not at all
* backport 344ef13bc0
* we are using irrlilch 1.8 so
  https://irrlicht.sourceforge.io/docu/classirr_1_1video_1_1_i_video_driver.html#a8c02ee280bb738cdf38b77e7a798244e
* do not remove the IrrlichtDevice due backguard compatibilty
2024-06-02 00:20:41 -04:00
1f161e2217 Add porting::getTotalSystemMemory() function
* becouse Convert textures to (A1)R5G6B5 format on low memory device
  tune up the commit 03737c4e9baeae7087ff986cbb901c64d312e8a6
* backported from 3bb240919a
2024-06-02 00:10:16 -04:00
284a473584 Android: get rid of getInputDialogState
* backported b7c470ae92
* let workaround of getInputDialogState
2024-06-01 23:43:02 -04:00
Maksym H
e21a1a6e62 Update mini-gmp 2024-06-01 23:38:12 -04:00
5bf0190771 fix sort is not a member of std on newers compilers 2024-06-01 19:49:19 -04:00
savilli
9866b07654 Fix potential freeze in core.check_for_falling 2024-06-01 00:14:23 -04:00
fbdf65c57b update CI - fix build of ssl packages on CI alpine images
* remove edge cos will always are changing
* openssl 1.1 is only at edge.. puff so maybe will provide errors on modern
2024-05-31 23:45:38 -04:00
fb8401098e CMake: link with Intl when necessary
* On some platforms, such as Linux with GNU libc, the gettext
  functions are present in the C standard library and libintl
  is not required. For other libc (uClibc-ng or musl) libintl
   may be required. Thanks Debian the mother of all distros!
* close https://github.com/minetest/minetest/issues/8583
* fixed https://github.com/minetest/minetest/issues/8588
2024-05-31 23:41:40 -04:00
e8265931bc update CI - added alpine build and use right dependency packages
* as always alpoine packages are full shit, puff
2024-05-31 22:44:37 -04:00
dbf529eed4 update CI - fix build for older releases and dont use private postgresql 2024-05-31 17:08:46 -04:00
e71ba6db9e update CI force usage of backports on older debians for json 2024-05-31 16:19:13 -04:00
a532068c86 update build CI use all server postgresql in older debians, trey also newer fedoras
* hardcoded cmake cos is the defaul for older versions of postgresql
  this also works on newer ones.. for custom paths reconfiguration
  must be done when runs cmake configure step
2024-05-31 16:12:24 -04:00
f266a5138e Check property duplicate login names
* Check for duplicate login in TOSERVER_INIT handler
  backported 492110a640
  i.e. checks for duplicate logins before sending all media data to the client
2024-05-31 15:47:20 -04:00
4171d9cc87 fix ci cmakelists typo error 2024-05-30 22:05:04 -04:00
c0f4c4de3e fix cmakelists install manpages error missing colon 2024-05-30 21:25:08 -04:00
0181518c79 fix package , fix ci build, tune cmake flags and rules
* tune up gitlab ci:
  * remove package part cos we have obs service
  * add feladora 38 and feladora 37 too
  * remove winbuntu 14, add debian 11, 12, winbuntu 17, 20, 22
  * use minenux minetest repo game (seems not work)
  * remove non buildable stages.. only build and package shit win
  * set multicraft as prefix path and artifacts
  * back cmake in list new behaviour for blacklist locales
  * gitlab ci buil for debian 8 using backports on jsoncpp
  * solved https://github.com/minetest/minetest/issues/6567
  * solved https://github.com/minetest/minetest/issues/7681
* cmake fixed to minimum supported and c++11 standar able
  * close #51
  * allow distro hardening and cflags env
    close #55
  * Fix no locales being generated when APPLY_LOCALE_BLACKLIST=0
  * Fix linking with Postgres libs:
  * closes https://github.com/minetest/minetest/issues/12149
  * closes https://github.com/minetest/minetest/issues/11219
  * PostgreSQL fallback code missed the includes https://github.com/minetest/minetest/issues/11219
  * a24899bf2d
  * 3e2145d662
  * integrates https://github.com/minetest/minetest/pull/11215
  *  a24899bf2d
  * backported 998e4820c9
2024-05-30 20:55:59 -04:00
ac78553119 tried gitlab CI with debian 8 and backports for jsoncpp 2024-05-30 17:33:25 -04:00
luk3yx
8e5c2c4897 Make automatic world name generation check worlds from other games as well 2024-05-30 17:31:26 -04:00
Maksym H
db11d3ec7b Android: replace porting::getTotalSystemMemory on SDL_GetSystemRAM 2024-05-30 17:30:43 -04:00
Maksym H
23a14cf674 Convert textures to (A1)R5G6B5 format on low memory device 2024-05-30 17:29:19 -04:00
dc28963b18 update CI - minimal debian (without extra repos) is 8 for jsoncpp 2024-05-30 17:16:30 -04:00
43070ef584 update build CI use up to date frexian for debian older targets 2024-05-30 17:03:27 -04:00
aab13f7dd2 update build CI due missing distro name for debian older targets 2024-05-30 16:59:16 -04:00
af558a6b41 update build CI for older and fit releases due erro ron older debian 2024-05-30 16:53:39 -04:00
cbcfc5347e update build CI for older and fit releases and add deb 12 2024-05-30 16:45:07 -04:00
7ab51bcd78 update build CI for older releases and add deb 12 2024-05-30 16:33:31 -04:00
Deve
d4d5ee3154 Free loaded data after alBufferData() (#147)
https://github.com/kcat/openal-soft/blob/master/examples/alplay.c#L266
2024-05-30 16:04:24 -04:00
Maksym H
40c42e3b8a MainMenu: minor cleanup 2024-05-30 16:04:01 -04:00
Maksym H
0f989e37ee Apple: Xcode 15 support 2024-05-30 15:59:28 -04:00
Maksym H
3c26abba0e Drop only 1 item if sneak is pressed 2024-05-30 15:58:29 -04:00
3c1e1a20da build definitions for multicraft like minetest minenux ones 2024-05-30 15:45:19 -04:00
Deve
4b622b058a Mobile: improve the chat experience (#146)
* Some chat input dialog fixes.

- If getAndroidChatOpen() is true then input dialog is created by chat.
- Hide touchscreengui when chat input dialog is open.

* Check input dialog owner in config registration, just in case

* Make sure there is no menu active before showing touchscreengui

* Reset input dialog owner when reading value
2024-05-30 15:29:19 -04:00
Maksym H
b116381d28 Fix background image scaling in MainMenu 2024-05-30 15:27:39 -04:00
Maksym H
575f130854 Version 2.0.5-release
* cherry picked from commit
  14a7fe0266
* fix main menu minor changes cherry picked from commit
  f917f5c8d0
2023-09-05 18:08:53 -04:00
880579ea15 fix header porting import and fix devices filtering conditionals
* Minor cleanup and improvements, cherry picked modified from commit
  66b1aafb4b
2023-09-05 15:37:40 -04:00
4fb563aed3 Apple: minor update and check depends
* cherry-picked from commit
  1a553f9efe#
* use jsoncpp in lib
2023-09-05 14:59:48 -04:00
7083b0fb09 Mobile: use SDL_IsTablet() to determine the device type
* cherry picked from commit
  1c1acc3cfb
* this commit define automatically the client mode, if tabled or not!
  is goot to be backported to final minetest
* this depends of the previous commit, so now phone devices and tables
  are buil using SDL, check commit bd244fb402ee965862f42455d18857d7453f2cac
  in this repository
2023-09-05 14:23:58 -04:00
bd244fb402 Android: update dependencies for us
* backported from 33d56dd6fb
  this will need deps from minenux-stuffs site https://gitlab.com/minetest-stuffs/multicraft-deps_androit
2023-09-05 14:04:38 -04:00
Maksym H
9cf737dde1 TouchScreenGUI: minor fixes 2023-09-05 13:45:53 -04:00
a25ed78f6c Update MultiCraft Font - remove broken lang th that seems work
* cherry picked from commit
  27548f8b13
2023-09-05 13:45:04 -04:00
Maksym H
592f225798 Apple: update 2023-09-05 13:42:55 -04:00
Maksym H
0785a0064a MainMenu: remove legacy tab 2023-09-05 13:22:38 -04:00
Daroc Alden
91784d6d04 Fix undefined behavior in TileLayer (#12125)
Initialize the values properly

(cherry picked from commit 8e918bb322ceb8dcb4101740391138191d490e6d)
2023-08-10 23:33:53 -04:00
Maksym H
9db28e0daa MainMenu improvements and cleanup (#141)
Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
(cherry picked from commit 75452a9e9abe14deaf80cd696b056f708cf4f0fb)
2023-08-10 23:00:28 -04:00
Maksym H
365c63e99e Decrease sneak margin to fix phasing through thin walls
(cherry picked from commit 803428eaffc43a698ad5216a7c9b8d71e23bf3ff)
2023-08-10 23:00:08 -04:00
Maksym H
714de48c75 Simplify hasNPotSupport() check
(cherry picked from commit 0b4dda3c4ac110f9b6ee0bab93529108f4d91150)
2023-08-10 22:59:32 -04:00
Maksym H
413367631f Windows: update
(cherry picked from commit 8d4be7181047a91265515ef32529c5edb1b455ad)
2023-08-10 22:58:34 -04:00
luk3yx
2714e5d0f6 Prevent suffocation in plantlike nodes
(cherry picked from commit 73680423f5994e73fdb2e49289309fcf41c6ccfb)
2023-08-10 22:56:37 -04:00
Deve
4157a8c8e1 TouchScreenGUI: handle screen resizing and fix memory leaks
(cherry picked from commit 9c5c0dcd3cef6d6b3bcab11e801d428c511e4bae)
2023-08-10 22:56:22 -04:00
Maksym H
9b03f0a82e Android: minor update
(cherry picked from commit 4e2d7463cda7630d76c8b26c6dc63b2b73f0e3e5)
2023-08-10 22:55:36 -04:00
Maksym H
4c4b00d719 Add hasNPotSupport() check in getTexture() function
(cherry picked from commit 63fa8c286fe09c5f7fc2c9a81be80f1b03d89be4)
2023-08-10 22:55:19 -04:00
Maksym H
c0f745994c macOS: minor update
(cherry picked from commit 01610cb8453077a6877f7b4ac9bacf7327fe282d)
2023-08-10 22:54:16 -04:00
luk3yx
e2726757f7 Log the name of entities that can't be added
(cherry picked from commit 2422c202d38a798f1162fb6aea79e53ed061117d)
2023-08-10 22:54:00 -04:00
luk3yx
d833004edd dlg_create_world.lua: Fix scrollbar slider
(cherry picked from commit 476d65034f50e5af4679cc85791504b884731ad2)
2023-08-10 22:52:36 -04:00
luk3yx
c16954e407 Add no_change_anim group
(cherry picked from commit d12f19fabf3fddc3f3bb83210ea80cf56f00fad8)
2023-08-10 22:52:14 -04:00
Maksym H
478a488348 Android: minor update
(cherry picked from commit d2408a9324f08fde9cb109490bf8fbf04f748531)
2023-08-10 22:50:25 -04:00
Maksym H
6fbceface0 Disable joysticks without SDL2
(cherry picked from commit 9b1209a68a4ae51f38dd2ebc97a7c168e9cacfc8)
2023-08-10 22:50:02 -04:00
luk3yx
e7d88cca42 Add pressed inventory slot background image
(cherry picked from commit f22951ab2b63c691f81d11267ff3e63e70457a14)
2023-08-10 22:48:22 -04:00
luk3yx
035e35c962 Scale statbar background texture correctly when its resoltuion differs
(cherry picked from commit 88c52130f81db79756087ffd95cd38d0ad2756a0)
2023-08-10 22:45:28 -04:00
Maksym H
70df6b60c2 Apple: switch to MultiCraft Font
(cherry picked from commit 58f63ae4a77ff691555f3397263b9d908b6d4456)
2023-08-10 22:45:04 -04:00
Deve
3219db4e89 Add build scripts for Windows using MSYS2 (#130) 2023-08-10 22:44:08 -04:00
Maksym H
732281da4a Remove formspec opacity in the ConfirmRegistration dialog 2023-08-10 22:30:31 -04:00
Deve
8c5e0cb61f Add SDL game controller support (#105) 2023-08-10 21:18:19 -04:00
luk3yx
371c3d3115 Don't crash on mod directories without init.lua 2023-08-10 21:17:52 -04:00
Maksym H
46c24e6c47 Improve the size of the loading progress bar (on GLES) 2023-08-10 21:17:05 -04:00
Deve
fe35c3a31c Add a fix for inventory that can't be open in some cases
The forceUpdateHoveredElement() forces GUI Environment to drop previously
grabbed elements that are not hovered anymore.
2023-08-10 21:11:51 -04:00
luk3yx
1f26bfa168 New world list (#135) 2023-08-10 21:11:10 -04:00
luk3yx
c3f00dedc2 Add list[] bgimg 2023-08-10 21:10:54 -04:00
Maksym H
1150926130 Android: minor update 2023-08-10 20:50:33 -04:00
Maksym H
f69818097b Version 2.0.4-release 2023-08-10 17:52:48 -04:00
Deve
8cd48bd34f Chat console fixes (#133)
* Some fixes for text selection in chat.

- Reset selection on chat open
- Reset selection on new message
- Reset selection when window size changed

* Don't move to bottom when user is scrolling chat

* Some work on prompt selection

* Make it working when text length is larger than max visible length

* Handle touch events for prompt

* Avoid moving chat history on new messages during scrolling.

Also simplify selection marks comparing.

* Move scroll to bottom on sending new message when console is opened without close on enter

* Fixed copy to clipboard when chat buffer is full and also keep text selected when new message arrives

* Some improvements for prompt selection

* Delete selected text on new input or backspace key press

* Fixed text selection and cursor pos for regular font

* Fixed drawing prompt with newlines

* Fixed empty prompt selection with ctrl+a

* Enable chat console on android tablets
2023-08-10 17:52:35 -04:00
Maksym H
f58cf228ae Switch to `ephemeral' sounds of eating and picking up items 2023-08-10 17:52:23 -04:00
Maksym H
6156f4b0a8 Builtin: localize a few functions 2023-08-10 17:52:05 -04:00
Maksym H
4237754178 Update minimap textures 2023-08-10 17:47:21 -04:00
Maksym H
e3b85d1207 Change main menu scrollbar style 2023-08-10 17:47:10 -04:00
luk3yx
a1a38c20af Use separate top/bottom textures for chat scrollbar 2023-08-10 17:46:59 -04:00
Maksym H
25a36b9291 Fix MinGW Action 2023-08-10 17:46:30 -04:00
luk3yx
eded502651 Allow table and textarea scrollbars to be customised (#129) 2023-08-10 17:45:45 -04:00
SmallJoker
28f62eccab Limit formspec fields to 640K (#13380)
Fixes an issue where long inputs could cause issues when dealing with formspecs.
The expected data is usually below 1 KiB, however that's not a technical limit.
2023-08-10 17:45:31 -04:00
luk3yx
6da47b485c Enable strip_color_codes by default 2023-08-10 17:45:12 -04:00
luk3yx
c99e7bd5f0 Improve main menu (#121)
Co-authored-by: Maksym H <Maksym48@pm.me>
2023-08-10 17:44:35 -04:00
Deve
0db262d8be Chat console improvements (#124) 2023-08-10 17:44:11 -04:00
luk3yx
0db84b59d0 New SSCSM minifier 2023-08-10 17:39:35 -04:00
Deve
5e88542be6 Fix left shift key in the change menu (#128)
* Set shift_down false when it's not pressed anymore

* Prefer character only if Char is not 0 which does not happen in SDL anyway
2023-08-10 17:39:13 -04:00
luk3yx
192f52f7c9 Catch InvalidPositionException in get_natural_light 2023-08-10 17:38:52 -04:00
Vincent Robinson
b92aa05c33 Add math.round and fix vector.round (#10803) 2023-08-10 17:38:23 -04:00
luk3yx
2f9c3e791e Make guiVolumeChange use custom scrollbar 2023-08-10 17:38:06 -04:00
Maksym H
cf99ce6b30 Make the console message height configurable 2023-08-10 17:17:14 -04:00
Maksym H
8d38855874 macOS: minor update 2023-08-10 17:10:57 -04:00
93785b726a Use SDL threads (#122) but seems only in androit
* backported from 245220985b
  for minenux
2023-08-10 17:10:36 -04:00
Maksym H
311c059bf8 Android: update dependencies 2023-08-10 17:06:05 -04:00
luk3yx
16a9de2fe2 Add .fast, .fly, .noclip and .pitch chatcommands (#120) 2023-08-10 17:05:56 -04:00
Maksym H
eb122a4e80 Minor engine changes and fixes 2023-08-10 17:05:42 -04:00
Maksym H
e4cb11e84b Temporary fix for MSVC Action 2023-01-24 23:33:04 +02:00
Deve
0df81d3e20
Add a possibility to copy text from non-writable edit box (#118) 2023-01-24 07:42:01 +02:00
luk3yx
22443daf2c SSCSM: Compress long messages 2023-01-18 12:12:50 +13:00
Maksym H
23844d8836 Mobile: minor legacy minimap tune 2023-01-17 12:55:16 +02:00
luk3yx
b5b0917f26
Main menu improvements (#117)
Co-authored-by: Maksym H <Maksym48@pm.me>
2023-01-12 00:37:26 +02:00
Maksym H
af36361f42 Android: rename initializePathsAndroid() to initializePaths() 2023-01-05 02:28:43 +02:00
Maksym H
141559689e Minor TouchScreenGUI fixes 2023-01-03 13:24:04 +02:00
Maksym H
93232becc0 Minor Hunger tune 2023-01-03 13:14:00 +02:00
sfan5
8c205f5bed Make fs::extractZipFile thread-safe 2022-12-21 12:52:26 +13:00
Loic Blot
1f4ff9cea6 fix: extractZipFile is not part of Client but more generic.
This solve a crash from mainmenu while extracting the zip
2022-12-21 12:52:20 +13:00
luk3yx
2a38a33ff8
Disable node_drop by default (#116)
* Drop `item_lava_destroy` builtin function
2022-12-20 09:58:52 +02:00
Maksym H
e8dc878d1f Fix and improve guiConfirmRegistration dialog 2022-12-16 14:32:50 +02:00
luk3yx
c820cb204a
Make MetaDataRef:get return nil instead of nothing (#114)
Co-authored-by: Jude Melton-Houghton <jwmhjwmh@gmail.com>
2022-12-13 08:00:12 +13:00
luk3yx
9985ffb2fc Fix metadata wiping when placing schematics 2022-12-11 13:21:48 +13:00
Maksym H
ccf20998f4
Android: move project files from build folder (#112) 2022-12-07 16:29:25 +02:00
Maksym H
13c4b3bd20
A new batch of MainMenu improvements (#110)
Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
2022-12-07 16:05:14 +02:00
Maksym H
ebe6a07c9a Fix and update Actions 2022-12-05 16:58:02 +02:00
Maksym H
6c5022f692
Apple: move project files from build folder (#111) 2022-12-02 11:40:55 +01:00
luk3yx
c7bb8e5baf Translate error message before clearing translations 2022-11-27 11:09:20 +13:00
Maksym H
da679d619e Android: simplify input dialog 2022-11-24 20:57:03 +02:00
Maksym H
9e83c219e7 Move GUI textures in separate folder 2022-11-23 15:27:06 +02:00
Maksym H
78d9ec0f2a Version 2.0.3-release 2022-11-14 13:03:37 +01:00
Maksym H
3f4adfd6bb Minor changes and fixes 2022-11-14 12:59:33 +01:00
Maksym H
75a5c0876e Disable progress bar texture overrides 2022-11-14 12:49:28 +01:00
Deve
b7a313a574 TouchScreenGUI: Better handle punch/interact 2022-11-14 00:54:03 +01:00
Maksym H
4b4946ca1e Improve debug logging 2022-11-14 00:46:46 +01:00
Maksym H
3d33abf1cd Yet another MainMenu improvements PR (#101)
* Yet another improvements instead of a complete redo

Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
2022-11-14 00:46:46 +01:00
Maksym H
3ce839dc11 macOS: update 2022-11-14 00:46:46 +01:00
Maksym H
a254814f11 TouchScreenGUI: use mouse_sensitivity setting instead of mouse_sensity 2022-11-11 22:24:06 +01:00
luk3yx
df9e57bf05 Add setting to disable texture packs 2022-11-12 10:20:32 +13:00
Bektur
ecc472ab14
Android: update and improve Kotlin part (#99) 2022-11-11 17:45:05 +01:00
Deve
8535658791
Use raw pointer for sound manager (#107)
Use raw pointer for sound manager to avoid a crash in destructor after exit(0)
2022-11-07 10:57:20 +01:00
Bektur
3ecb0895aa
Extend API and add get_secret_key for secure operations (#84)
Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
2022-10-29 13:31:21 +03:00
Maksym H
e9157515b9
Android: add mouse support (#102)
Co-authored-by: Deve <deveee@gmail.com>
2022-10-24 13:31:08 +03:00
Maksym H
8f74118a43 Simplify double jump for fly function 2022-10-21 20:39:38 +03:00
luk3yx
dd5407e0cb
Allow extracting password-protected zips (#104) 2022-10-18 12:25:08 +02:00
Maksym H
1c703d231c Small fix for the font_size mess 2022-10-16 10:57:44 +03:00
Maksym H
cc93ab60b4 Update of luautf8 with Unicode 15 support
https://github.com/starwing/luautf8/releases/tag/0.1.4
2022-10-16 10:38:13 +03:00
luk3yx
b8f3c0c177
Fix /help formspec background 2022-10-12 23:28:30 +03:00
Maksym H
a6b126e91f TouchScreenGUI: add Aux1 button 2022-10-03 10:07:53 +02:00
luk3yx
48e8b205ec
Change language on the fly in the advanced settings dialog (#100) 2022-10-02 20:02:49 +02:00
luk3yx
7f4a384c69 Allow transparency in scrollbar button textures and fix horizontal image scrollbars 2022-10-02 11:32:07 +13:00
Deve
ef72016f28
Android: use Irrlicht with SDL2 device (#77)
Co-authored-by: Maksim <Maksym48@pm.me>
2022-09-29 18:35:21 +02:00
Deve
9d894bc80e
macOS: update and switch off from Cocoapods (#98)
Co-authored-by: Maksym H <Maksym48@pm.me>
2022-09-25 12:10:27 +02:00
Maksym H
d051f3a1b3 Android: minor update 2022-09-25 11:06:27 +02:00
Maksym H
443332f33b Minor code sync 2022-09-23 13:19:07 +02:00
luk3yx
caa7ae51bb
Only show HUD co-ordinates when minimap is showing or debug mode is on (#93) 2022-09-23 15:50:45 +12:00
Maksym H
bfd46c88c8 Minor statbars update 2022-09-20 19:56:11 +02:00
luk3yx
6befa9c8e9 Save settings when closing volume/keys dialogs 2022-09-20 12:10:02 +02:00
Bektur
bd51d690ab
Android: update and add getRoundScreen method (#97) 2022-09-18 02:05:37 +02:00
Bektur
cbb0bc2f53
Simplifying the drawing of the progress bar (#96)
Co-authored-by: Maksym H <Maksym48@pm.me>
2022-09-08 21:28:25 +03:00
luk3yx
4918f6a50f
Improve change keys, change volume and change password menus (#94)
Co-authored-by: Maksym H <Maksym48@pm.me>
2022-09-08 12:52:31 +03:00
luk3yx
087a56cf06 Don't crash with a negative length in particle definitions 2022-09-07 20:51:07 +12:00
Deve
fe5495288e
macOS: use SDL2 (#90) 2022-09-02 00:26:24 +03:00
Maksym H
6e76ef4320
HUD: use the HUD API directly (#92)
Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
2022-08-29 21:20:56 +03:00
Maksim
7624c29d96 Improve HUD text scaling 2022-08-20 22:12:54 +03:00
Maksim
ee2ebbfb91 Improve Pause menu 2022-08-20 22:03:42 +03:00
luk3yx
a6299e1cb5 Remove C++ wielded item status text
This clashes with the status text in some games that implement the same 
thing in Lua
2022-08-14 10:11:50 +12:00
Maksym H
6f87b3855d
Drop intllib and string.trim8 (#88) 2022-08-13 22:39:37 +02:00
luk3yx
d006cb54f5 Fix crash when built with RUN_IN_PLACE=TRUE 2022-08-11 19:05:42 +12:00
Maksim
0810698049 Version 2.0.2-release 2022-08-08 00:31:01 +02:00
Maksim
bd4b420ccd Minor changes and fixes 2022-08-08 00:31:01 +02:00
luk3yx
c658140b4b
Add string.buffer to Lua environment (if it exists) 2022-08-06 23:00:45 +02:00
luk3yx
17f710bfd5 Send SSCSMs in singleplayer and add more APIs to SSCSM sandbox (#72) 2022-08-05 20:53:08 +02:00
AFCMS
fdd2efbbbe Add minetest.settings to CSM API and allow CSMs to provide settingtypes.txt (#12131)
Co-authored-by: sfan5 <sfan5@live.de>
Co-authored-by: SmallJoker <SmallJoker@users.noreply.github.com>
2022-08-04 11:39:06 +12:00
Maksim
7f4d379e02 Update Credits and Readme 2022-08-03 11:40:14 +02:00
sfan5
735b01bd5f Protect a few more settings from being set from mods
Of those settings main_menu_script has concrete security impact, the rest are added out of abundance of caution.
2022-08-03 19:34:20 +12:00
sfan5
36883505da Protect mg_name and mg_flags from being set by Lua (#11010) 2022-08-03 19:34:15 +12:00
luk3yx
7f85835a8f Re-add static_spawn.lua (fixes #81) 2022-08-03 14:01:36 +12:00
luk3yx
fb78a8a0cd
Add update reminder (#49)
Co-authored-by: Maksym <Maksym48@pm.me>
Co-authored-by: ubulem <berkut87@gmail.com>
2022-08-02 09:48:01 +02:00
x2048
0fed6077fe Use legacy image implementation (no NNAA filter) when not using 9-slice image (#12608) 2022-08-01 17:33:59 +12:00
luk3yx
65dc4b42d3
Mobile: add change language dropdown and reset settings button (#75) 2022-07-29 13:12:22 +02:00
Maksym H
1c19f6069f
Drop the mobile_friendly server feature (#79) 2022-07-28 17:53:11 +02:00
luk3yx
e22b748ce9 Only log large invalid JSON strings when built in debug mode 2022-07-22 19:04:49 +12:00
luk3yx
d31d2387f3
MainMenu: some changes and fixes (#68)
Co-authored-by: Maksim <Maksym48@pm.me>
2022-07-18 23:09:48 +03:00
Maksim
17ebe562a3 Android: minor update 2022-07-13 14:07:19 +03:00
Wuzzy
f9d97b5d05 Tweak duration_to_string formatting 2022-07-12 00:07:19 +03:00
luk3yx
357d3aaf8e
Make /setspawn world specific (#67) 2022-07-06 22:15:27 +03:00
Maksim
7b0794d243 Temporary fix for the transparent particles glitch 2022-07-03 23:35:56 +03:00
Vincent Robinson
0bfc98fe26 Backport (II): "FormSpec: 9-slice images, animated_images, and fgimg_middle (#12453)"
* FormSpec: 9-slice images and animated_images

* Add fgimg_middle; clean up code

* Address issues, add tests

* Fix stupid error; bump formspec version

* Re-add image[] elements without a size
2022-07-03 21:31:56 +03:00
luk3yx
f09359eb58
Fix drowning in minetest_game doors (#66)
Fixes #65
2022-07-02 22:32:53 +03:00
luk3yx
d4f6bf31c2 Fix some main menu bugs and improve UI (#63)
Co-authored-by: Maksim <Maksym48@pm.me>
2022-06-26 23:45:45 +03:00
luk3yx
3cf9c06ae4
Mobile: don't stretch formspec to entire screen if it has a tabheader (#62) 2022-06-22 09:00:28 +03:00
Maksym
3991e90bbc Update MainMenu and replace tabs on buttons (#61)
Co-authored-by: luk3yx <luk3yx@users.noreply.github.com>
2022-06-17 20:39:48 +03:00
Maksim
fbd1c6b88d Backport: "FormSpec: 9-slice images, animated_images, and fgimg_middle (#10265)"
Co-Authored-By: Vincent Robinson <robinsonvincent89@gmail.com>
2022-06-15 10:45:38 +03:00
luk3yx
d6c82c3f7c Improve main menu (#60)
Co-authored-by: Maksim <Maksym48@pm.me>
2022-06-14 12:02:40 +03:00
paradust7
e621d5b02c Inline triLinearInterpolationNoEase and triLinearInterpolation (#12421)
Performance profiling on Linux AMD64 showed this to be a significant bottleneck. The non-inlined functions are expensive due to XMM registers spilling onto the stack.
2022-06-13 17:16:45 +12:00
Maksim
e66d7fd516 Android: fix few crashes 2022-06-12 19:13:47 +03:00
612 changed files with 18610 additions and 8007 deletions

View File

@ -6,9 +6,9 @@ labels: Unconfirmed bug
assignees: '' assignees: ''
--- ---
##### Minetest version ##### MultiCraft version
<!-- <!--
Paste Minetest version between quotes below Paste MultiCraft version between quotes below
If you are on a devel version, please add git commit hash If you are on a devel version, please add git commit hash
You can use `minetest --version` to find it. You can use `minetest --version` to find it.
--> -->

View File

@ -8,7 +8,7 @@ on:
- 'lib/**.cpp' - 'lib/**.cpp'
- 'src/**.[ch]' - 'src/**.[ch]'
- 'src/**.cpp' - 'src/**.cpp'
- 'build/android/**' - 'Android/**'
- '.github/workflows/android.yml' - '.github/workflows/android.yml'
pull_request: pull_request:
paths: paths:
@ -16,7 +16,7 @@ on:
- 'lib/**.cpp' - 'lib/**.cpp'
- 'src/**.[ch]' - 'src/**.[ch]'
- 'src/**.cpp' - 'src/**.cpp'
- 'build/android/**' - 'Android/**'
- '.github/workflows/android.yml' - '.github/workflows/android.yml'
jobs: jobs:
@ -24,22 +24,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '11' java-version: '17'
- name: Install GNU gettext - name: Install GNU gettext
run: sudo apt install gettext run: sudo apt install gettext
- name: Build with Gradle - name: Build with Gradle
run: cd build/android; ./gradlew assemblerelease run: cd Android; ./gradlew assemblerelease
- name: Save armeabi artifact - name: Save armeabi artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: MultiCraft-armeabi-v7a.apk name: MultiCraft-armeabi-v7a.apk
path: build/android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk path: Android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk
- name: Save arm64 artifact - name: Save arm64 artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: MultiCraft-arm64-v8a.apk name: MultiCraft-arm64-v8a.apk
path: build/android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk path: Android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk

View File

@ -48,22 +48,22 @@ jobs:
# run: | # run: |
# ./bin/multicraft --run-unittests # ./bin/multicraft --run-unittests
# This is the current gcc compiler (available in bionic) # This is the current gcc compiler (available in jammy)
gcc_8: gcc_10:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install deps - name: Install deps
run: | run: |
source ./util/ci/common.sh source ./util/ci/common.sh
install_linux_deps g++-8 install_linux_deps g++-10
- name: Build - name: Build
run: | run: |
./util/ci/build.sh ./util/ci/build.sh
env: env:
CC: gcc-8 CC: gcc-10
CXX: g++-8 CXX: g++-10
- name: Test - name: Test
run: | run: |
@ -92,30 +92,30 @@ jobs:
# ./bin/multicraft --run-unittests # ./bin/multicraft --run-unittests
# This is the current clang version # This is the current clang version
clang_9: clang_11:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install deps - name: Install deps
run: | run: |
source ./util/ci/common.sh source ./util/ci/common.sh
install_linux_deps clang-9 valgrind libluajit-5.1-dev install_linux_deps clang-11 valgrind libluajit-5.1-dev
- name: Build - name: Build
run: | run: |
./util/ci/build.sh ./util/ci/build.sh
env: env:
CC: clang-9 CC: clang-11
CXX: clang++-9 CXX: clang++-11
CMAKE_FLAGS: "-DREQUIRE_LUAJIT=1" CMAKE_FLAGS: "-DREQUIRE_LUAJIT=1"
- name: Test - name: Test
run: | run: |
./bin/multicraft --run-unittests ./bin/multicraft --run-unittests
- name: Valgrind # - name: Valgrind
run: | # run: |
valgrind --leak-check=full --leak-check-heuristics=all --undef-value-errors=no --error-exitcode=9 ./bin/multicraft --run-unittests # valgrind --leak-check=full --leak-check-heuristics=all --undef-value-errors=no --error-exitcode=9 ./bin/multicraft --run-unittests
# Build with prometheus-cpp (server-only) # Build with prometheus-cpp (server-only)
# clang_9_prometheus: # clang_9_prometheus:
@ -171,18 +171,18 @@ jobs:
docker: docker:
name: "Docker image" name: "Docker image"
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Build docker image - name: Build docker image
run: | run: |
docker build . docker build .
win32: win32:
name: "MinGW cross-compiler (32-bit)" name: "MinGW cross-compiler (32-bit)"
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install compiler - name: Install compiler
run: | run: |
sudo apt-get update -q && sudo apt-get install gettext -qyy sudo apt-get update -q && sudo apt-get install gettext -qyy
@ -198,9 +198,9 @@ jobs:
win64: win64:
name: "MinGW cross-compiler (64-bit)" name: "MinGW cross-compiler (64-bit)"
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install compiler - name: Install compiler
run: | run: |
sudo apt-get update -q && sudo apt-get install gettext -qyy sudo apt-get update -q && sudo apt-get install gettext -qyy
@ -214,25 +214,67 @@ jobs:
NO_MINETEST_GAME: 1 NO_MINETEST_GAME: 1
NO_PACKAGE: 1 NO_PACKAGE: 1
msys2-mingw64:
name: "MSYS2-MinGW (64-bit)"
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: mingw-w64-x86_64-clang mingw-w64-x86_64-cmake mingw-w64-x86_64-autotools mingw-w64-x86_64-tcl git zip
- name: Build
run: |
cd ./Windows
./Start.sh
cmake --build . -j
cd -
strip -s ./bin/multicraft.exe
- name: Create ZIP
run: |
mkdir MultiCraft
cp -r ./bin MultiCraft/
cp -r ./builtin MultiCraft/
cp -r ./client MultiCraft/
cp -r ./fonts MultiCraft/
cp -r ./locale MultiCraft/
cp -r ./textures MultiCraft/
zip -r MultiCraft.zip MultiCraft
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: MultiCraft-msys2-mingw64
path: ./MultiCraft.zip
msvc: msvc:
name: VS 2019 ${{ matrix.config.arch }}-${{ matrix.type }} name: VS 2022 ${{ matrix.config.arch }}-${{ matrix.type }}
runs-on: windows-2019 runs-on: windows-2022
env: env:
VCPKG_VERSION: af2287382b1991dbdcb7e5112d236f3323b9dd7a VCPKG_VERSION: a42af01b72c28a8e1d7b48107b33e4f286a55ef6
# 2022.03.10 # 2023.11.20
vcpkg_packages: irrlicht zlib curl[winssl] openal-soft libvorbis libogg sqlite3 freetype luajit libiconv gettext jsoncpp vcpkg_packages: irrlicht zlib curl[winssl] openal-soft libvorbis libogg sqlite3 freetype luajit gmp jsoncpp
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
config: config:
- { - {
arch: x86, arch: x86,
generator: "-G'Visual Studio 16 2019' -A Win32", generator: "-G'Visual Studio 17 2022' -A Win32",
vcpkg_triplet: x86-windows vcpkg_triplet: x86-windows
} }
- { - {
arch: x64, arch: x64,
generator: "-G'Visual Studio 16 2019' -A x64", generator: "-G'Visual Studio 17 2022' -A x64",
vcpkg_triplet: x64-windows vcpkg_triplet: x64-windows
} }
type: [portable] type: [portable]
@ -242,7 +284,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Restore from cache and run vcpkg - name: Restore from cache and run vcpkg
uses: lukka/run-vcpkg@v7 uses: lukka/run-vcpkg@v7
@ -259,7 +301,6 @@ jobs:
-DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" ` -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" `
-DCMAKE_BUILD_TYPE=Release ` -DCMAKE_BUILD_TYPE=Release `
-DENABLE_POSTGRESQL=OFF ` -DENABLE_POSTGRESQL=OFF `
-DENABLE_SYSTEM_JSONCPP=ON `
-DRUN_IN_PLACE=${{ contains(matrix.type, 'portable') }} . -DRUN_IN_PLACE=${{ contains(matrix.type, 'portable') }} .
- name: Build - name: Build
@ -283,5 +324,5 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: msvc-${{ matrix.config.arch }}-${{ matrix.type }} name: MultiCraft-msvc-${{ matrix.config.arch }}-${{ matrix.type }}
path: .\Package\ path: .\Package\

View File

@ -25,27 +25,27 @@ on:
jobs: jobs:
clang_format: clang_format:
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install clang-format - name: Install clang-format
run: | run: |
sudo apt-get install clang-format-9 -qyy sudo apt-get install clang-format-11 -qyy
- name: Run clang-format - name: Run clang-format
run: | run: |
source ./util/ci/lint.sh source ./util/ci/lint.sh
perform_lint perform_lint
env: env:
CLANG_FORMAT: clang-format-9 CLANG_FORMAT: clang-format-11
clang_tidy: clang_tidy:
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Install deps - name: Install deps
run: | run: |
sudo apt-get install clang-tidy-9 -qyy sudo apt-get install clang-tidy-11 -qyy
source ./util/ci/common.sh source ./util/ci/common.sh
install_linux_deps install_linux_deps

View File

@ -14,7 +14,7 @@ on:
jobs: jobs:
luacheck: luacheck:
name: "Builtin Luacheck and Unit Tests" name: "Builtin Luacheck and Unit Tests"
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: leafo/gh-actions-lua@v9 - uses: leafo/gh-actions-lua@v9

3
.gitignore vendored
View File

@ -41,6 +41,7 @@ build/.cmake/
/bin/ /bin/
/games/* /games/*
!/games/devtest/ !/games/devtest/
!/games/minetest
/cache /cache
/textures/* /textures/*
!/textures/base/ !/textures/base/
@ -55,8 +56,6 @@ build/.cmake/
/clientmods/* /clientmods/*
!/clientmods/preview/ !/clientmods/preview/
/client/mod_storage/ /client/mod_storage/
/builtin/mainmenu/hosting/
/textures/base/pack/hosting/
## Configuration/log files ## Configuration/log files
multicraft.conf multicraft.conf

View File

@ -1,293 +1,216 @@
--- ---
# Github repository is cloned every day on Gitlab.com # Github repository is really at minetest.org using the poikilos git.minetest.io
# https://gitlab.com/minetest/minetest # https://gitlab.com/minenux/minetest-engine-minetest
# Pipelines URL: https://gitlab.com/minetest/minetest/pipelines # Pipelines URL: https://gitlab.com/minenux/minetest-engine-minetest/pipelines
# packages moved to https://build.opensuse.org/project/show/home:venenux:minenux
# in future we only build here, or made apk packs for alpine
variables:
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_DEPTH: 1
GIT_STRATEGY: clone
stages: stages:
- build - build
- package
- deploy
variables: .build_template: &build_definition
MINETEST_GAME_REPO: "https://github.com/minetest/minetest_game.git"
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
.build_template:
stage: build stage: build
script: script:
- mkdir cmakebuild - mkdir cmakebuild
- mkdir -p artifact/minetest/usr/ - mkdir -p artifact/multicraft/usr/
- cd cmakebuild - cd cmakebuild
- cmake -DCMAKE_INSTALL_PREFIX=../artifact/minetest/usr/ -DCMAKE_BUILD_TYPE=Release -DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE -DENABLE_SYSTEM_JSONCPP=TRUE -DBUILD_SERVER=TRUE .. - cmake -DCMAKE_INSTALL_PREFIX=../artifact/multicraft/usr/ -DBUILD_SERVER=ON -DBUILD_CLIENT=ON -DRUN_IN_PLACE=OFF -DENABLE_CURL=ON -DENABLE_SOUND=ON -DENABLE_LUAJIT=ON -DENABLE_GETTEXT=ON -DENABLE_FREETYPE=ON -DENABLE_SYSTEM_GMP=ON -DENABLE_SYSTEM_JSONCPP=ON -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_POSTGRESQL=ON ..
- make -j2 - make -j$(nproc)
- make install - make install
artifacts: artifacts:
when: on_success when: on_success
expire_in: 1h expire_in: 1y
paths: paths:
- artifact/* - artifact/*
.debpkg_template: ##
stage: package ## Alpine the limited distro for nonsocial geeks
before_script: ##
- apt-get update -y
- apt-get install -y git
- mkdir -p build/deb/minetest/DEBIAN/
- cp misc/debpkg-control build/deb/minetest/DEBIAN/control
- cp -a artifact/minetest/usr build/deb/minetest/
script:
- git clone $MINETEST_GAME_REPO build/deb/minetest/usr/share/minetest/games/minetest_game
- rm -rf build/deb/minetest/usr/share/minetest/games/minetest/.git
- sed -i 's/DATEPLACEHOLDER/'$(date +%y.%m.%d)'/g' build/deb/minetest/DEBIAN/control
- sed -i 's/LEVELDB_PLACEHOLDER/'$LEVELDB_PKG'/g' build/deb/minetest/DEBIAN/control
- cd build/deb/ && dpkg-deb -b minetest/ && mv minetest.deb ../../
artifacts:
expire_in: 90 day
paths:
- ./*.deb
.debpkg_install: build:alpine-312:
stage: deploy extends: .build_template
image: alpine:3.12
before_script: before_script:
- apt-get update -y - apk update
script: - apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
- apt-get install -y ./*.deb
- minetest --version build:alpine-314:
extends: .build_template
image: alpine:3.14
before_script:
- apk update
- apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
build:alpine-316:
extends: .build_template
image: alpine:3.16
before_script:
- apk update
- apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
build:alpine-319:
extends: .build_template
image: alpine:3.19
before_script:
- apk update
- apk add build-base cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl-dev samurai
# error - need to enable testing repo due spatial index
#build:alpine-edge:
# extends: .build_template
# image: alpine:edge
# before_script:
# - apk update
# - apk add build-base git cmake pkgconf gettext-dev bzip2-dev curl-dev libnl3-dev rtmpdump-dev libidn2-dev ncurses-dev freetype-dev mesa-dev gmp-dev irrlicht-dev libjpeg-turbo-dev jsoncpp-dev leveldb-dev luajit-dev lua5.1-dev libogg-dev openal-soft-dev libpng-dev postgresql-dev hiredis-dev sqlite-dev libvorbis-dev libxi-dev zlib-dev doxygen libxrandr-dev libx11-dev zstd-dev openssl1.1-compat-dev samurai libspatialindex-dev
## ##
## Debian ## Debian mother of many distros
## ##
# Jessie
build:debian-8-64:
<<: *build_definition
image: amd64/debian:8
before_script:
- echo "" > /etc/apt/apt.conf.d/50venenuxcustom
- echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::AllowDowngradeToInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::AllowReleaseInfoChange::Suite \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::Check-Valid-Until \"false\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::Languages \"en\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Aptitude::CmdLine::Ignore-Trust-Violations \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- rm -rf /etc/apt/sources.list
- echo "deb http://archive.debian.org/debian/ jessie main contrib" > /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://archive.debian.org/debian/ jessie-backports main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://deb.freexian.com/extended-lts jessie main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
- DEBIAN_FRONTEND=noninteractive apt-get update -y || true
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes install build-essential cmake pkg-config cmake-data debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev libpq-dev postgresql-server-dev-all libhiredis-dev zlib1g-dev doxygen libxrandr-dev x11proto-xf86vidmode-dev
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes -t jessie-backports install libjsoncpp-dev
build:debian-8-32:
<<: *build_definition
image: i386/debian:8
before_script:
- echo "" > /etc/apt/apt.conf.d/50venenuxcustom
- echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::AllowDowngradeToInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::AllowReleaseInfoChange::Suite \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::Check-Valid-Until \"false\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::Languages \"en\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Aptitude::CmdLine::Ignore-Trust-Violations \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- rm -rf /etc/apt/sources.list
- echo "deb http://archive.debian.org/debian/ jessie main contrib" > /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://archive.debian.org/debian/ jessie-backports main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://deb.freexian.com/extended-lts jessie main contrib non-free" >> /etc/apt/sources.list.d/50debianoficial.list
- DEBIAN_FRONTEND=noninteractive apt-get update -y || true
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes install build-essential cmake pkg-config cmake-data debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev libpq-dev postgresql-server-dev-all libhiredis-dev zlib1g-dev doxygen libxrandr-dev x11proto-xf86vidmode-dev
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes -t jessie-backports install libjsoncpp-dev
# Stretch # Stretch
build:debian-9: build:debian-9-32:
extends: .build_template <<: *build_definition
image: debian:9 image: i386/debian:9
before_script: before_script:
- apt-get update -y - echo "" > /etc/apt/apt.conf.d/50venenuxcustom
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev - echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- echo "deb http://archive.debian.org/debian/ stretch main contrib" > /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://deb.freexian.com/extended-lts stretch main contrib non-free" > /etc/apt/sources.list
- DEBIAN_FRONTEND=noninteractive apt-get update -y || true
- DEBIAN_FRONTEND=noninteractive apt-get -y --force-yes install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
package:debian-9: # Bullseye
extends: .debpkg_template
image: debian:9
needs:
- build:debian-9
variables:
LEVELDB_PKG: libleveldb1v5
deploy:debian-9: build:debian-11:
extends: .debpkg_install <<: *build_definition
image: debian:9 image: debian:11
needs:
- package:debian-9
# Buster
build:debian-10:
extends: .build_template
image: debian:10
before_script: before_script:
- apt-get update -y - apt-get update -y || true
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev - apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev postgresql-server-dev-all libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
package:debian-10: # Bookworm
extends: .debpkg_template
image: debian:10
needs:
- build:debian-10
variables:
LEVELDB_PKG: libleveldb1d
deploy:debian-10: build:debian-12:
extends: .debpkg_install <<: *build_definition
image: debian:10 image: debian:12
needs:
- package:debian-10
##
## Ubuntu
##
# Xenial
build:ubuntu-16.04:
extends: .build_template
image: ubuntu:xenial
before_script: before_script:
- apt-get update -y - apt-get update -y || true
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev - apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
package:ubuntu-16.04: ##
extends: .debpkg_template ## winbuntu the distro for stupid users
image: ubuntu:xenial ##
needs:
- build:ubuntu-16.04
variables:
LEVELDB_PKG: libleveldb1v5
deploy:ubuntu-16.04: build:ubuntu-22.04:
extends: .debpkg_install <<: *build_definition
image: ubuntu:xenial image: ubuntu:jammy
needs:
- package:ubuntu-16.04
# Bionic
build:ubuntu-18.04:
extends: .build_template
image: ubuntu:bionic
before_script: before_script:
- apt-get update -y - DEBIAN_FRONTEND=noninteractive apt-get update -y
- apt-get -y install build-essential libirrlicht-dev cmake libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev - DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
package:ubuntu-18.04: # Focal most close to bullseye
extends: .debpkg_template
image: ubuntu:bionic
needs:
- build:ubuntu-18.04
variables:
LEVELDB_PKG: libleveldb1v5
deploy:ubuntu-18.04: build:ubuntu-20.04:
extends: .debpkg_install <<: *build_definition
image: ubuntu:bionic image: ubuntu:focal
needs:
- package:ubuntu-18.04
##
## Fedora
##
# Fedora 28 <-> RHEL 8
build:fedora-28:
extends: .build_template
image: fedora:28
before_script: before_script:
- dnf -y install make automake gcc gcc-c++ kernel-devel cmake libcurl-devel openal-soft-devel libvorbis-devel libXxf86vm-devel libogg-devel freetype-devel mesa-libGL-devel zlib-devel jsoncpp-devel irrlicht-devel gmp-devel sqlite-devel luajit-devel leveldb-devel ncurses-devel spatialindex-devel - DEBIAN_FRONTEND=noninteractive apt-get update -y
- DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
## # yakkety most close to jessie
## MinGW for Windows
##
.generic_win_template: build:ubuntu-16.10:
image: ubuntu:bionic <<: *build_definition
image: ubuntu:yakkety
before_script: before_script:
- apt-get update -y - echo "" > /etc/apt/apt.conf.d/50venenuxcustom
- apt-get install -y wget xz-utils unzip git cmake gettext - echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- wget -nv http://minetest.kitsunemimi.pw/mingw-w64-${WIN_ARCH}_9.2.0_ubuntu18.04.tar.xz -O mingw.tar.xz - echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- tar -xaf mingw.tar.xz -C /usr - rm -rf /etc/apt/sources.list
- echo "deb http://old-releases.ubuntu.com/ubuntu/ yakkety main restricted universe multiverse" > /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://old-releases.ubuntu.com/ubuntu/ yakkety-updates main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://old-releases.ubuntu.com/ubuntu yakkety-security main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
- apt-get update -y || true
- apt-get -y --force-yes install build-essential cmake pkg-config debhelper lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev
.build_win_template: # Zesty most close to stretch
extends: .generic_win_template
stage: build
artifacts:
expire_in: 1h
paths:
- build/minetest/_build/*
.package_win_template: build:ubuntu-17.04:
extends: .generic_win_template <<: *build_definition
stage: package image: ubuntu:zesty
script:
- unzip build/minetest/_build/minetest-*.zip
- cp -p /usr/${WIN_ARCH}-w64-mingw32/bin/libgcc*.dll minetest-*-win*/bin/
- cp -p /usr/${WIN_ARCH}-w64-mingw32/bin/libstdc++*.dll minetest-*-win*/bin/
- cp -p /usr/${WIN_ARCH}-w64-mingw32/bin/libwinpthread*.dll minetest-*-win*/bin/
artifacts:
expire_in: 90 day
paths:
- minetest-*-win*/*
build:win32:
extends: .build_win_template
script:
- ./util/buildbot/buildwin32.sh build
variables:
WIN_ARCH: "i686"
package:win32:
extends: .package_win_template
needs:
- build:win32
variables:
WIN_ARCH: "i686"
build:win64:
extends: .build_win_template
script:
- ./util/buildbot/buildwin64.sh build
variables:
WIN_ARCH: "x86_64"
package:win64:
extends: .package_win_template
needs:
- build:win64
variables:
WIN_ARCH: "x86_64"
##
## Docker
##
package:docker:
stage: package
image: docker:stable
services:
- docker:dind
before_script: before_script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com - echo "" > /etc/apt/apt.conf.d/50venenuxcustom
script: - echo "APT::Get::AllowUnauthenticated \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- docker build . -t ${CONTAINER_IMAGE}/server:$CI_COMMIT_SHA -t ${CONTAINER_IMAGE}/server:$CI_COMMIT_REF_NAME -t ${CONTAINER_IMAGE}/server:latest - echo "Acquire::AllowInsecureRepositories \"true\";" >> /etc/apt/apt.conf.d/50venenuxcustom
- docker push ${CONTAINER_IMAGE}/server:$CI_COMMIT_SHA - rm -rf /etc/apt/sources.list
- docker push ${CONTAINER_IMAGE}/server:$CI_COMMIT_REF_NAME - echo "deb http://old-releases.ubuntu.com/ubuntu/ zesty main restricted universe multiverse" > /etc/apt/sources.list.d/50debianoficial.list
- docker push ${CONTAINER_IMAGE}/server:latest - echo "deb http://old-releases.ubuntu.com/ubuntu/ zesty-updates main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
- echo "deb http://old-releases.ubuntu.com/ubuntu zesty-security main restricted universe multiverse" >> /etc/apt/sources.list.d/50debianoficial.list
- apt-get update -y || true
- apt-get -y --force-yes install build-essential cmake pkg-config debhelper dh-systemd dh-autoreconf lsb-release gettext libbz2-dev libcurl4-gnutls-dev libnl-genl-3-dev libnl-3-dev librtmp-dev libidn11-dev libncurses-dev libfreetype6-dev libglu1-mesa-dev libgmp-dev libirrlicht-dev libjpeg-dev libjsoncpp-dev libleveldb-dev libluajit-5.1-dev liblua5.1-dev libogg-dev libopenal-dev libpng-dev libpq-dev libhiredis-dev libspatialindex-dev libsqlite3-dev libvorbis-dev libx11-dev libxxf86vm-dev postgresql-server-dev-all libpq-dev libhiredis-dev zlib1g-dev doxygen libxrandr-dev mesa-common-dev x11proto-xf86vidmode-dev libzstd-dev
## ##
## Gitlab Pages (Lua API documentation) ## Feladora shit distro
## ##
pages: build:fedora-36:
stage: deploy <<: *build_definition
image: python:3.8 image: fedora:36
before_script: before_script:
- pip install git+https://github.com/Python-Markdown/markdown.git - dnf -y install make automake gcc gcc-c++ kernel-devel cmake pkgconfig bzip2-devel gettext-devel sqlite-devel zlib-devel libpng-devel libjpeg-turbo-devel libXxf86vm-devel mesa-libGL-devel irrlicht-devel desktop-file-utils systemd openal* libvorbis* jsoncpp-devel libcurl-devel libcurl luajit-devel leveldb-devel gmp-devel libappstream-glib freetype-devel spatialindex-devel openssl-devel libogg-devel hiredis-devel libzstd-devel libXi-devel ncurses-devel doxygen
- pip install git+https://github.com/mkdocs/mkdocs.git
- pip install pygments
script:
- cd doc/mkdocs && ./build.sh
artifacts:
paths:
- public
only:
- master
## build:fedora-37:
## AppImage <<: *build_definition
## image: fedora:37
package:appimage-client:
stage: package
image: appimagecrafters/appimage-builder
needs:
- build:ubuntu-18.04
before_script: before_script:
- apt-get update -y - dnf -y install make automake gcc gcc-c++ kernel-devel cmake pkgconfig bzip2-devel gettext-devel sqlite-devel zlib-devel libpng-devel libjpeg-turbo-devel libXxf86vm-devel mesa-libGL-devel irrlicht-devel desktop-file-utils systemd openal* libvorbis* jsoncpp-devel libcurl-devel libcurl luajit-devel leveldb-devel gmp-devel libappstream-glib freetype-devel spatialindex-devel openssl-devel libogg-devel hiredis-devel libzstd-devel libXi-devel ncurses-devel doxygen
- apt-get install -y git wget
# Collect files
- mkdir AppDir
- cp -a artifact/minetest/usr/ AppDir/usr/
- rm AppDir/usr/bin/minetestserver
- cp -a clientmods AppDir/usr/share/minetest
script:
- git clone $MINETEST_GAME_REPO AppDir/usr/share/minetest/games/minetest_game
- rm -rf AppDir/usr/share/minetest/games/minetest/.git
- export VERSION=$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA
# Remove PrefersNonDefaultGPU property due to validation errors
- sed -i '/PrefersNonDefaultGPU/d' AppDir/usr/share/applications/net.minetest.minetest.desktop
- appimage-builder --skip-test
artifacts:
expire_in: 90 day
paths:
- ./*.AppImage

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "games/minetest"]
path = games/minetest
url = https://codeberg.org/minenux/minetest-game-minetest
branch = stable-5.2

View File

@ -20,7 +20,7 @@ read_globals = {
string = {fields = {"split", "trim"}}, string = {fields = {"split", "trim"}},
table = {fields = {"copy", "getn", "indexof", "insert_all"}}, table = {fields = {"copy", "getn", "indexof", "insert_all"}},
math = {fields = {"hypot"}}, math = {fields = {"hypot", "round"}},
} }
globals = { globals = {

View File

@ -2,13 +2,13 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
android { android {
compileSdkVersion 32 compileSdkVersion 34
buildToolsVersion '32.0.0' buildToolsVersion '34.0.0'
ndkVersion '23.1.7779620' ndkVersion '25.2.9519653'
defaultConfig { defaultConfig {
applicationId 'com.multicraft.game' applicationId 'com.multicraft.game'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 32 targetSdkVersion 34
versionName "${versionMajor}.${versionMinor}.${versionPatch}" versionName "${versionMajor}.${versionMinor}.${versionPatch}"
versionCode project.versionCode versionCode project.versionCode
} }
@ -47,26 +47,29 @@ android {
abi { abi {
enable true enable true
reset() reset()
include 'armeabi-v7a', 'arm64-v8a' //noinspection ChromeOsAbiSupport
include 'armeabi-v7a', 'arm64-v8a', 'x86_64'
} }
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions.jvmTarget = "17"
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
namespace = "com.multicraft.game"
} }
import com.android.build.OutputFile
import org.apache.tools.ant.taskdefs.condition.Os import org.apache.tools.ant.taskdefs.condition.Os
task prepareAssetsFiles() { tasks.register('prepareAssetsFiles') {
def assetsFolder = "build/assets/Files" def assetsFolder = "build/assets/Files"
def projRoot = "../../.." def projRoot = "../.."
copy { copy {
from "${projRoot}/builtin" into "${assetsFolder}/builtin" exclude '*.txt' from "${projRoot}/builtin" into "${assetsFolder}/builtin" exclude '*.txt'
@ -75,7 +78,7 @@ task prepareAssetsFiles() {
from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders" from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders"
} }
copy { copy {
from "../native/deps/Android/Irrlicht/shaders" into "${assetsFolder}/client/shaders/Irrlicht" from "../native/deps/irrlicht/shaders" into "${assetsFolder}/client/shaders/Irrlicht"
} }
copy { copy {
from "${projRoot}/fonts/MultiCraftFont.ttf" into "${assetsFolder}/fonts" from "${projRoot}/fonts/MultiCraftFont.ttf" into "${assetsFolder}/fonts"
@ -92,40 +95,25 @@ task prepareAssetsFiles() {
copy { copy {
from "${projRoot}/textures" into "${assetsFolder}/textures" exclude '*.txt' from "${projRoot}/textures" into "${assetsFolder}/textures" exclude '*.txt'
} }
task zipAssetsFiles(type: Zip) {
archiveFileName = "Files.zip"
destinationDirectory = file("src/main/assets/data")
from "${assetsFolder}"
}
} }
task prepareAssetsGames() { tasks.register('zipAssetsFiles', Zip) {
def assetsFolder = "build/assets/games" dependsOn prepareAssetsFiles
def projRoot = "../../.." archiveFileName = 'assets.zip'
def gameToCopy = "default" destinationDirectory = file('src/main/assets')
from('build/assets/Files')
copy {
from "${projRoot}/games/${gameToCopy}" into "${assetsFolder}/games/${gameToCopy}"
} }
task zipAssetsGames(type: Zip) { tasks.named("preBuild") {
archiveFileName = "games.zip" dependsOn(zipAssetsFiles)
destinationDirectory = file("src/main/assets/data")
from "${assetsFolder}"
} }
}
preBuild.dependsOn zipAssetsFiles
preBuild.dependsOn zipAssetsGames
// Map for the version code that gives each ABI a value. // Map for the version code that gives each ABI a value.
def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1] def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1, 'x86_64': 2]
android.applicationVariants.all { variant -> android.applicationVariants.configureEach { variant ->
variant.outputs.each { variant.outputs.each {
output -> output ->
def abiName = output.getFilter(OutputFile.ABI) def abiName = output.filters[0].identifier
output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode
} }
} }
@ -135,9 +123,10 @@ dependencies {
implementation project(':native') implementation project(':native')
/* Third-party libraries */ /* Third-party libraries */
implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.appcompat:appcompat-resources:1.4.1' implementation 'androidx.appcompat:appcompat-resources:1.6.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' implementation("androidx.browser:browser:1.6.0")
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.work:work-runtime-ktx:2.8.1'
implementation 'com.google.android.material:material:1.10.0'
} }

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.multicraft.game"
android:installLocation="auto"> android:installLocation="auto">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -28,6 +27,7 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -41,8 +41,8 @@
android:value="3.0" /> android:value="3.0" />
<activity <activity
android:name="com.multicraft.game.MainActivity" android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|navigation|screenSize" android:configChanges="orientation|keyboardHidden|navigation|screenSize|screenLayout"
android:exported="true" android:exported="true"
android:maxAspectRatio="3.0" android:maxAspectRatio="3.0"
android:screenOrientation="sensorLandscape" android:screenOrientation="sensorLandscape"
@ -54,8 +54,8 @@
</activity> </activity>
<activity <activity
android:name="com.multicraft.game.GameActivity" android:name=".GameActivity"
android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize" android:configChanges="orientation|keyboard|keyboardHidden|navigation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTask" android:launchMode="singleTask"
@ -71,6 +71,19 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".dialogs.ConnectionDialog"
android:configChanges="orientation|keyboardHidden|navigation|screenSize"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:theme="@style/CustomDialog" />
<activity
android:name=".dialogs.RestartDialog"
android:configChanges="orientation|keyboardHidden|navigation|screenSize"
android:exported="false"
android:screenOrientation="sensorLandscape"
android:theme="@style/CustomDialog" />
</application> </application>
</manifest> </manifest>

View File

@ -1,7 +1,7 @@
/* /*
MultiCraft MultiCraft
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua> Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com> Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by it under the terms of the GNU Lesser General Public License as published by

View File

@ -0,0 +1,307 @@
/*
MultiCraft
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3.0 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.multicraft.game
import android.content.res.Configuration
import android.net.Uri
import android.os.*
import android.text.InputType
import android.view.*
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_OFF
import com.multicraft.game.MainActivity.Companion.radius
import com.multicraft.game.databinding.*
import com.multicraft.game.helpers.*
import com.multicraft.game.helpers.ApiLevelHelper.isOreo
import org.libsdl.app.SDLActivity
import java.util.*
import kotlin.system.exitProcess
class GameActivity : SDLActivity() {
companion object {
var isMultiPlayer = false
var isInputActive = false
@JvmStatic
external fun pauseGame()
@JvmStatic
external fun keyboardEvent(keyboard: Boolean)
}
private var messageReturnValue = ""
private var hasKeyboard = false
override fun getLibraries() = arrayOf("MultiCraft")
override fun getMainSharedObject() =
"${getContext().applicationInfo.nativeLibraryDir}/libMultiCraft.so"
override fun onCreate(savedInstanceState: Bundle?) {
try {
super.onCreate(savedInstanceState)
} catch (e: Error) {
exitProcess(0)
} catch (e: Exception) {
exitProcess(0)
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
hasKeyboard = hasHardKeyboard()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) window.makeFullScreen()
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
// Ignore the back press so MultiCraft can handle it
}
override fun onPause() {
super.onPause()
pauseGame()
}
override fun onResume() {
super.onResume()
if (hasKeyboard) keyboardEvent(true)
window.makeFullScreen()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val statusKeyboard = hasHardKeyboard()
if (hasKeyboard != statusKeyboard) {
hasKeyboard = statusKeyboard
keyboardEvent(hasKeyboard)
}
}
@Suppress("unused")
fun showDialog(hint: String?, current: String?, editType: Int) {
isInputActive = true
messageReturnValue = ""
if (editType == 1)
runOnUiThread { showMultiLineDialog(hint, current) }
else
runOnUiThread { showSingleDialog(hint, current, editType) }
}
private fun showSingleDialog(hint: String?, current: String?, editType: Int) {
val builder = AlertDialog.Builder(this, R.style.FullScreenDialogStyle)
val binding = InputTextBinding.inflate(layoutInflater)
var hintText: String = hint?.ifEmpty {
resources.getString(if (editType == 3) R.string.input_password else R.string.input_text)
}.toString()
hintText = hintText.replace(":$".toRegex(), "")
binding.input.hint = hintText
builder.setView(binding.root)
val alertDialog = builder.create()
val editText = binding.editText
editText.requestFocus()
editText.setText(current.toString())
editText.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
var inputType = InputType.TYPE_CLASS_TEXT
if (editType == 3) {
inputType = inputType or InputType.TYPE_TEXT_VARIATION_PASSWORD
if (isOreo())
editText.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
}
editText.inputType = inputType
editText.setSelection(editText.text?.length ?: 0)
// for Android OS
editText.setOnEditorActionListener { _: TextView?, keyCode: Int, _: KeyEvent? ->
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
imm.hideSoftInputFromWindow(editText.windowToken, 0)
messageReturnValue = editText.text.toString()
alertDialog.dismiss()
isInputActive = false
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
if (isChromebook()) {
editText.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? ->
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
imm.hideSoftInputFromWindow(editText.windowToken, 0)
messageReturnValue = editText.text.toString()
alertDialog.dismiss()
isInputActive = false
return@setOnKeyListener true
}
return@setOnKeyListener false
}
}
binding.input.setEndIconOnClickListener {
imm.hideSoftInputFromWindow(editText.windowToken, 0)
messageReturnValue = editText.text.toString()
alertDialog.dismiss()
isInputActive = false
}
binding.rl.setOnClickListener {
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
messageReturnValue = current.toString()
alertDialog.dismiss()
isInputActive = false
}
val alertWindow = alertDialog.window!!
// should be above `show()`
alertWindow.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
alertDialog.show()
if (!isTablet())
alertWindow.makeFullScreenAlert()
alertDialog.setOnCancelListener {
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
messageReturnValue = current.toString()
isInputActive = false
}
}
private fun showMultiLineDialog(hint: String?, current: String?) {
val builder = AlertDialog.Builder(this, R.style.FullScreenDialogStyle)
val binding = MultilineInputBinding.inflate(layoutInflater)
var hintText: String = hint?.ifEmpty {
resources.getString(R.string.input_text)
}.toString()
hintText = hintText.replace(":$".toRegex(), "")
binding.multiInput.hint = hintText
builder.setView(binding.root)
val alertDialog = builder.create()
val editText = binding.multiEditText
editText.requestFocus()
editText.setText(current.toString())
editText.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
editText.setSelection(editText.text?.length ?: 0)
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
// for Android OS
editText.setOnEditorActionListener { _: TextView?, keyCode: Int, _: KeyEvent? ->
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
imm.hideSoftInputFromWindow(editText.windowToken, 0)
messageReturnValue = editText.text.toString()
alertDialog.dismiss()
isInputActive = false
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
if (isChromebook()) {
editText.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? ->
if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_ENDCALL) {
imm.hideSoftInputFromWindow(editText.windowToken, 0)
messageReturnValue = editText.text.toString()
alertDialog.dismiss()
isInputActive = false
return@setOnKeyListener true
}
return@setOnKeyListener false
}
}
binding.multiInput.setEndIconOnClickListener {
imm.hideSoftInputFromWindow(editText.windowToken, 0)
messageReturnValue = editText.text.toString()
alertDialog.dismiss()
isInputActive = false
}
binding.multiRl.setOnClickListener {
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
messageReturnValue = current.toString()
alertDialog.dismiss()
isInputActive = false
}
// should be above `show()`
val alertWindow = alertDialog.window!!
alertWindow.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE)
alertDialog.show()
if (!isTablet())
alertWindow.makeFullScreenAlert()
alertDialog.setOnCancelListener {
window.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_HIDDEN)
messageReturnValue = current.toString()
isInputActive = false
}
}
@Suppress("unused")
fun isDialogActive() = isInputActive
@Suppress("unused")
fun getDialogValue(): String {
val value = messageReturnValue
messageReturnValue = ""
return value
}
@Suppress("unused")
fun getDensity() = resources.displayMetrics.density
@Suppress("unused")
fun notifyServerConnect(multiplayer: Boolean) {
isMultiPlayer = multiplayer
}
@Suppress("unused")
fun notifyExitGame() {
}
@Suppress("unused")
fun openURI(uri: String?) {
val builder = CustomTabsIntent.Builder()
builder.setShareState(SHARE_STATE_OFF)
.setStartAnimations(this, R.anim.slide_in_bottom, R.anim.slide_out_top)
.setExitAnimations(this, R.anim.slide_in_top, R.anim.slide_out_bottom)
val customTabsIntent = builder.build()
try {
customTabsIntent.launchUrl(this, Uri.parse(uri))
} catch (ignored: Exception) {
}
}
@Suppress("unused")
fun finishGame(exc: String?) {
finishApp(true)
}
@Suppress("unused")
fun handleError(exc: String?) {
}
@Suppress("unused")
fun upgrade(item: String) {
}
@Suppress("unused")
fun getSecretKey(key: String): String {
return key
}
@Suppress("unused")
fun getRoundScreen(): Int {
return radius
}
}

View File

@ -0,0 +1,235 @@
/*
MultiCraft
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3.0 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.multicraft.game
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.provider.Settings.ACTION_WIFI_SETTINGS
import android.provider.Settings.ACTION_WIRELESS_SETTINGS
import android.view.RoundedCorner
import android.view.WindowManager
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.work.WorkInfo
import com.multicraft.game.databinding.ActivityMainBinding
import com.multicraft.game.dialogs.ConnectionDialog
import com.multicraft.game.helpers.*
import com.multicraft.game.helpers.ApiLevelHelper.isAndroid12
import com.multicraft.game.helpers.ApiLevelHelper.isPie
import com.multicraft.game.helpers.PreferenceHelper.TAG_BUILD_VER
import com.multicraft.game.helpers.PreferenceHelper.getStringValue
import com.multicraft.game.helpers.PreferenceHelper.set
import com.multicraft.game.workmanager.UnzipWorker.Companion.PROGRESS
import com.multicraft.game.workmanager.WorkerViewModel
import com.multicraft.game.workmanager.WorkerViewModelFactory
import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var externalStorage: File? = null
private val sep = File.separator
private lateinit var prefs: SharedPreferences
private lateinit var restartStartForResult: ActivityResultLauncher<Intent>
private lateinit var connStartForResult: ActivityResultLauncher<Intent>
private val versionCode = BuildConfig.VERSION_CODE
private val versionName = "${BuildConfig.VERSION_NAME}+$versionCode"
companion object {
var radius = 0
const val NO_SPACE_LEFT = "ENOSPC"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
connStartForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
checkAppVersion()
}
restartStartForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == RESULT_OK)
finishApp(true)
else
finishApp(false)
}
try {
prefs = PreferenceHelper.init(this)
externalStorage = getExternalFilesDir(null)
listOf(filesDir, cacheDir, externalStorage).requireNoNulls()
checkConnection()
} catch (e: Exception) {
val isRestart = e.message?.contains(NO_SPACE_LEFT) != true
showRestartDialog(restartStartForResult, isRestart)
}
}
override fun onResume() {
super.onResume()
window.makeFullScreen()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) window.makeFullScreen()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (isPie()) {
val cutout = window.decorView.rootWindowInsets.displayCutout
if (cutout != null) {
radius = 40
}
if (isAndroid12()) {
val insets = window.decorView.rootWindowInsets
if (insets != null) {
val tl = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)
radius = tl?.radius ?: if (cutout != null) 40 else 0
}
}
}
val animation = binding.loadingAnim.drawable as AnimationDrawable
animation.start()
}
private fun startNative() {
val initLua = File(filesDir, "builtin${sep}mainmenu${sep}init.lua")
if (initLua.exists() && initLua.canRead()) {
val intent = Intent(this, GameActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
} else {
prefs[TAG_BUILD_VER] = "0"
showRestartDialog(restartStartForResult)
}
}
private fun prepareToRun() {
val filesList = mutableListOf<File>().apply {
addAll(listOf(
"builtin",
"client${sep}shaders",
"fonts",
"games${sep}default",
"textures${sep}base"
).map { File(filesDir, it) })
}
val zips = mutableListOf("assets.zip")
lifecycleScope.launch {
filesList.forEach { it.deleteRecursively() }
zips.forEach {
try {
assets.open(it).use { input ->
File(cacheDir, it).copyInputStreamToFile(input)
}
} catch (e: IOException) {
val isNotEnoughSpace = e.message!!.contains(NO_SPACE_LEFT)
runOnUiThread { showRestartDialog(restartStartForResult, !isNotEnoughSpace) }
return@forEach
}
}
try {
startUnzipWorker(zips.toTypedArray())
} catch (e: Exception) {
runOnUiThread { showRestartDialog(restartStartForResult) }
}
}
}
private fun checkAppVersion() {
val prefVersion = prefs.getStringValue(TAG_BUILD_VER)
if (prefVersion == versionName)
startNative()
else
prepareToRun()
}
private fun showConnectionDialog() {
val intent = Intent(this, ConnectionDialog::class.java)
val startForResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
when (it.resultCode) {
RESULT_OK -> connStartForResult.launch(Intent(ACTION_WIFI_SETTINGS))
RESULT_FIRST_USER -> connStartForResult.launch(Intent(ACTION_WIRELESS_SETTINGS))
else -> checkAppVersion()
}
}
startForResult.launch(intent)
}
// check connection available
private fun checkConnection() = lifecycleScope.launch {
if (isConnected()) checkAppVersion()
else try {
showConnectionDialog()
} catch (e: Exception) {
checkAppVersion()
}
}
private fun startUnzipWorker(file: Array<String>) {
val viewModelFactory = WorkerViewModelFactory(application, file)
val viewModel = ViewModelProvider(this, viewModelFactory)[WorkerViewModel::class.java]
viewModel.unzippingWorkObserver
.observe(this, Observer { workInfo ->
if (workInfo == null)
return@Observer
val progress = workInfo.progress.getInt(PROGRESS, 0)
if (progress > 0) {
val progressMessage = "${getString(R.string.loading)} $progress%"
binding.tvProgress.text = progressMessage
}
if (workInfo.state.isFinished) {
if (workInfo.state == WorkInfo.State.FAILED) {
val isRestart = workInfo.outputData.getBoolean("restart", true)
showRestartDialog(restartStartForResult, isRestart)
} else if (workInfo.state == WorkInfo.State.SUCCEEDED) {
prefs[TAG_BUILD_VER] = versionName
startNative()
}
}
})
viewModel.startOneTimeWorkRequest()
}
}

View File

@ -0,0 +1,96 @@
/*
MultiCraft
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3.0 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.multicraft.game.dialogs
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.SIM_STATE_READY
import android.view.View
import android.widget.LinearLayout
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import com.multicraft.game.databinding.ConnectionDialogBinding
import com.multicraft.game.helpers.ApiLevelHelper.isOreo
import com.multicraft.game.helpers.makeFullScreen
import org.libsdl.app.SDLActivity
class ConnectionDialog : AppCompatActivity() {
private fun isSimCardPresent(): Boolean {
val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
if (!isOreo())
return telephonyManager.simState == SIM_STATE_READY
val isFirstSimPresent = telephonyManager.getSimState(0) == SIM_STATE_READY
val isSecondSimPresent = telephonyManager.getSimState(1) == SIM_STATE_READY
return isFirstSimPresent || isSecondSimPresent
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ConnectionDialogBinding.inflate(layoutInflater)
if (SDLActivity.isTablet()) {
val param = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
0.5f
)
binding.connRoot.layoutParams = param
}
if (isSimCardPresent())
binding.mobile.visibility = View.VISIBLE
setContentView(binding.root)
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
binding.wifi.setOnClickListener {
setResult(Activity.RESULT_OK)
finish()
}
binding.mobile.setOnClickListener {
setResult(Activity.RESULT_FIRST_USER)
finish()
}
binding.ignore.setOnClickListener {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
override fun onResume() {
super.onResume()
window.makeFullScreen()
}
override fun attachBaseContext(base: Context?) {
val configuration = Configuration(base?.resources?.configuration)
configuration.fontScale = 1.0f
applyOverrideConfiguration(configuration)
super.attachBaseContext(base)
}
}

View File

@ -0,0 +1,76 @@
/*
MultiCraft
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3.0 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.multicraft.game.dialogs
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.widget.LinearLayout
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import com.multicraft.game.databinding.RestartDialogBinding
import com.multicraft.game.helpers.makeFullScreen
import org.libsdl.app.SDLActivity
class RestartDialog : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = RestartDialogBinding.inflate(layoutInflater)
if (SDLActivity.isTablet()) {
val param = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
0.5f
)
binding.restartRoot.layoutParams = param
}
setContentView(binding.root)
onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
val message = intent.getStringExtra("message")!!
binding.errorDesc.text = message
binding.restart.setOnClickListener {
setResult(Activity.RESULT_OK)
finish()
}
binding.close.setOnClickListener {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
override fun onResume() {
super.onResume()
window.makeFullScreen()
}
override fun attachBaseContext(base: Context?) {
val configuration = Configuration(base?.resources?.configuration)
configuration.fontScale = 1.0f
applyOverrideConfiguration(configuration)
super.attachBaseContext(base)
}
}

View File

@ -1,7 +1,7 @@
/* /*
MultiCraft MultiCraft
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua> Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com> Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by it under the terms of the GNU Lesser General Public License as published by
@ -30,5 +30,7 @@ object ApiLevelHelper {
fun isOreo() = isGreaterOrEqual(O) fun isOreo() = isGreaterOrEqual(O)
fun isPie() = isGreaterOrEqual(P)
fun isAndroid12() = isGreaterOrEqual(S) fun isAndroid12() = isGreaterOrEqual(S)
} }

View File

@ -1,7 +1,7 @@
/* /*
MultiCraft MultiCraft
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua> Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com> Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by it under the terms of the GNU Lesser General Public License as published by
@ -24,9 +24,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
object PreferenceHelper { object PreferenceHelper {
const val TAG_SHORTCUT_EXIST = "createShortcut"
const val TAG_BUILD_VER = "buildVer" const val TAG_BUILD_VER = "buildVer"
const val TAG_LAUNCH_TIMES = "launchTimes"
fun init(context: Context): SharedPreferences = fun init(context: Context): SharedPreferences =
context.getSharedPreferences("MultiCraftSettings", Context.MODE_PRIVATE) context.getSharedPreferences("MultiCraftSettings", Context.MODE_PRIVATE)
@ -46,16 +44,6 @@ object PreferenceHelper {
else -> throw UnsupportedOperationException("Not yet implemented") else -> throw UnsupportedOperationException("Not yet implemented")
} }
fun SharedPreferences.getBoolValue(key: String): Boolean = when (key) {
TAG_SHORTCUT_EXIST -> getBoolean(key, false)
else -> throw UnsupportedOperationException("Not yet implemented")
}
fun SharedPreferences.getIntValue(key: String) = when (key) {
TAG_LAUNCH_TIMES -> getInt(key, 0)
else -> throw UnsupportedOperationException("Not yet implemented")
}
fun SharedPreferences.getStringValue(key: String) = when (key) { fun SharedPreferences.getStringValue(key: String) = when (key) {
TAG_BUILD_VER -> getString(key, "0") as String TAG_BUILD_VER -> getString(key, "0") as String
else -> throw UnsupportedOperationException("Not yet implemented") else -> throw UnsupportedOperationException("Not yet implemented")

View File

@ -0,0 +1,103 @@
/*
MultiCraft
Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3.0 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package com.multicraft.game.helpers
import android.app.*
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.*
import android.os.*
import android.view.Window
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.*
import com.multicraft.game.*
import com.multicraft.game.databinding.*
import com.multicraft.game.dialogs.RestartDialog
import com.multicraft.game.helpers.ApiLevelHelper.isAndroid12
import com.multicraft.game.helpers.ApiLevelHelper.isMarshmallow
import java.io.*
import java.util.*
// Activity extensions
fun Activity.finishApp(restart: Boolean) {
if (restart) {
val intent = Intent(this, this::class.java)
val mPendingIntentId = 1337
val flag =
if (isAndroid12()) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_CANCEL_CURRENT
val mgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
mgr.set(
AlarmManager.RTC, System.currentTimeMillis(), PendingIntent.getActivity(
this, mPendingIntentId, intent, flag
)
)
}
finish()
}
fun Activity.isConnected(): Boolean {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (isMarshmallow()) {
val activeNetwork = cm.activeNetwork ?: return false
val capabilities = cm.getNetworkCapabilities(activeNetwork) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
} else @Suppress("DEPRECATION") {
val activeNetworkInfo = cm.activeNetworkInfo ?: return false
return activeNetworkInfo.isConnected
}
}
fun AppCompatActivity.showRestartDialog(
startForResult: ActivityResultLauncher<Intent>,
isRestart: Boolean = true
) {
val message =
if (isRestart) getString(R.string.restart) else getString(R.string.no_space)
val intent = Intent(this, RestartDialog::class.java)
intent.putExtra("message", message)
startForResult.launch(intent)
}
fun Activity.hasHardKeyboard() =
resources.configuration.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO
// Other extensions
fun File.copyInputStreamToFile(inputStream: InputStream) =
outputStream().use { fileOut -> inputStream.copyTo(fileOut, 8192) }
fun Window.makeFullScreen() {
WindowCompat.setDecorFitsSystemWindows(this, false)
WindowInsetsControllerCompat(this, decorView).let {
it.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
it.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
fun Window.makeFullScreenAlert() {
WindowInsetsControllerCompat(this, decorView).let {
it.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
it.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}

View File

@ -1,7 +1,7 @@
/* /*
MultiCraft MultiCraft
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua> Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com> Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by it under the terms of the GNU Lesser General Public License as published by
@ -25,13 +25,11 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.Context.NOTIFICATION_SERVICE import android.content.Context.NOTIFICATION_SERVICE
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.work.CoroutineWorker import androidx.work.*
import androidx.work.ForegroundInfo import com.multicraft.game.MainActivity.Companion.NO_SPACE_LEFT
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.multicraft.game.R import com.multicraft.game.R
import com.multicraft.game.helpers.ApiLevelHelper.isOreo import com.multicraft.game.helpers.ApiLevelHelper.isOreo
import com.multicraft.game.helpers.Utilities.copyInputStreamToFile import com.multicraft.game.helpers.copyInputStreamToFile
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -86,7 +84,11 @@ class UnzipWorker(private val appContext: Context, workerParams: WorkerParameter
} }
Result.success() Result.success()
} catch (e: IOException) { } catch (e: IOException) {
Result.failure() val isNotEnoughSpace = e.localizedMessage!!.contains(NO_SPACE_LEFT)
val out = Data.Builder()
.putBoolean("restart", !isNotEnoughSpace)
.build()
Result.failure(out)
} finally { } finally {
zips.forEach { File(appContext.cacheDir, it).delete() } zips.forEach { File(appContext.cacheDir, it).delete() }
} }

View File

@ -1,7 +1,7 @@
/* /*
MultiCraft MultiCraft
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua> Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com> Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by it under the terms of the GNU Lesser General Public License as published by

View File

@ -1,7 +1,7 @@
/* /*
MultiCraft MultiCraft
Copyright (C) 2014-2021 MoNTE48, Maksim Gamarnik <MoNTE48@mail.ua> Copyright (C) 2014-2023 MoNTE48, Maksim Gamarnik <Maksym48@pm.me>
Copyright (C) 2014-2021 ubulem, Bektur Mambetov <berkut87@gmail.com> Copyright (C) 2014-2023 ubulem, Bektur Mambetov <berkut87@gmail.com>
This program is free software; you can redistribute it and/or modify This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by it under the terms of the GNU Lesser General Public License as published by
@ -30,7 +30,7 @@ class WorkerViewModelFactory(
private val zips: Array<String> private val zips: Array<String>
) : ) :
ViewModelProvider.Factory { ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WorkerViewModel::class.java)) { if (modelClass.isAssignableFrom(WorkerViewModel::class.java)) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return WorkerViewModel(application, zips) as T return WorkerViewModel(application, zips) as T

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromYDelta="100%p"
android:toYDelta="0%p" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromYDelta="-100%p"
android:toYDelta="0%p" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromYDelta="0%p"
android:toYDelta="100%p" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromYDelta="0%p"
android:toYDelta="-100%p" />

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="?attr/colorBackgroundFloating" /> <solid android:color="@color/dark" />
<corners android:radius="12dp" /> <corners
android:topLeftRadius="5dp"
android:topRightRadius="5dp" />
</shape> </shape>

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="@color/not_white" /> <solid android:color="@color/light" />
<corners android:radius="12dp" /> <corners
android:topLeftRadius="5dp"
android:topRightRadius="5dp" />
</shape> </shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/green_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/green" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/red_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/red" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/yellow_pressed" android:state_pressed="true" />
<item android:drawable="@drawable/yellow" />
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/green_mc"
android:pathData="M3.4,20.4l17.45,-7.48c0.81,-0.35 0.81,-1.49 0,-1.84L3.4,3.6c-0.66,-0.29 -1.39,0.2 -1.39,0.91L2,9.12c0,0.5 0.37,0.93 0.87,0.99L17,12 2.87,13.88c-0.5,0.07 -0.87,0.5 -0.87,1l0.01,4.61c0,0.71 0.73,1.2 1.39,0.91z" />
</vector>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/loading_anim1"
android:duration="125" />
<item
android:drawable="@drawable/loading_anim2"
android:duration="125" />
<item
android:drawable="@drawable/loading_anim3"
android:duration="125" />
<item
android:drawable="@drawable/loading_anim4"
android:duration="125" />
<item
android:drawable="@drawable/loading_anim5"
android:duration="125" />
<item
android:drawable="@drawable/loading_anim6"
android:duration="125" />
<item
android:drawable="@drawable/loading_anim7"
android:duration="125" />
<item
android:drawable="@drawable/loading_anim8"
android:duration="125" />
</animation-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 134 B

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

View File

@ -0,0 +1,22 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/loading_anim"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/loading" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/preparing"
android:textColor="@color/light"
android:textSize="16sp" />
</LinearLayout>

View File

@ -0,0 +1,78 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:gravity="center"
android:weightSum="1">
<LinearLayout
android:id="@+id/conn_root"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:background="@drawable/bg_common"
android:orientation="vertical"
android:padding="16dp"
tools:ignore="UselessParent">
<androidx.appcompat.widget.AppCompatTextView
style="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableStart="@mipmap/ic_dialog"
android:drawablePadding="8dp"
android:gravity="center"
android:text="@string/conn_title"
android:textColor="@color/grey_900" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="8dp"
android:text="@string/conn_message"
android:textColor="@color/grey_900"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/ignore"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_margin="8dp"
android:layout_weight="0.33"
android:background="@drawable/btn_yellow"
android:text="@string/ignore"
android:textAllCaps="false"
android:textColor="@android:color/white" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/mobile"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_margin="8dp"
android:layout_weight="0.33"
android:background="@drawable/btn_green"
android:text="@string/conn_mobile"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/wifi"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_margin="8dp"
android:layout_weight="0.33"
android:background="@drawable/btn_green"
android:text="@string/conn_wifi"
android:textAllCaps="false"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,26 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input"
style="@style/TextInputLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/bg_input"
android:hint="@string/input_text"
android:padding="4dp"
app:endIconDrawable="@drawable/ic_baseline_send"
app:endIconMode="custom"
app:endIconTint="@null"
app:hintTextColor="@color/green_mc">
<com.multicraft.game.CustomEditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="60dp" />
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>

View File

@ -0,0 +1,28 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/multiRl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/multiInput"
style="@style/TextInputLayoutStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@drawable/bg_input"
android:hint="@string/input_text"
android:padding="4dp"
app:endIconDrawable="@drawable/ic_baseline_send"
app:endIconMode="custom"
app:endIconTint="@null"
app:hintTextColor="@color/green_mc">
<com.multicraft.game.CustomEditText
android:id="@+id/multiEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:maxLines="8" />
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>

View File

@ -0,0 +1,86 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:gravity="center"
android:weightSum="1">
<LinearLayout
android:id="@+id/restart_root"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.7"
android:background="@drawable/bg_common"
android:orientation="vertical"
android:padding="16dp"
tools:ignore="UselessParent">
<androidx.appcompat.widget.AppCompatTextView
style="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableStart="@mipmap/ic_dialog"
android:drawablePadding="8dp"
android:gravity="center"
android:text="@string/sorry"
android:textColor="@color/grey_900" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/restart"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
app:srcCompat="@drawable/sad" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/error_desc"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minHeight="128dp"
android:padding="4dp"
android:text="@string/restart"
android:textColor="@color/grey_900"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/close"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_margin="8dp"
android:layout_weight="0.5"
android:background="@drawable/btn_red"
android:text="@string/close_game"
android:textAllCaps="false"
android:textColor="@android:color/white" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/restart"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_margin="8dp"
android:layout_weight="0.5"
android:background="@drawable/btn_green"
android:text="@string/restart_game"
android:textAllCaps="false"
android:textColor="@android:color/white" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -3,19 +3,15 @@
<!-- подготовка к запуску --> <!-- подготовка к запуску -->
<string name="preparing">Подготовка к запуску&#8230;</string> <string name="preparing">Подготовка к запуску&#8230;</string>
<string name="loading">Загрузка&#8230;</string> <string name="loading">Загрузка&#8230;</string>
<string name="loadingp">Загрузка&#8230; %d%%</string>
<string name="notification_title">Загрузка MultiCraft</string> <string name="notification_title">Загрузка MultiCraft</string>
<string name="notification_description">Осталось меньше минуты&#8230;</string> <string name="notification_description">Осталось меньше минуты&#8230;</string>
<string name="input_text">Введите текст</string>
<string name="input_text">Введите Текст</string> <string name="input_password">Введите пароль</string>
<string name="input_password">Пароль</string>
<string name="done">Готово</string>
<!-- диалог отсутствия подключения --> <!-- диалог отсутствия подключения -->
<string name="conn_title">Нет Интернет Подключения!</string> <string name="conn_title">Нет Интернет Подключения!</string>
<string name="conn_message">Для полноценной игры, MultiCraft требует подключение к Интернету.\nВ противном случае вам будет недоступно Обновление игры и режим Мультиплеера!</string> <string name="conn_message">Для полноценной игры, MultiCraft требует подключение к Интернету.\nВ противном случае Обновление игры и режим Мультиплеера будут недоступны!</string>
<string name="conn_mobile">3G/4G</string> <string name="ignore">Пропустить</string>
<string name="ignore">Игнорировать</string>
<!-- Crash --> <!-- Crash -->
<string name="sorry">Нам очень жаль!</string> <string name="sorry">Нам очень жаль!</string>

View File

@ -1,14 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="CustomDialog" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
<item name="android:windowBackground">@drawable/bg</item> <item name="android:windowBackground">@drawable/bg</item>
<item name="android:windowFullscreen">true</item> <item name="android:windowFullscreen">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item> <item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
<item name="fontFamily">@font/multicraftfont</item> <item name="fontFamily">@font/multicraftfont</item>
<item name="colorControlActivated">@color/green</item>
<item name="colorPrimary">@color/green</item>
</style> </style>
</resources> </resources>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="CustomDialog" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="windowActionBar">false</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFullscreen">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
<item name="fontFamily">@font/multicraftfont</item>
</style>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<color name="light">#FAFAFA</color>
<color name="dark">#121212</color>
<color name="green_mc">#32783C</color>
<color name="grey_900">#212121</color>
</resources>

View File

@ -1,24 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">MultiCraft</string> <string name="app_name" translatable="false">MultiCraft</string>
<!-- preparation for start --> <!-- preparation for start -->
<string name="preparing">Preparing to launch&#8230;</string> <string name="preparing">Preparing to launch&#8230;</string>
<string name="loading">Loading&#8230;</string> <string name="loading">Loading&#8230;</string>
<string name="loadingp">Loading&#8230; %d%%</string>
<string name="notification_title">Loading MultiCraft</string> <string name="notification_title">Loading MultiCraft</string>
<string name="notification_description">Less than 1 minute&#8230;</string> <string name="notification_description">Less than 1 minute&#8230;</string>
<string name="ok" translatable="false">OK</string>
<string name="input_text">Text Input</string> <string name="input_text">Enter text</string>
<string name="input_password">Password</string> <string name="input_password">Enter password</string>
<string name="done">Done</string>
<!-- no connection dialog --> <!-- no connection dialog -->
<string name="conn_title">No Internet Connection!</string> <string name="conn_title">No Internet Connection!</string>
<string name="conn_message">MultiCraft requires an Internet connection to use all game features.\nOtherwise, you will not get Updates and Multiplayer mode will be not available!</string> <string name="conn_message">MultiCraft requires an Internet connection to use all game features.\nOtherwise, you will not get Updates and Multiplayer mode will be not available!</string>
<string name="conn_wifi" translatable="false">Wi-Fi</string> <string name="conn_wifi" translatable="false">Wi-Fi</string>
<string name="conn_mobile">Mobile Data</string> <string name="conn_mobile" translatable="false">LTE</string>
<string name="ignore">Ignore</string> <string name="ignore">Ignore</string>
<!-- Crash --> <!-- Crash -->

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="android:windowBackground">@drawable/bg</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
<item name="fontFamily">@font/multicraftfont</item>
</style>
<style name="CustomDialog" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="windowActionBar">false</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFullscreen">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="p">shortEdges</item>
<item name="fontFamily">@font/multicraftfont</item>
</style>
<style name="FullScreenDialogStyle" parent="Theme.MaterialComponents.Light.Dialog">
<item name="android:windowFullscreen">true</item>
<item name="android:windowIsFloating">false</item>
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowEnterAnimation">@null</item>
</style>
<style name="TextInputLayoutStyle" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="boxStrokeColor">@color/green_mc</item>
</style>
</resources>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude
domain="sharedpref"
path="MultiCraftSettings.xml" />
</cloud-backup>
</data-extraction-rules>

View File

@ -2,11 +2,12 @@
project.ext.set("versionMajor", 2) // Version Major project.ext.set("versionMajor", 2) // Version Major
project.ext.set("versionMinor", 0) // Version Minor project.ext.set("versionMinor", 0) // Version Minor
project.ext.set("versionPatch", 1) // Version Patch project.ext.set("versionPatch", 6) // Version Patch
project.ext.set("versionExtra", "") // Version Extra project.ext.set("versionExtra", "") // Version Extra
project.ext.set("versionCode", 100) // Android Version Code project.ext.set("versionCode", 200) // Android Version Code
// NOTE: +2 after each release! project.ext.set("developmentBuild", 0) // Whether it is a development build, or a release
// +1 for ARM and +1 for ARM64 APK's, because // NOTE: +3 after each release!
// +1 for ARM, +1 for ARM64 and +1 for x86_64 APK's, because
// each APK must have a larger `versionCode` than the previous // each APK must have a larger `versionCode` than the previous
buildscript { buildscript {
@ -15,9 +16,10 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.1.2' classpath 'com.android.tools.build:gradle:8.2.1'
//noinspection GradleDependency
classpath 'de.undercouch:gradle-download-task:4.1.2' classpath 'de.undercouch:gradle-download-task:4.1.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }
@ -30,7 +32,7 @@ allprojects {
} }
} }
task clean(type: Delete) { tasks.register('clean', Delete) {
delete rootProject.buildDir delete rootProject.layout.buildDirectory
delete 'native/deps' delete 'native/deps'
} }

14
Android/gradle.properties Normal file
View File

@ -0,0 +1,14 @@
<#if isLowMemory>
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
<#else>
org.gradle.jvmargs=-Xmx16G -XX:MaxMetaspaceSize=8G -XX:+HeapDumpOnOutOfMemoryError
</#if>
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.parallel.threads=8
org.gradle.configureondemand=true
android.enableJetifier=false
android.useAndroidX=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@ -1,6 +1,6 @@
#Fri Feb 11 12:29:43 EET 2022 #Thu Oct 05 18:46:10 EEST 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
Android/gradlew vendored Executable file
View File

@ -0,0 +1,240 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -51,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@ -61,38 +64,26 @@ echo location of your Java installation.
goto fail goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 set EXIT_CODE=%ERRORLEVEL%
exit /b 1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

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