1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-29 21:56:54 +02:00

Merge pull request from vcmi/beta

Merge beta -> master
This commit is contained in:
Ivan Savenko 2024-06-20 14:19:47 +03:00 committed by GitHub
commit 48f32aa5e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
264 changed files with 10111 additions and 11764 deletions
.github/workflows
AI
CI
CMakeLists.txtCMakePresets.jsonChangeLog.mdGlobal.h
Mods/vcmi/config/vcmi
android
.gitignoreAndroidManifest.xmlGeneratedVersion.java.inandroiddeployqt.json.indefs.gradlegradle.properties
vcmi-app
.gitignorebuild.gradle
src/main
AndroidManifest.xml
java
res

@ -66,7 +66,7 @@ jobs:
pack: 1
pack_type: RelWithDebInfo
extension: exe
preset: windows-msvc-release-ccache
preset: windows-msvc-release
- platform: mingw
os: ubuntu-22.04
test: 0
@ -88,16 +88,16 @@ jobs:
preset: windows-mingw-conan-linux
conan_profile: mingw32-linux.jinja
- platform: android-32
os: ubuntu-22.04
os: macos-14
extension: apk
preset: android-conan-ninja-release
preset: android-daily-release
conan_profile: android-32
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
artifact_platform: armeabi-v7a
- platform: android-64
os: ubuntu-22.04
os: macos-14
extension: apk
preset: android-conan-ninja-release
preset: android-daily-release
conan_profile: android-64
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
artifact_platform: arm64-v8a
@ -187,6 +187,12 @@ jobs:
env:
GENERATE_ONLY_BUILT_CONFIG: 1
- uses: actions/setup-java@v4
if: ${{ startsWith(matrix.platform, 'android') }}
with:
distribution: 'temurin'
java-version: '11'
- name: Build Number
run: |
source '${{github.workspace}}/CI/get_package_name.sh'
@ -201,8 +207,15 @@ jobs:
- name: Configure
run: |
if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC14=1; fi
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC14:+-DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14}
if [[ ${{matrix.preset}} == linux-gcc-test ]]
then
cmake -DENABLE_CCACHE:BOOL=ON -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 --preset ${{ matrix.preset }}
elif [[ ${{matrix.platform}} != msvc ]]
then
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
else
cmake --preset ${{ matrix.preset }}
fi
- name: Build
run: |
@ -215,7 +228,7 @@ jobs:
run: |
ctest --preset ${{matrix.preset}}
- name: Kill XProtect to work around CPack issue on macOS
- name: Kill XProtect to work around CPack issue on macOS
if: ${{ startsWith(matrix.platform, 'mac') }}
run: |
# Cf. https://github.com/actions/runner-images/issues/7522#issuecomment-1556766641
@ -234,13 +247,6 @@ jobs:
&& '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
rm -rf _CPack_Packages
- name: Create Android package
if: ${{ startsWith(matrix.platform, 'android') }}
run: |
cd android
./gradlew assembleDaily --info
echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/apk/daily/*.${{ matrix.extension }})" >> $GITHUB_ENV
- name: Additional logs
if: ${{ failure() && steps.cpack.outcome == 'failure' && matrix.platform == 'msvc' }}
run: |
@ -254,7 +260,14 @@ jobs:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
path: |
${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
- name: Find Android package
if: ${{ startsWith(matrix.platform, 'android') }}
run: |
builtApkPath="$(ls ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs/apk/release/*.${{ matrix.extension }})"
ANDROID_APK_PATH="${{ github.workspace }}/$VCMI_PACKAGE_FILE_NAME.${{ matrix.extension }}"
mv "$builtApkPath" "$ANDROID_APK_PATH"
echo "ANDROID_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV
- name: Android artifacts
if: ${{ startsWith(matrix.platform, 'android') }}
uses: actions/upload-artifact@v4
@ -262,7 +275,7 @@ jobs:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
path: |
${{ env.ANDROID_APK_PATH }}
- name: Symbols
if: ${{ matrix.platform == 'msvc' }}
uses: actions/upload-artifact@v4
@ -283,19 +296,17 @@ jobs:
if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }}
continue-on-error: true
run: |
if cd '${{github.workspace}}/android/vcmi-app/build/outputs/apk/daily' ; then
mv '${{ env.ANDROID_APK_PATH }}' "$VCMI_PACKAGE_FILE_NAME.${{ matrix.extension }}"
else
if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then
cd '${{github.workspace}}/out/build/${{matrix.preset}}'
fi
source '${{github.workspace}}/CI/upload_package.sh'
env:
DEPLOY_RSA: ${{ secrets.DEPLOY_RSA }}
PACKAGE_EXTENSION: ${{ matrix.extension }}
# copy-pasted mostly
bundle_release:
needs: build
if: always() && github.ref == 'refs/heads/master'
strategy:
@ -303,7 +314,6 @@ jobs:
include:
- platform: android-32
os: ubuntu-22.04
extension: aab
preset: android-conan-ninja-release
conan_profile: android-32
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
@ -367,13 +377,13 @@ jobs:
uses: actions/download-artifact@v4
with:
name: Android JNI android-64
path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/
path: ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/libs
- name: Create Android package
run: |
cd android
cd out/build/${{ matrix.preset }}/android-build
./gradlew bundleRelease --info
echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV
echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/out/build/${{ matrix.preset }}/android-build/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV
env:
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}

@ -259,27 +259,46 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
return BattleAction::makeDefend(stack);
}
std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
return reachability.distances[h1] < reachability.distances[h2];
});
std::vector<BattleHex> targetHexes = hexes;
for(auto hex : hexes)
for(int i = 0; i < 5; i++)
{
if(vstd::contains(avHexes, hex))
std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
return reachability.distances[h1] < reachability.distances[h2];
});
for(auto hex : targetHexes)
{
return BattleAction::makeMove(stack, hex);
if(vstd::contains(avHexes, hex))
{
return BattleAction::makeMove(stack, hex);
}
if(stack->coversPos(hex))
{
logAi->warn("Warning: already standing on neighbouring tile!");
//We shouldn't even be here...
return BattleAction::makeDefend(stack);
}
}
if(stack->coversPos(hex))
if(reachability.distances[targetHexes.front()] <= GameConstants::BFIELD_SIZE)
{
logAi->warn("Warning: already standing on neighbouring tile!");
//We shouldn't even be here...
return BattleAction::makeDefend(stack);
break;
}
std::vector<BattleHex> copy = targetHexes;
for(auto hex : copy)
{
vstd::concatenate(targetHexes, hex.allNeighbouringTiles());
}
vstd::removeDuplicates(targetHexes);
}
BattleHex bestNeighbor = hexes.front();
BattleHex bestNeighbor = targetHexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
@ -602,10 +621,10 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state);
}
for(auto unit : allUnits)
for(const auto & unit : allUnits)
{
auto newHealth = unit->getAvailableHealth();
auto oldHealth = healthOfStack[unit->unitId()];
auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units
if(oldHealth != newHealth)
{
@ -732,6 +751,3 @@ void BattleEvaluator::print(const std::string & text) const
{
logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
}

@ -390,7 +390,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
const AttackPossibility & ap,
uint8_t turn,
PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb)
std::shared_ptr<HypotheticBattle> hb) const
{
ReachabilityData result;
@ -402,7 +402,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto hex : hexes)
{
vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap[hex] : getOneTurnReachableUnits(turn, hex));
vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
}
vstd::removeDuplicates(allReachableUnits);
@ -481,7 +481,7 @@ float BattleExchangeEvaluator::evaluateExchange(
uint8_t turn,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb)
std::shared_ptr<HypotheticBattle> hb) const
{
BattleScore score = calculateExchange(ap, turn, targets, damageCache, hb);
@ -502,7 +502,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
uint8_t turn,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb)
std::shared_ptr<HypotheticBattle> hb) const
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
@ -613,7 +613,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
}
else
{
auto reachable = exchangeBattle->battleGetUnitsIf([&](const battle::Unit * u) -> bool
auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool
{
if(u->unitSide() == attacker->unitSide())
return false;
@ -621,7 +621,10 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
if(!exchangeBattle->getForUpdate(u->unitId())->alive())
return false;
return vstd::contains_if(reachabilityMap[u->getPosition()], [&](const battle::Unit * other) -> bool
if (!u->getPosition().isValid())
return false; // e.g. tower shooters
return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
{
return attacker->unitId() == other->unitId();
});
@ -732,7 +735,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(std::shared_ptr<HypotheticBa
}
}
std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex)
std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex) const
{
std::vector<const battle::Unit *> result;
@ -756,13 +759,10 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
auto unitSpeed = unit->getMovementRange(turn);
auto radius = unitSpeed * (turn + 1);
ReachabilityInfo unitReachability = vstd::getOrCompute(
reachabilityCache,
unit->unitId(),
[&](ReachabilityInfo & data)
{
data = turnBattle.getReachability(unit);
});
auto reachabilityIter = reachabilityCache.find(unit->unitId());
assert(reachabilityIter != reachabilityCache.end()); // missing updateReachabilityMap call?
ReachabilityInfo unitReachability = reachabilityIter != reachabilityCache.end() ? reachabilityIter->second : turnBattle.getReachability(unit);
bool reachable = unitReachability.distances[hex] <= radius;

@ -139,7 +139,7 @@ private:
uint8_t turn,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb);
std::shared_ptr<HypotheticBattle> hb) const;
bool canBeHitThisTurn(const AttackPossibility & ap);
@ -162,16 +162,16 @@ public:
uint8_t turn,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb);
std::shared_ptr<HypotheticBattle> hb) const;
std::vector<const battle::Unit *> getOneTurnReachableUnits(uint8_t turn, BattleHex hex);
std::vector<const battle::Unit *> getOneTurnReachableUnits(uint8_t turn, BattleHex hex) const;
void updateReachabilityMap(std::shared_ptr<HypotheticBattle> hb);
ReachabilityData getExchangeUnits(
const AttackPossibility & ap,
uint8_t turn,
PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb);
std::shared_ptr<HypotheticBattle> hb) const;
bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position);

@ -1321,7 +1321,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
{
destinationTeleport = exitId;
if(exitPos.valid())
destinationTeleportPos = h->convertFromVisitablePos(exitPos);
destinationTeleportPos = exitPos;
cb->moveHero(*h, h->pos, false);
destinationTeleport = ObjectInstanceID();
destinationTeleportPos = int3(-1);
@ -1331,17 +1331,32 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
auto doChannelProbing = [&]() -> void
{
auto currentPos = h->visitablePos();
auto currentExit = getObj(currentPos, true)->id;
auto currentTeleport = getObj(currentPos, true);
status.setChannelProbing(true);
for(auto exit : teleportChannelProbingList)
doTeleportMovement(exit, int3(-1));
teleportChannelProbingList.clear();
status.setChannelProbing(false);
if(currentTeleport)
{
auto currentExit = currentTeleport->id;
doTeleportMovement(currentExit, currentPos);
status.setChannelProbing(true);
for(auto exit : teleportChannelProbingList)
doTeleportMovement(exit, int3(-1));
teleportChannelProbingList.clear();
status.setChannelProbing(false);
doTeleportMovement(currentExit, currentPos);
}
else
{
logAi->debug("Unexpected channel probbing at " + currentPos.toString());
teleportChannelProbingList.clear();
status.setChannelProbing(false);
}
};
teleportChannelProbingList.clear();
status.setChannelProbing(false);
for(; i > 0; i--)
{
int3 currentCoord = path.nodes[i].coord;

@ -17,10 +17,10 @@ namespace NKAI
struct ClusterObjectInfo
{
float priority;
float movementCost;
uint64_t danger;
uint8_t turn;
float priority = 0.f;
float movementCost = 0.f;
uint64_t danger = 0;
uint8_t turn = 0;
};
struct ObjectInstanceIDHash

@ -1203,7 +1203,10 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
std::vector<const ChainActor *> actorsVector(actorsOfInitial.begin(), actorsOfInitial.end());
tbb::concurrent_vector<CGPathNode *> output;
if(actorsVector.size() * initialNodes.size() > 1000)
// TODO: re-enable after fixing thread races. See issue for details:
// https://github.com/vcmi/vcmi/pull/4130
#if 0
if (actorsVector.size() * initialNodes.size() > 1000)
{
tbb::parallel_for(tbb::blocked_range<size_t>(0, actorsVector.size()), [&](const tbb::blocked_range<size_t> & r)
{
@ -1216,6 +1219,7 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *>
std::copy(output.begin(), output.end(), std::back_inserter(initialNodes));
}
else
#endif
{
for(auto actor : actorsVector)
{

@ -1899,7 +1899,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
{
destinationTeleport = exitId;
if(exitPos.valid())
destinationTeleportPos = h->convertFromVisitablePos(exitPos);
destinationTeleportPos = exitPos;
cb->moveHero(*h, h->pos, false);
destinationTeleport = ObjectInstanceID();
destinationTeleportPos = int3(-1);

@ -1,8 +1,9 @@
#!/usr/bin/env bash
sudo apt-get update
sudo apt-get install ninja-build
echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV
brew install ninja
mkdir ~/.conan ; cd ~/.conan
curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.0/$DEPS_FILENAME.txz" \
curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.1/$DEPS_FILENAME.txz" \
| tar -xf - --xz

4
CI/conan/android-32-ndk Normal file

@ -0,0 +1,4 @@
include(android-32)
[tool_requires]
android-ndk/r25c

4
CI/conan/android-64-ndk Normal file

@ -0,0 +1,4 @@
include(android-64)
[tool_requires]
android-ndk/r25c

@ -58,13 +58,22 @@ option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" O
# Platform-specific options
if(ANDROID)
set(ANDROID_TARGET_SDK_VERSION "33" CACHE STRING "Android target SDK version")
set(ANDROIDDEPLOYQT_OPTIONS "" CACHE STRING "Additional androiddeployqt options separated by semi-colon")
set(ANDROID_GRADLE_PROPERTIES "" CACHE STRING "Additional Gradle properties separated by semi-colon")
set(ENABLE_STATIC_LIBS ON)
set(ENABLE_LAUNCHER OFF)
set(ENABLE_LAUNCHER ON)
else()
option(ENABLE_STATIC_LIBS "Build library and all components such as AI statically" OFF)
option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
endif()
if(APPLE_IOS)
set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
endif()
if(APPLE_IOS OR ANDROID)
set(ENABLE_MONOLITHIC_INSTALL OFF)
set(ENABLE_SINGLE_APP_BUILD ON)
@ -100,11 +109,6 @@ if (ENABLE_STATIC_LIBS AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
if(APPLE_IOS)
set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
endif()
if(ENABLE_COLORIZED_COMPILER_OUTPUT)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
add_compile_options(-fcolor-diagnostics)
@ -147,10 +151,6 @@ set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR
include(VCMIUtils)
include(VersionDefinition)
if(ANDROID)
set(VCMI_VERSION "${APP_SHORT_VERSION}")
configure_file("android/GeneratedVersion.java.in" "${CMAKE_SOURCE_DIR}/android/vcmi-app/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java" @ONLY)
endif()
vcmi_print_important_variables()
@ -575,8 +575,12 @@ elseif(APPLE)
endif()
elseif(ANDROID)
include(GNUInstallDirs)
set(LIB_DIR "jniLibs/${ANDROID_ABI}")
set(DATA_DIR "assets")
set(LIB_DIR "libs/${ANDROID_ABI}")
# required by Qt
set(androidPackageSourceDir "${CMAKE_SOURCE_DIR}/android")
set(androidQtBuildDir "${CMAKE_BINARY_DIR}/android-build")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${androidQtBuildDir}/${LIB_DIR}")
else()
# includes lib path which determines where to install shared libraries (either /lib or /lib64)
include(GNUInstallDirs)
@ -621,6 +625,13 @@ else()
set(SCRIPTING_LIB_DIR "${LIB_DIR}/scripting")
endif()
# common Qt paths
if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
get_target_property(qmakePath Qt${QT_VERSION_MAJOR}::qmake IMPORTED_LOCATION)
get_filename_component(qtDir "${qmakePath}/../../" ABSOLUTE)
set(qtBinDir "${qtDir}/bin")
endif()
#######################################
# Add subdirectories #
#######################################
@ -682,32 +693,15 @@ endif()
#######################################
if(ANDROID)
string(REPLACE ";" "\n" ANDROID_GRADLE_PROPERTIES_MULTILINE "${ANDROID_GRADLE_PROPERTIES}")
file(WRITE "${androidPackageSourceDir}/vcmi-app/gradle.properties" "signingRoot=${CMAKE_SOURCE_DIR}/CI/android\n${ANDROID_GRADLE_PROPERTIES_MULTILINE}")
if(ANDROID_STL MATCHES "_shared$")
set(stlLibName "${CMAKE_SHARED_LIBRARY_PREFIX}${ANDROID_STL}${CMAKE_SHARED_LIBRARY_SUFFIX}")
install(FILES "${CMAKE_SYSROOT}/usr/lib/${ANDROID_SYSROOT_LIB_SUBDIR}/${stlLibName}"
DESTINATION ${LIB_DIR}
)
endif()
# zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip
install(CODE "
cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX
OUTPUT_VARIABLE absolute_install_prefix
)
set(absolute_data_dir \"\${absolute_install_prefix}/${DATA_DIR}\")
file(MAKE_DIRECTORY \"\${absolute_data_dir}\")
set(internal_data_zip \"\${absolute_data_dir}/internalData.zip\")
execute_process(COMMAND
\"${CMAKE_COMMAND}\" -E tar c \"\${internal_data_zip}\" --format=zip -- config Mods
WORKING_DIRECTORY \"${CMAKE_SOURCE_DIR}\"
)
file(MD5 \"\${internal_data_zip}\" internal_data_zip_md5)
file(WRITE \"\${absolute_data_dir}/internalDataHash.txt\"
\${internal_data_zip_md5}
)
")
else()
install(DIRECTORY config DESTINATION ${DATA_DIR})
if (ENABLE_CLIENT OR ENABLE_SERVER)

@ -52,7 +52,7 @@
"hidden": true,
"cacheVariables": {
"ENABLE_LOBBY": "ON",
"ENABLE_TEST": "ON",
"ENABLE_TEST": "ON",
"ENABLE_LUA": "ON"
}
},
@ -294,6 +294,15 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
}
},
{
"name": "android-daily-release",
"displayName": "Android daily release",
"description": "VCMI Android daily build",
"inherits": "android-conan-ninja-release",
"cacheVariables": {
"ANDROID_GRADLE_PROPERTIES": "applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily"
}
}
],
"buildPresets": [
@ -412,6 +421,11 @@
"name": "android-conan-ninja-release",
"configurePreset": "android-conan-ninja-release",
"inherits": "default-release"
},
{
"name": "android-daily-release",
"configurePreset": "android-daily-release",
"inherits": "android-conan-ninja-release"
}
],
"testPresets": [

@ -1,3 +1,79 @@
# 1.5.2 -> 1.5.3
### Stability
* Fixed possible crash when hero class has no valid commander.
* Fixed crash when pressing spacebar or enter during combat when hero has no tactics skill.
* Fixed crash when receiving a commander level-up after winning a battle in a garrison owned by an enemy player.
* Fixed possible crash when exiting a multiplayer game.
* Game will now display an error message and exit after loading instead of crashing silently if a creature's combat animation is missing.
* Game should now generate crash dump on uncaught c++ exception throw
* Fixed crash when player finishes game with negative score
* Fixed crash when opening tavern window in some localisations
* Fixed crash on loading previously generated random map when mods that add object with same name are used
* Game will now display an error message instead of silent crash if game data directory is not accessible
### Mechanics
* Transport Artefact victory condition will no longer trigger if another player has completed it.
* Fixed wandering monster combat not triggering when landing in its zone of control when flying from above the monster using the Fly spell.
* Fixed potentially infinite movement loop when the hero has Admiral's Hat whirlpool immunity and the hero tries to enter and exit the same whirlpool.
* If game picks gold for a random resource pile that has predetermined by map amount, its amount will be correctly multiplied by 100
* Fixed hero not being able to learn spells from a mod in some cases, even if they are available from the town's mage guild.
* The game will now actually take resources from seers' huts with the Gather Resources mission instead of awarding them.
* Heroes with double spell points will no longer trigger the Mana Vortex.
* If turn timer runs out during pve battle game will end player turn after a battle instead of forcing retreat
### Interface
* Fixed reversed button functions in Exchange Window
* Fixed allied towns being missing from the list when using the advanced or expert Town Portal spell.
* Fixed corrupted UI that could appear for a frame under certain conditions
* The '*' symbol and non-printable characters can no longer be used in savegames due to Windows file system restrictions.
* Pressing Ctrl while hovering over the adventure map will now display tile coordinates in the status bar.
* Selection of another hero while hero is selected now requires Shift press instead of Ctrl
* Fixed hero troops in the info box view flashing briefly during hero movement.
* Reduced excessive memory usage on adventure map by several hundreds of megabytes (most noticeable on systems with large screen resolution)
* Haptic feedback is now enabled by default on Android and on iOS
* It is now possible to scroll through artifacts backpack using mouse wheel or swipe
### Launcher
* Android now uses the same Qt-based launcher as other systems
* Fixed attempt to install a submod when installing new mod that depends on a submod of another mod
* Fixed wrong order of activating mods in chain when installing multiple mods at once
* Mod list no longer shows mod version column. Version is now only shown in the mod description.
* Launcher will now skip the Heroes 3 data import step if data has been found automatically
* Fixed inport of existing data files on iOS. This option now requires iOS 13 or later
* Fixed import using offline installer on iOS.
* Buttons to open data directories in the Help tab are now hidden on mobile systems if they can't be opened with file browser
* Added the configuration files directory to the Help tab as it is located separately on Linux systems
* Removed H3 data language selection during setup in favor of auto-detection
* Replaced checkboxes with toggle buttons for easier of access on touchscreens.
* Added interface for configuring several previously existing but inaccessible options in Launcher:
* Selection of input tolerance precision for all input types
* Relative cursor mode for mobile systems (was only available on Android)
* Haptic feedback toggle for mobile systems (was only available on Android)
* Sound and music volume (was only available in game)
* Selection of long touch interval (was only available in game)
* Selection of upscaling filter used by SDL
* Controller input sensitivity and acceleration.
### AI
* Fixed crash when Nullkiller AI tries to explore after losing the hero in combat.
* Fixed rare crash when Nullkiller AI tries to use portals
* Fixed potential crash when Nullkiller AI has access to Town Portal spell
* Fixed potential crash when Battle AI selects a spell to cast from a hero with summon spells.
* Several fixes to Nullkiller AI exploration logic
* Fixed bug leading to Battle AI doing nothing if targeted unit is unreachable
### Random Maps Generator
* Fixed crash when player selects a random number of players and selects a different colour to play, resulting in a non-continuous list of players.
* Fixed rare crash when generating maps with water
### Map Editor
* Fixed crash on closing map editor
### Modding
* Added new building type 'thievesGuild' which implements HotA building in Cove.
* Creature terrain limiter now actually accepts terrain as parameter
# 1.5.1 -> 1.5.2
### Stability

@ -348,6 +348,15 @@ namespace vstd
return std::find(c.begin(),c.end(),i);
}
// returns existing value from map, or default value if key does not exists
template <typename Map>
const typename Map::mapped_type & find_or(const Map& m, const typename Map::key_type& key, const typename Map::mapped_type& defaultValue) {
auto it = m.find(key);
if (it == m.end())
return defaultValue;
return it->second;
}
//returns first key that maps to given value if present, returns success via found if provided
template <typename Key, typename T>
Key findKey(const std::map<Key, T> & map, const T & value, bool * found = nullptr)
@ -684,20 +693,6 @@ namespace vstd
return false;
}
template<class M, class Key, class F>
typename M::mapped_type & getOrCompute(M & m, const Key & k, F f)
{
typedef typename M::mapped_type V;
std::pair<typename M::iterator, bool> r = m.insert(typename M::value_type(k, V()));
V & v = r.first->second;
if(r.second)
f(v);
return v;
}
//c++20 feature
template<typename Arithmetic, typename Floating>
Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f)

@ -252,6 +252,13 @@
"vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden",
"vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden",
"vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden",
"vcmi.battleWindow.damageRetaliation.will" : "Wird Vergeltung üben ",
"vcmi.battleWindow.damageRetaliation.may" : "Kann Vergeltung üben ",
"vcmi.battleWindow.damageRetaliation.never" : "Wird keine Vergeltung üben.",
"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
"vcmi.battleWindow.killed" : "Getötet",
"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s wurden durch gezielte Schüsse getötet!",
"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s wurde mit einem gezielten Schuss getötet!",

@ -55,7 +55,7 @@
"vcmi.radialWheel.moveDown" : "Mover para baixo",
"vcmi.radialWheel.moveBottom" : "Mover para o fundo",
"vcmi.spellBook.search" : "procurar...",
"vcmi.spellBook.search" : "Procurar...",
"vcmi.mainMenu.serverConnecting" : "Conectando...",
"vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:",
@ -139,9 +139,9 @@
"vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.",
"vcmi.server.errors.modsToEnable" : "{Os seguintes mods são necessários}",
"vcmi.server.errors.modsToDisable" : "{Os seguintes mods devem ser desativados}",
"vcmi.server.errors.modNoDependency" : "Falha ao carregar mod {'%s'}!\n Ele depende do mod {'%s'} que não está ativo!\n",
"vcmi.server.errors.modConflict" : "Falha ao carregar mod {'%s'}!\n Conflita com o mod ativo {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Falha ao carregar salvamento! Entidade desconhecida '%s' encontrada no jogo salvo! O salvamento pode não ser compatível com a versão atualmente instalada dos mods!",
"vcmi.server.errors.modNoDependency" : "Falha ao carregar o mod {'%s'}!\n Ele depende do mod {'%s'} que não está ativo!\n",
"vcmi.server.errors.modConflict" : "Falha ao carregar o mod {'%s'}!\n Conflita com o mod ativo {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Falha ao carregar o salvamento! Entidade desconhecida '%s' encontrada no jogo salvo! O salvamento pode não ser compatível com a versão atualmente instalada dos mods!",
"vcmi.dimensionDoor.seaToLandError" : "Não é possível teleportar do mar para a terra ou vice-versa com uma Porta Dimensional.",
@ -189,8 +189,8 @@
"vcmi.adventureOptions.infoBarPick.help" : "{Mostra as Mensagens no Painel de Informações}\n\nSempre que possível, as mensagens do jogo provenientes de objetos no mapa serão mostradas no painel de informações, em vez de aparecerem em uma janela separada.",
"vcmi.adventureOptions.numericQuantities.hover" : "Quantidades Numéricas de Criaturas",
"vcmi.adventureOptions.numericQuantities.help" : "{Quantidades Numéricas de Criaturas}\n\nMostra as quantidades aproximadas de criaturas inimigas no formato numérico A-B.",
"vcmi.adventureOptions.forceMovementInfo.hover" : "Mostrar Sempre o Custo de Movimento",
"vcmi.adventureOptions.forceMovementInfo.help" : "{Mostrar Sempre o Custo de Movimento}\n\nSempre mostra os dados de pontos de movimento na barra de status (em vez de apenas visualizá-los enquanto você mantém pressionada a tecla ALT).",
"vcmi.adventureOptions.forceMovementInfo.hover" : "Sempre Mostrar o Custo de Movimento",
"vcmi.adventureOptions.forceMovementInfo.help" : "{Sempre Mostrar o Custo de Movimento}\n\nSempre mostra os dados de pontos de movimento na barra de status (em vez de apenas visualizá-los enquanto você mantém pressionada a tecla ALT).",
"vcmi.adventureOptions.showGrid.hover" : "Mostrar Grade",
"vcmi.adventureOptions.showGrid.help" : "{Mostrar Grade}\n\nMostra a sobreposição da grade, destacando as fronteiras entre as telhas do mapa de aventura.",
"vcmi.adventureOptions.borderScroll.hover" : "Rolagem de Borda",
@ -235,8 +235,8 @@
"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostra as Janelas de Estatísticas de Heróis}\n\nAlterna permanentemente as janelas de estatísticas dos heróis que mostram estatísticas primárias e pontos de feitiço.",
"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pular Música de Introdução",
"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pula a Música de Introdução}\n\nPermite ações durante a música de introdução que toca no início de cada batalha.",
"vcmi.battleOptions.endWithAutocombat.hover": "Terminar a Batalha",
"vcmi.battleOptions.endWithAutocombat.help": "{Termina a Batalha}\n\nO Combate Automático reproduz a batalha até o final instantâneo.",
"vcmi.battleOptions.endWithAutocombat.hover": "Terminar a batalha",
"vcmi.battleOptions.endWithAutocombat.help": "{Termina a batalha}\n\nO Combate Automático reproduz a batalha até o final instantâneo.",
"vcmi.adventureMap.revisitObject.hover" : "Revisitar Objeto",
"vcmi.adventureMap.revisitObject.help" : "{Revisitar Objeto}\n\nSe um herói estiver atualmente em um Objeto do Mapa, ele pode revisitar o local.",
@ -277,9 +277,9 @@
"vcmi.tutorialWindow.decription.AbortSpell" : "Toque e mantenha pressionado para cancelar um feitiço.",
"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Mostrar Criaturas Disponíveis",
"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Mostrar Criaturas Disponíveis}\n\nMostra o número de criaturas disponíveis para compra em vez de seu crescimento no resumo da cidade (canto inferior esquerdo da tela da cidade).",
"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Mostrar Crescimento Semanal de Criaturas",
"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Mostrar Crescimento Semanal de Criaturas}\n\nMostra o crescimento semanal das criaturas em vez da quantidade disponível no resumo da cidade (canto inferior esquerdo da tela da cidade).",
"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Mostrar Criaturas Disponíveis}\n\nMostra o número de criaturas disponíveis para compra em vez de sua produção no resumo da cidade (canto inferior esquerdo da tela da cidade).",
"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Mostrar Produção Semanal de Criaturas",
"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Mostrar Produção Semanal de Criaturas}\n\nMostra a produção semanal das criaturas em vez da quantidade disponível no resumo da cidade (canto inferior esquerdo da tela da cidade).",
"vcmi.otherOptions.compactTownCreatureInfo.hover" : "Informações Compactas de Criaturas",
"vcmi.otherOptions.compactTownCreatureInfo.help" : "{Informações Compactas de Criaturas}\n\nMostra informações menores para criaturas da cidade no resumo da cidade (canto inferior esquerdo da tela da cidade).",
@ -352,18 +352,18 @@
"vcmi.optionsTab.turnTime.select" : "Selecionar cronômetro do turno",
"vcmi.optionsTab.turnTime.unlimited" : "Tempo de turno ilimitado",
"vcmi.optionsTab.turnTime.classic.1" : "Cronômetro clássico: 1 minuto",
"vcmi.optionsTab.turnTime.classic.2" : "Cronômetro clássico: 2 minutos",
"vcmi.optionsTab.turnTime.classic.5" : "Cronômetro clássico: 5 minutos",
"vcmi.optionsTab.turnTime.classic.10" : "Cronômetro clássico: 10 minutos",
"vcmi.optionsTab.turnTime.classic.20" : "Cronômetro clássico: 20 minutos",
"vcmi.optionsTab.turnTime.classic.30" : "Cronômetro clássico: 30 minutos",
"vcmi.optionsTab.turnTime.chess.20" : "Xadrez: 20:00 + 10:00 + 02:00 + 00:00",
"vcmi.optionsTab.turnTime.chess.16" : "Xadrez: 16:00 + 08:00 + 01:30 + 00:00",
"vcmi.optionsTab.turnTime.chess.8" : "Xadrez: 08:00 + 04:00 + 01:00 + 00:00",
"vcmi.optionsTab.turnTime.chess.4" : "Xadrez: 04:00 + 02:00 + 00:30 + 00:00",
"vcmi.optionsTab.turnTime.chess.2" : "Xadrez: 02:00 + 01:00 + 00:15 + 00:00",
"vcmi.optionsTab.turnTime.chess.1" : "Xadrez: 01:00 + 01:00 + 00:00 + 00:00",
"vcmi.optionsTab.turnTime.classic.1" : "Cronômetro clássico 1 minuto",
"vcmi.optionsTab.turnTime.classic.2" : "Cronômetro clássico 2 minutos",
"vcmi.optionsTab.turnTime.classic.5" : "Cronômetro clássico 5 minutos",
"vcmi.optionsTab.turnTime.classic.10" : "Cronômetro clássico 10 minutos",
"vcmi.optionsTab.turnTime.classic.20" : "Cronômetro clássico 20 minutos",
"vcmi.optionsTab.turnTime.classic.30" : "Cronômetro clássico 30 minutos",
"vcmi.optionsTab.turnTime.chess.20" : "Xadrez 20:00 + 10:00 + 02:00 + 00:00",
"vcmi.optionsTab.turnTime.chess.16" : "Xadrez 16:00 + 08:00 + 01:30 + 00:00",
"vcmi.optionsTab.turnTime.chess.8" : "Xadrez 08:00 + 04:00 + 01:00 + 00:00",
"vcmi.optionsTab.turnTime.chess.4" : "Xadrez 04:00 + 02:00 + 00:30 + 00:00",
"vcmi.optionsTab.turnTime.chess.2" : "Xadrez 02:00 + 01:00 + 00:15 + 00:00",
"vcmi.optionsTab.turnTime.chess.1" : "Xadrez 01:00 + 01:00 + 00:00 + 00:00",
"vcmi.optionsTab.simturns.select" : "Selecionar turnos simultâneos",
"vcmi.optionsTab.simturns.none" : "Sem turnos simultâneos",
@ -490,13 +490,13 @@
"core.bonus.AIR_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia do Ar",
"core.bonus.ATTACKS_ALL_ADJACENT.name" : "Ataque em Todas as Direções",
"core.bonus.ATTACKS_ALL_ADJACENT.description" : "Ataca todos os inimigos adjacentes",
"core.bonus.BLOCKS_RETALIATION.name" : "Sem Contra-ataques",
"core.bonus.BLOCKS_RETALIATION.name" : "Evita Contra-ataques",
"core.bonus.BLOCKS_RETALIATION.description" : "O inimigo não pode contra-atacar",
"core.bonus.BLOCKS_RANGED_RETALIATION.name" : "Sem Contra-ataques à Distância",
"core.bonus.BLOCKS_RANGED_RETALIATION.name" : "Evita Contra-ataques à Distância",
"core.bonus.BLOCKS_RANGED_RETALIATION.description" : "O inimigo não pode contra-atacar usando um ataque à distância",
"core.bonus.CATAPULT.name" : "Catapulta",
"core.bonus.CATAPULT.description" : "Ataca as muralhas de cerco",
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Reduz Custo de Conjuração (${val})",
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Custo de Conjuração (${val})",
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Reduz o custo de conjuração de feitiços para o herói em ${val}",
"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name" : "Absorvedor Mágico (${val})",
"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description" : "Aumenta o custo de conjuração dos feitiços inimigos em ${val}",
@ -507,7 +507,7 @@
"core.bonus.DEATH_STARE.name" : "Olhar da Morte (${val}%)",
"core.bonus.DEATH_STARE.description" : "Tem ${val}% de chance de matar uma única criatura",
"core.bonus.DEFENSIVE_STANCE.name" : "Bônus de Defesa",
"core.bonus.DEFENSIVE_STANCE.description" : "+${val} de defesa ao se defender",
"core.bonus.DEFENSIVE_STANCE.description" : "+${val} de Defesa ao se defender",
"core.bonus.DESTRUCTION.name" : "Destruição",
"core.bonus.DESTRUCTION.description" : "Tem ${val}% de chance de matar unidades extras após o ataque",
"core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Golpe Mortal",
@ -521,7 +521,7 @@
"core.bonus.ENCHANTED.name" : "Encantado",
"core.bonus.ENCHANTED.description" : "Afetado por ${subtype.spell} permanente",
"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ignorar Ataque (${val}%)",
"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Ao ser atacado, ${val}% do ataque do atacante é ignorado",
"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Ao ser atacado, ${val}% do ataque do agressor é ignorado",
"core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ignorar Defesa (${val}%)",
"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "Ao atacar, ${val}% da defesa do defensor é ignorada",
"core.bonus.FIRE_IMMUNITY.name" : "Imunidade ao Fogo",
@ -545,7 +545,7 @@
"core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Redução de Dano (${val}%)",
"core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Reduz o dano físico de ataques à distância ou corpo a corpo",
"core.bonus.HATE.name" : "Odeia ${subtype.creature}",
"core.bonus.HATE.description" : "Causa ${val}% a mais de dano a ${subtype.creature}",
"core.bonus.HATE.description" : "${val}% a mais de dano a ${subtype.creature}",
"core.bonus.HEALER.name" : "Curandeiro",
"core.bonus.HEALER.description" : "Cura unidades aliadas",
"core.bonus.HP_REGENERATION.name" : "Regeneração",

@ -125,6 +125,13 @@
"vcmi.lobby.mod.state.version" : "Розбіжність версій",
"vcmi.lobby.mod.state.excessive" : "Має бути вимкнена",
"vcmi.lobby.mod.state.missing" : "Не встановлена",
"vcmi.lobby.pvp.coin.hover" : "Монетка.",
"vcmi.lobby.pvp.coin.help" : "Підкинути монетку",
"vcmi.lobby.pvp.randomTown.hover" : "Випадкове місто",
"vcmi.lobby.pvp.randomTown.help" : "Написати в чаті випадкове місто",
"vcmi.lobby.pvp.randomTownVs.hover" : "Випадкові міста",
"vcmi.lobby.pvp.randomTownVs.help" : "Написати в чаті два випадкових міста",
"vcmi.lobby.pvp.versus" : "проти",
"vcmi.client.errors.invalidMap" : "{Пошкоджена карта або кампанія}\n\nНе вдалося запустити гру! Вибрана карта або кампанія може бути невірною або пошкодженою. Причина:\n%s",
"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
@ -253,7 +260,6 @@
"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
"vcmi.battleWindow.killed" : "Загинуло",
"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s було вбито влучними пострілами!",
"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s було вбито влучним пострілом!",
"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s було вбито влучними пострілами!",
@ -381,6 +387,14 @@
"vcmi.optionsTab.simturns.months.1" : " %d місяць",
"vcmi.optionsTab.simturns.months.2" : " %d місяці",
"vcmi.optionsTab.extraOptions.hover" : "Розширені опції",
"vcmi.optionsTab.extraOptions.help" : "Додаткові налаштування для гри",
"vcmi.optionsTab.cheatAllowed.hover" : "Дозволити чит-коди",
"vcmi.optionsTab.unlimitedReplay.hover" : "Необмежена кількість перегравань бою",
"vcmi.optionsTab.cheatAllowed.help" : "{Дозволити чіт-коди}\nДозволяє вводити чит-коди під час гри.",
"vcmi.optionsTab.unlimitedReplay.help" : "{Необмежена кількість перегравань бою}\nКількість перегравань боїв не обмежена.",
// Custom victory conditions for H3 campaigns and HotA maps
"vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!",
"vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!",
@ -389,6 +403,84 @@
"vcmi.map.victoryCondition.collectArtifacts.message" : "Здобути три артефакти",
"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Вітаємо! Усі ваші вороги переможені, і ви маєте Альянс Ангелів! Перемога ваша!",
"vcmi.map.victoryCondition.angelicAlliance.message" : "Перемогти всіх ворогів і створити Альянс Ангелів",
"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "На жаль, ви втратили частину Альянсу Ангелів. Все втрачено.",
// few strings from WoG used by vcmi
// "vcmi.stackExperience.description" : "» S t a c k E x p e r i e n c e D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i",
"vcmi.stackExperience.rank.0" : "Базовий",
"vcmi.stackExperience.rank.1" : "Початківець",
"vcmi.stackExperience.rank.2" : "Підготовлений",
"vcmi.stackExperience.rank.3" : "Кваліфікований",
"vcmi.stackExperience.rank.4" : "Перевірений",
"vcmi.stackExperience.rank.5" : "Ветеран",
"vcmi.stackExperience.rank.6" : "Адепт",
"vcmi.stackExperience.rank.7" : "Експерт",
"vcmi.stackExperience.rank.8" : "Еліта",
"vcmi.stackExperience.rank.9" : "Майстер",
"vcmi.stackExperience.rank.10" : "Ас",
// Strings for HotA Seer Hut / Quest Guards
"core.seerhut.quest.heroClass.complete.0" : "А, ти %s. Ось тобі подарунок. Приймаєш?",
"core.seerhut.quest.heroClass.complete.1" : "А, ти %s. Ось тобі подарунок. Приймаєш?",
"core.seerhut.quest.heroClass.complete.2" : "А, ти %s. Ось тобі подарунок. Приймаєш?",
"core.seerhut.quest.heroClass.complete.3" : "Вартові помічають, що ви - %s, і пропонують вас пропустити. Чи погоджуєтесь ви?",
"core.seerhut.quest.heroClass.complete.4" : "Вартові помічають, що ви - %s, і пропонують вас пропустити. Чи погоджуєтесь ви?",
"core.seerhut.quest.heroClass.complete.5" : "Вартові помічають, що ви - %s, і пропонують вас пропустити. Чи погоджуєтесь ви?",
"core.seerhut.quest.heroClass.description.0" : "Відправити %s до %s",
"core.seerhut.quest.heroClass.description.1" : "Відправити %s до %s",
"core.seerhut.quest.heroClass.description.2" : "Відправити %s до %s",
"core.seerhut.quest.heroClass.description.3" : "Відправити %s до щоб відкрити ворота",
"core.seerhut.quest.heroClass.description.4" : "Відправити %s до щоб відкрити ворота",
"core.seerhut.quest.heroClass.description.5" : "Відправити %s до щоб відкрити ворота",
"core.seerhut.quest.heroClass.hover.0" : "(шукає героя класу %s)",
"core.seerhut.quest.heroClass.hover.1" : "(шукає героя класу %s)",
"core.seerhut.quest.heroClass.hover.2" : "(шукає героя класу %s)",
"core.seerhut.quest.heroClass.hover.3" : "(шукає героя класу %s)",
"core.seerhut.quest.heroClass.hover.4" : "(шукає героя класу %s)",
"core.seerhut.quest.heroClass.hover.5" : "(шукає героя класу %s)",
"core.seerhut.quest.heroClass.receive.0" : "У мене є подарунок для %s.",
"core.seerhut.quest.heroClass.receive.1" : "У мене є подарунок для %s.",
"core.seerhut.quest.heroClass.receive.2" : "У мене є подарунок для %s.",
"core.seerhut.quest.heroClass.receive.3" : "Вартові кажуть, що пропускають лише %s.",
"core.seerhut.quest.heroClass.receive.4" : "Вартові кажуть, що пропускають лише %s.",
"core.seerhut.quest.heroClass.receive.5" : "Вартові кажуть, що пропускають лише %s.",
"core.seerhut.quest.heroClass.visit.0" : "Ти не %s. У мене для тебе нічого немає. Йди геть!",
"core.seerhut.quest.heroClass.visit.1" : "Ти не %s. У мене для тебе нічого немає. Йди геть!",
"core.seerhut.quest.heroClass.visit.2" : "Ти не %s. У мене для тебе нічого немає. Йди геть!",
"core.seerhut.quest.heroClass.visit.3" : "Вартові пропускають лише %s.",
"core.seerhut.quest.heroClass.visit.4" : "Вартові пропускають лише %s.",
"core.seerhut.quest.heroClass.visit.5" : "Вартові пропускають лише %s.",
"core.seerhut.quest.reachDate.complete.0" : "Тепер я вільний. Ось що у мене є для тебе. Ти приймаєш?",
"core.seerhut.quest.reachDate.complete.1" : "Тепер я вільний. Ось що у мене є для тебе. Ти приймаєш?",
"core.seerhut.quest.reachDate.complete.2" : "Тепер я вільний. Ось що у мене є для тебе. Ти приймаєш?",
"core.seerhut.quest.reachDate.complete.3" : "Тепер ви можете пройти. Бажаєте пройти?",
"core.seerhut.quest.reachDate.complete.4" : "Тепер ви можете пройти. Бажаєте пройти?",
"core.seerhut.quest.reachDate.complete.5" : "Тепер ви можете пройти. Бажаєте пройти?",
"core.seerhut.quest.reachDate.description.0" : "Зачекайте до %s для %s",
"core.seerhut.quest.reachDate.description.1" : "Зачекайте до %s для %s",
"core.seerhut.quest.reachDate.description.2" : "Зачекайте до %s для %s",
"core.seerhut.quest.reachDate.description.3" : "Зачекайте до %s, щоб відкрити ворота",
"core.seerhut.quest.reachDate.description.4" : "Зачекайте до %s, щоб відкрити ворота",
"core.seerhut.quest.reachDate.description.5" : "Зачекайте до %s, щоб відкрити ворота",
"core.seerhut.quest.reachDate.hover.0" : "(Повертайтеся не раніше, ніж через %s)",
"core.seerhut.quest.reachDate.hover.1" : "(Повертайтеся не раніше, ніж через %s)",
"core.seerhut.quest.reachDate.hover.2" : "(Повертайтеся не раніше, ніж через %s)",
"core.seerhut.quest.reachDate.hover.3" : "(Повертайтеся не раніше, ніж через %s)",
"core.seerhut.quest.reachDate.hover.4" : "(Повертайтеся не раніше, ніж через %s)",
"core.seerhut.quest.reachDate.hover.5" : "(Повертайтеся не раніше, ніж через %s)",
"core.seerhut.quest.reachDate.receive.0" : "Я зайнят. Повертайтеся не раніше, ніж через %s",
"core.seerhut.quest.reachDate.receive.1" : "Я зайнят. Повертайтеся не раніше, ніж через %s",
"core.seerhut.quest.reachDate.receive.2" : "Я зайнят. Повертайтеся не раніше, ніж через %s",
"core.seerhut.quest.reachDate.receive.3" : "Закрито до %s.",
"core.seerhut.quest.reachDate.receive.4" : "Закрито до %s.",
"core.seerhut.quest.reachDate.receive.5" : "Закрито до %s.",
"core.seerhut.quest.reachDate.visit.0" : "Я зайнятий. Приходь не раніше, ніж %s",
"core.seerhut.quest.reachDate.visit.1" : "Я зайнятий. Приходь не раніше, ніж %s",
"core.seerhut.quest.reachDate.visit.2" : "Я зайнятий. Приходь не раніше, ніж %s",
"core.seerhut.quest.reachDate.visit.3" : "Закрито до %s.",
"core.seerhut.quest.reachDate.visit.4" : "Закрито до %s.",
"core.seerhut.quest.reachDate.visit.5" : "Закрито до %s.",
"core.bonus.ADDITIONAL_ATTACK.name" : "Подвійний удар",
"core.bonus.ADDITIONAL_ATTACK.description" : "Атакує двічі",

4
android/.gitignore vendored

@ -1,9 +1,11 @@
*.iml
.gradle
/local.properties
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# generated by CMake build
/vcmi-app/gradle.properties

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.vcmi.vcmi">
<!-- %%INSERT_PERMISSIONS -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
Remove the comment if you do not require these default permissions. -->
<!-- %%INSERT_PERMISSIONS_DISABLED -->
<!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application.
Remove the comment if you do not require these default features. -->
<!-- %%INSERT_FEATURES -->
<supports-screens
android:largeScreens="true"
android:xlargeScreens="true" />
<application
android:name="org.qtproject.qt5.android.bindings.QtApplication"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:allowBackup="false"
android:installLocation="auto"
android:icon="@mipmap/ic_launcher"
android:label="${applicationLabel}"
android:testOnly="false"
android:supportsRtl="true"
android:usesCleartextTraffic="false">
<activity
android:name=".ActivityLauncher"
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
android:exported="true"
android:screenOrientation="sensorLandscape">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
<meta-data android:name="android.app.repository" android:value="default"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
<!-- Deploy Qt libs as part of package -->
<meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
<!-- Run with local libs -->
<meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
<meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
<meta-data android:name="android.app.load_local_libs_resource_id" android:resource="@array/load_local_libs"/>
<meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
<meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
<!-- Messages maps -->
<meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/>
<meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/>
<meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/>
<meta-data android:value="@string/unsupported_android_version" android:name="android.app.unsupported_android_version"/>
<!-- Messages maps -->
<!-- Background running -->
<!-- Warning: changing this value to true may cause unexpected crashes if the
application still try to draw after
"applicationStateChanged(Qt::ApplicationSuspended)"
signal is sent! -->
<meta-data android:name="android.app.background_running" android:value="false"/>
<!-- Background running -->
<!-- auto screen scale factor -->
<meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
<!-- auto screen scale factor -->
<!-- extract android style -->
<!-- available android:values :
* default - In most cases this will be the same as "full", but it can also be something else if needed, e.g., for compatibility reasons
* full - useful QWidget & Quick Controls 1 apps
* minimal - useful for Quick Controls 2 apps, it is much faster than "full"
* none - useful for apps that don't use any of the above Qt modules
-->
<meta-data android:name="android.app.extract_android_style" android:value="none"/>
<!-- extract android style -->
</activity>
<activity
android:name=".VcmiSDLActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="sensorLandscape" />
<service
android:name=".ServerService"
android:process="eu.vcmi.vcmi.srv"
android:description="@string/server_name"
android:exported="false"/>
</application>
</manifest>

@ -1,9 +0,0 @@
package eu.vcmi.vcmi.util;
/**
* Generated via cmake
*/
public class GeneratedVersion
{
public static final String VCMI_VERSION = "@VCMI_VERSION@";
}

@ -0,0 +1,17 @@
{
"android-min-sdk-version": "@CMAKE_ANDROID_API@",
"android-package-source-directory": "@androidPackageSourceDir@",
"android-target-sdk-version": "@ANDROID_TARGET_SDK_VERSION@",
"application-binary": "vcmiclient",
"architectures": {
"@ANDROID_ABI@": "@ANDROID_SYSROOT_LIB_SUBDIR@"
},
"ndk": "@CMAKE_ANDROID_NDK@",
"ndk-host": "@ANDROID_HOST_TAG@",
"qt": "@qtDir@",
"sdk": "@androidSdkDir@",
"sdkBuildToolsRevision": "31.0.0",
"stdcpp-path": "@ANDROID_TOOLCHAIN_ROOT@/sysroot/usr/lib/",
"tool-prefix": "llvm",
"toolchain-prefix": "llvm"
}

@ -1,4 +1,3 @@
ext {
// these values will be retrieved during gradle build
gitInfoVcmi = "none"
}

@ -7,13 +7,18 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Qt-generated properties

@ -1,8 +0,0 @@
/build
# generated by CMake build
/src/main/assets/internalData.zip
/src/main/assets/internalDataHash.txt
/src/main/java/eu/vcmi/vcmi/util/GeneratedVersion.java
/src/main/jniLibs
/src/main/res/raw/authors.txt

@ -1,20 +1,49 @@
plugins {
id 'com.android.application'
}
apply plugin: 'com.android.application'
android {
compileSdk 33
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
ndkVersion '25.2.9519653'
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
defaultConfig {
applicationId "is.xyz.vcmi"
minSdk 19
targetSdk 33
versionCode 1522
versionName "1.5.2"
compileSdk = androidCompileSdkVersion.takeAfter("-") as Integer // has "android-" prepended
minSdk = qtMinSdkVersion as Integer
targetSdk = qtTargetSdkVersion as Integer // ANDROID_TARGET_SDK_VERSION in the CMake project
versionCode 1530
versionName "1.5.3"
setProperty("archivesBaseName", "vcmi")
}
sourceSets {
main {
// Qt requires these to be in the android project root
manifest.srcFile '../AndroidManifest.xml'
jniLibs.srcDirs = ['../libs']
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'src/main/res', '../res']
}
}
signingConfigs {
releaseSigning
dailySigning
@ -36,27 +65,18 @@ android {
release {
minifyEnabled false
zipAlignEnabled true
signingConfig signingConfigs.releaseSigning
applicationIdSuffix = project.findProperty('applicationIdSuffix')
signingConfig = signingConfigs[project.findProperty('signingConfig') ?: 'releaseSigning']
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
manifestPlaceholders = [
applicationLabel: '@string/app_name',
applicationLabel: project.findProperty('applicationLabel') ?: 'VCMI',
]
ndk {
debugSymbolLevel 'full'
}
}
daily {
initWith release
applicationIdSuffix '.daily'
signingConfig signingConfigs.dailySigning
manifestPlaceholders = [
applicationLabel: 'VCMI daily',
]
}
}
applicationVariants.all { variant -> RenameOutput(project.archivesBaseName, variant) }
tasks.withType(JavaCompile) {
options.compilerArgs += ["-Xlint:deprecation"]
}
@ -66,30 +86,9 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
dataBinding true
}
}
def RenameOutput(final baseName, final variant) {
final def buildTaskId = System.getenv("GITHUB_RUN_ID")
ResolveGitInfo()
def name = baseName + "-" + ext.gitInfoVcmi
if (buildTaskId != null && !buildTaskId.isEmpty()) {
name = buildTaskId + "-" + name
}
if (!variant.buildType.name != "release") {
name += "-" + variant.buildType.name
}
variant.outputs.each { output ->
def oldPath = output.outputFile.getAbsolutePath()
output.outputFileName = name + oldPath.substring(oldPath.lastIndexOf("."))
// Do not compress Qt binary resources file
aaptOptions {
noCompress 'rcc'
}
}
@ -111,16 +110,6 @@ def CommandOutput(final cmd, final arguments, final cwd) {
}
}
def ResolveGitInfo() {
if (ext.gitInfoVcmi != "none") {
return
}
ext.gitInfoVcmi =
CommandOutput("git", ["log", "-1", "--pretty=%D", "--decorate-refs=refs/remotes/origin/*"], ".").replace("origin/", "").replace(", HEAD", "").replaceAll("[^a-zA-Z0-9\\-_]", "_") +
"-" +
CommandOutput("git", ["describe", "--match=", "--always", "--abbrev=7"], ".")
}
def SigningPropertiesPath(final basePath, final signingConfigKey) {
return file("${basePath}/${signingConfigKey}.properties")
}
@ -130,9 +119,8 @@ def SigningKeystorePath(final basePath, final keystoreFileName) {
}
def LoadSigningConfig(final signingConfigKey) {
final def projectRoot = "${project.projectDir}/../../CI/android"
final def props = new Properties()
final def propFile = SigningPropertiesPath(projectRoot, signingConfigKey)
final def propFile = SigningPropertiesPath(signingRoot, signingConfigKey)
def signingConfig = android.signingConfigs.getAt(signingConfigKey)
@ -143,7 +131,7 @@ def LoadSigningConfig(final signingConfigKey) {
&& props.containsKey('STORE_FILE')
&& props.containsKey('KEY_ALIAS')) {
signingConfig.storeFile = SigningKeystorePath(projectRoot, props['STORE_FILE'])
signingConfig.storeFile = SigningKeystorePath(signingRoot, props['STORE_FILE'])
signingConfig.storePassword = props['STORE_PASSWORD']
signingConfig.keyAlias = props['KEY_ALIAS']
@ -167,9 +155,7 @@ def LoadSigningConfig(final signingConfigKey) {
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.gms:play-services-base:18.2.0'
implementation 'com.google.android.gms:play-services-basement:18.1.0'
implementation fileTree(dir: '../libs', include: ['*.jar', '*.aar'])
implementation 'androidx.annotation:annotation:1.7.1'
implementation 'androidx.documentfile:documentfile:1.0.1'
}

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.vcmi.vcmi">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:extractNativeLibs="true"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:allowBackup="false"
android:installLocation="auto"
android:icon="@mipmap/ic_launcher"
android:label="${applicationLabel}"
android:testOnly="false"
android:supportsRtl="true"
android:theme="@style/Theme.VCMI">
<activity
android:exported="true"
android:name=".ActivityLauncher"
android:screenOrientation="sensorLandscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ActivityError"
android:screenOrientation="sensorLandscape" />
<activity
android:name=".ActivityMods"
android:screenOrientation="sensorLandscape" />
<activity
android:name=".ActivityAbout"
android:screenOrientation="sensorLandscape" />
<activity
android:name=".VcmiSDLActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="sensorLandscape"
android:theme="@style/Theme.VCMI.Full" />
<service
android:name=".ServerService"
android:process="eu.vcmi.vcmi.srv"
android:description="@string/server_name"
android:exported="false"/>
</application>
</manifest>

@ -1,94 +0,0 @@
package eu.vcmi.vcmi;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.UnderlineSpan;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import eu.vcmi.vcmi.content.DialogAuthors;
import eu.vcmi.vcmi.util.GeneratedVersion;
import eu.vcmi.vcmi.util.Utils;
/**
* @author F
*/
public class ActivityAbout extends ActivityWithToolbar
{
private static final String DIALOG_AUTHORS_TAG = "DIALOG_AUTHORS_TAG";
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
initToolbar(R.string.about_title);
initControl(R.id.about_version_app, getString(R.string.about_version_app, GeneratedVersion.VCMI_VERSION));
initControl(R.id.about_version_launcher, getString(R.string.about_version_launcher, Utils.appVersionName(this)));
initControlUrl(R.id.about_link_portal, R.string.about_links_main, R.string.url_project_page, this::onUrlPressed);
initControlUrl(R.id.about_link_repo_main, R.string.about_links_repo, R.string.url_project_repo, this::onUrlPressed);
initControlUrl(R.id.about_link_repo_launcher, R.string.about_links_repo_launcher, R.string.url_launcher_repo, this::onUrlPressed);
initControlBtn(R.id.about_btn_authors, this::onBtnAuthorsPressed);
initControlUrl(R.id.about_btn_privacy, R.string.about_btn_privacy, R.string.url_launcher_privacy, this::onUrlPressed);
}
private void initControlBtn(final int viewId, final View.OnClickListener callback)
{
findViewById(viewId).setOnClickListener(callback);
}
private void initControlUrl(final int textViewResId, final int baseTextRes, final int urlTextRes, final IInternalUrlCallback callback)
{
final TextView ctrl = (TextView) findViewById(textViewResId);
final String urlText = getString(urlTextRes);
final String fullText = getString(baseTextRes, urlText);
ctrl.setText(decoratedLinkText(fullText, fullText.indexOf(urlText), fullText.length()));
ctrl.setOnClickListener(v -> callback.onPressed(urlText));
}
private Spanned decoratedLinkText(final String rawText, final int start, final int end)
{
final SpannableString spannableString = new SpannableString(rawText);
spannableString.setSpan(new UnderlineSpan(), start, end, 0);
spannableString.setSpan(new ForegroundColorSpan(ContextCompat.getColor(this, R.color.accent)), start, end, 0);
return spannableString;
}
private void initControl(final int textViewResId, final String text)
{
((TextView) findViewById(textViewResId)).setText(text);
}
private void onBtnAuthorsPressed(final View v)
{
final DialogAuthors dialogAuthors = new DialogAuthors();
dialogAuthors.show(getSupportFragmentManager(), DIALOG_AUTHORS_TAG);
}
private void onUrlPressed(final String url)
{
try
{
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
catch (final ActivityNotFoundException ignored)
{
Toast.makeText(this, R.string.about_error_opening_url, Toast.LENGTH_LONG).show();
}
}
private interface IInternalUrlCallback
{
void onPressed(final String link);
}
}

@ -1,58 +0,0 @@
package eu.vcmi.vcmi;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.SharedPrefs;
/**
* @author F
*/
public abstract class ActivityBase extends AppCompatActivity
{
protected SharedPrefs mPrefs;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setupExceptionHandler();
mPrefs = new SharedPrefs(this);
}
private void setupExceptionHandler()
{
final Thread.UncaughtExceptionHandler prevHandler = Thread.getDefaultUncaughtExceptionHandler();
if (prevHandler != null && !(prevHandler instanceof VCMIExceptionHandler)) // no need to recreate it if it's already setup
{
Thread.setDefaultUncaughtExceptionHandler(new VCMIExceptionHandler(prevHandler));
}
}
private static class VCMIExceptionHandler implements Thread.UncaughtExceptionHandler
{
private Thread.UncaughtExceptionHandler mPrevHandler;
private VCMIExceptionHandler(final Thread.UncaughtExceptionHandler prevHandler)
{
mPrevHandler = prevHandler;
}
@Override
public void uncaughtException(final Thread thread, final Throwable throwable)
{
Log.e(this, "Unhandled exception", throwable); // to save the exception to file before crashing
if (mPrevHandler != null && !(mPrevHandler instanceof VCMIExceptionHandler))
{
mPrevHandler.uncaughtException(thread, throwable);
}
else
{
System.exit(1);
}
}
}
}

@ -1,48 +0,0 @@
package eu.vcmi.vcmi;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import android.view.View;
import android.widget.TextView;
/**
* @author F
*/
public class ActivityError extends ActivityWithToolbar
{
public static final String ARG_ERROR_MSG = "ActivityError.msg";
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_error);
initToolbar(R.string.launcher_title);
final View btnTryAgain = findViewById(R.id.error_btn_try_again);
btnTryAgain.setOnClickListener(new OnErrorRetryPressed());
final Bundle extras = getIntent().getExtras();
if (extras != null)
{
final String errorMessage = extras.getString(ARG_ERROR_MSG);
final TextView errorMessageView = (TextView) findViewById(R.id.error_message);
if (errorMessage != null)
{
errorMessageView.setText(errorMessage);
}
}
}
private class OnErrorRetryPressed implements View.OnClickListener
{
@Override
public void onClick(final View v)
{
// basically restarts main activity
startActivity(new Intent(ActivityError.this, ActivityLauncher.class));
finish();
}
}
}

@ -1,307 +1,62 @@
package eu.vcmi.vcmi;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONObject;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import android.os.Environment;
import android.provider.DocumentsContract;
import eu.vcmi.vcmi.content.AsyncLauncherInitialization;
import eu.vcmi.vcmi.settings.AdventureAiController;
import eu.vcmi.vcmi.settings.LanguageSettingController;
import eu.vcmi.vcmi.settings.CopyDataController;
import eu.vcmi.vcmi.settings.ExportDataController;
import eu.vcmi.vcmi.settings.LauncherSettingController;
import eu.vcmi.vcmi.settings.ModsBtnController;
import eu.vcmi.vcmi.settings.MusicSettingController;
import eu.vcmi.vcmi.settings.PointerModeSettingController;
import eu.vcmi.vcmi.settings.PointerMultiplierSettingController;
import eu.vcmi.vcmi.settings.ScreenScaleSettingController;
import eu.vcmi.vcmi.settings.ScreenScaleSettingDialog;
import eu.vcmi.vcmi.settings.SoundSettingController;
import eu.vcmi.vcmi.settings.StartGameController;
import androidx.annotation.Nullable;
import java.io.File;
import eu.vcmi.vcmi.VcmiSDLActivity;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.SharedPrefs;
import org.libsdl.app.SDL;
/**
* @author F
*/
public class ActivityLauncher extends ActivityWithToolbar
public class ActivityLauncher extends org.qtproject.qt5.android.bindings.QtActivity
{
public static final int PERMISSIONS_REQ_CODE = 123;
private static final int PICK_EXTERNAL_VCMI_DATA_TO_COPY = 1;
private final List<LauncherSettingController<?, ?>> mActualSettings = new ArrayList<>();
private View mProgress;
private TextView mErrorMessage;
private Config mConfig;
private LauncherSettingController<String, Config> mCtrlLanguage;
private LauncherSettingController<PointerModeSettingController.PointerMode, Config> mCtrlPointerMode;
private LauncherSettingController<Void, Void> mCtrlStart;
private LauncherSettingController<Float, Config> mCtrlPointerMulti;
private LauncherSettingController<ScreenScaleSettingController.ScreenScale, Config> mCtrlScreenScale;
private LauncherSettingController<Integer, Config> mCtrlSoundVol;
private LauncherSettingController<Integer, Config> mCtrlMusicVol;
private LauncherSettingController<String, Config> mAiController;
private CopyDataController mCtrlCopy;
private ExportDataController mCtrlExport;
private final AsyncLauncherInitialization.ILauncherCallbacks mInitCallbacks = new AsyncLauncherInitialization.ILauncherCallbacks()
{
@Override
public Activity ctx()
{
return ActivityLauncher.this;
}
@Override
public SharedPrefs prefs()
{
return mPrefs;
}
@Override
public void onInitSuccess()
{
loadConfigFile();
mCtrlStart.show();
mCtrlCopy.show();
mCtrlExport.show();
for (LauncherSettingController<?, ?> setting: mActualSettings) {
setting.show();
}
mErrorMessage.setVisibility(View.GONE);
mProgress.setVisibility(View.GONE);
}
@Override
public void onInitFailure(final AsyncLauncherInitialization.InitResult result)
{
mCtrlCopy.show();
if (result.mFailSilently)
{
return;
}
ActivityLauncher.this.onInitFailure(result);
}
};
public boolean justLaunched = true;
@Override
public void onCreate(final Bundle savedInstanceState)
public void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
justLaunched = savedInstanceState == null;
SDL.setContext(this);
}
if (savedInstanceState == null) // only clear the log if this is initial onCreate and not config change
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent resultData)
{
if (requestCode == PICK_EXTERNAL_VCMI_DATA_TO_COPY && resultCode == Activity.RESULT_OK)
{
Log.init();
}
Log.i(this, "Starting launcher");
setContentView(R.layout.activity_launcher);
initToolbar(R.string.launcher_title, true);
mProgress = findViewById(R.id.launcher_progress);
mErrorMessage = (TextView) findViewById(R.id.launcher_error);
mErrorMessage.setVisibility(View.GONE);
((TextView) findViewById(R.id.launcher_version_info)).setText(getString(R.string.launcher_version, BuildConfig.VERSION_NAME));
initSettingsGui();
}
@Override
public void onStart()
{
super.onStart();
new AsyncLauncherInitialization(mInitCallbacks).execute((Void) null);
}
@Override
public void onBackPressed()
{
saveConfig();
super.onBackPressed();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu)
{
getMenuInflater().inflate(R.menu.menu_launcher, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
if (item.getItemId() == R.id.menu_launcher_about)
{
startActivity(new Intent(this, ActivityAbout.class));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData)
{
if(requestCode == CopyDataController.PICK_EXTERNAL_VCMI_DATA_TO_COPY
&& resultCode == Activity.RESULT_OK)
{
Uri uri;
if (resultData != null)
{
uri = resultData.getData();
mCtrlCopy.copyData(uri);
}
return;
}
if(requestCode == ExportDataController.PICK_DIRECTORY_TO_EXPORT
&& resultCode == Activity.RESULT_OK)
{
Uri uri = null;
if (resultData != null)
{
uri = resultData.getData();
mCtrlExport.copyData(uri);
}
if (resultData != null && FileUtil.copyData(resultData.getData(), this))
NativeMethods.heroesDataUpdate();
return;
}
super.onActivityResult(requestCode, resultCode, resultData);
}
public void requestStoragePermissions()
public void copyHeroesData()
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
{
requestPermissions(
new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
PERMISSIONS_REQ_CODE);
}
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data"))
);
startActivityForResult(intent, PICK_EXTERNAL_VCMI_DATA_TO_COPY);
}
private void initSettingsGui()
public void onLaunchGameBtnPressed()
{
mCtrlStart = new StartGameController(this, v -> onLaunchGameBtnPressed()).init(R.id.launcher_btn_start);
(mCtrlCopy = new CopyDataController(this)).init(R.id.launcher_btn_copy);
(mCtrlExport = new ExportDataController(this)).init(R.id.launcher_btn_export);
new ModsBtnController(this, v -> startActivity(new Intent(ActivityLauncher.this, ActivityMods.class))).init(R.id.launcher_btn_mods);
mCtrlLanguage = new LanguageSettingController(this).init(R.id.launcher_btn_cp, mConfig);
mCtrlPointerMode = new PointerModeSettingController(this).init(R.id.launcher_btn_pointer_mode, mConfig);
mCtrlPointerMulti = new PointerMultiplierSettingController(this).init(R.id.launcher_btn_pointer_multi, mConfig);
mCtrlScreenScale = new ScreenScaleSettingController(this).init(R.id.launcher_btn_scale, mConfig);
mCtrlSoundVol = new SoundSettingController(this).init(R.id.launcher_btn_volume_sound, mConfig);
mCtrlMusicVol = new MusicSettingController(this).init(R.id.launcher_btn_volume_music, mConfig);
mAiController = new AdventureAiController(this).init(R.id.launcher_btn_adventure_ai, mConfig);
mActualSettings.clear();
mActualSettings.add(mCtrlLanguage);
mActualSettings.add(mCtrlPointerMode);
mActualSettings.add(mCtrlPointerMulti);
mActualSettings.add(mCtrlScreenScale);
mActualSettings.add(mCtrlSoundVol);
mActualSettings.add(mCtrlMusicVol);
mActualSettings.add(mAiController);
mCtrlStart.hide(); // start is initially hidden, until we confirm that everything is okay via AsyncLauncherInitialization
mCtrlCopy.hide();
mCtrlExport.hide();
}
private void onLaunchGameBtnPressed()
{
saveConfig();
startActivity(new Intent(ActivityLauncher.this, VcmiSDLActivity.class));
}
private void saveConfig()
{
if (mConfig == null)
{
return;
}
try
{
mConfig.save(new File(FileUtil.configFileLocation(Storage.getVcmiDataDir(this))));
}
catch (final Exception e)
{
Toast.makeText(this, getString(R.string.launcher_error_config_saving_failed, e.getMessage()), Toast.LENGTH_LONG).show();
}
}
private void loadConfigFile()
{
try
{
final String settingsFileContent = FileUtil.read(
new File(FileUtil.configFileLocation(Storage.getVcmiDataDir(this))));
mConfig = Config.load(new JSONObject(settingsFileContent));
}
catch (final Exception e)
{
Log.e(this, "Could not load config file", e);
mConfig = new Config();
}
onConfigUpdated();
}
private void onConfigUpdated()
{
if(mConfig.mScreenScale == -1)
mConfig.updateScreenScale(ScreenScaleSettingDialog.getSupportedScalingRange(ActivityLauncher.this)[1]);
updateCtrlConfig(mCtrlLanguage, mConfig);
updateCtrlConfig(mCtrlPointerMode, mConfig);
updateCtrlConfig(mCtrlPointerMulti, mConfig);
updateCtrlConfig(mCtrlScreenScale, mConfig);
updateCtrlConfig(mCtrlSoundVol, mConfig);
updateCtrlConfig(mCtrlMusicVol, mConfig);
updateCtrlConfig(mAiController, mConfig);
}
private <TSetting, TConf> void updateCtrlConfig(
final LauncherSettingController<TSetting, TConf> ctrl,
final TConf config)
{
if (ctrl != null)
{
ctrl.updateConfig(config);
}
}
private void onInitFailure(final AsyncLauncherInitialization.InitResult initResult)
{
Log.d(this, "Init failed with " + initResult);
mProgress.setVisibility(View.GONE);
mCtrlStart.hide();
for (LauncherSettingController<?, ?> setting: mActualSettings)
{
setting.hide();
}
mErrorMessage.setVisibility(View.VISIBLE);
mErrorMessage.setText(initResult.mMessage);
}
}

@ -1,351 +0,0 @@
package eu.vcmi.vcmi;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.common.GooglePlayServicesRepairableException;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.security.ProviderInstaller;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.content.ModBaseViewHolder;
import eu.vcmi.vcmi.content.ModsAdapter;
import eu.vcmi.vcmi.mods.VCMIMod;
import eu.vcmi.vcmi.mods.VCMIModContainer;
import eu.vcmi.vcmi.mods.VCMIModsRepo;
import eu.vcmi.vcmi.util.InstallModAsync;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.ServerResponse;
/**
* @author F
*/
public class ActivityMods extends ActivityWithToolbar
{
private static final boolean ENABLE_REPO_DOWNLOADING = true;
private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.5.json";
private VCMIModsRepo mRepo;
private RecyclerView mRecycler;
private VCMIModContainer mModContainer;
private TextView mErrorMessage;
private View mProgress;
private ModsAdapter mModsAdapter;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mods);
initToolbar(R.string.mods_title);
mRepo = new VCMIModsRepo();
mProgress = findViewById(R.id.mods_progress);
mErrorMessage = (TextView) findViewById(R.id.mods_error_text);
mErrorMessage.setVisibility(View.GONE);
mRecycler = (RecyclerView) findViewById(R.id.mods_recycler);
mRecycler.setItemAnimator(new DefaultItemAnimator());
mRecycler.setLayoutManager(new LinearLayoutManager(this));
mRecycler.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
mRecycler.setVisibility(View.GONE);
mModsAdapter = new ModsAdapter(new OnAdapterItemAction());
mRecycler.setAdapter(mModsAdapter);
new AsyncLoadLocalMods().execute((Void) null);
try {
ProviderInstaller.installIfNeeded(this);
} catch (GooglePlayServicesRepairableException e) {
GooglePlayServicesUtil.getErrorDialog(e.getConnectionStatusCode(), this, 0);
} catch (GooglePlayServicesNotAvailableException e) {
Log.e("SecurityException", "Google Play Services not available.");
}
}
private void loadLocalModData() throws IOException, JSONException
{
final File dataRoot = Storage.getVcmiDataDir(this);
final String internalDataRoot = getFilesDir() + "/" + Const.VCMI_DATA_ROOT_FOLDER_NAME;
final File modsRoot = new File(dataRoot,"/Mods");
final File internalModsRoot = new File(internalDataRoot + "/Mods");
if (!modsRoot.exists() && !internalModsRoot.exists())
{
Log.w(this, "We don't have mods folders");
return;
}
final File[] modsFiles = modsRoot.listFiles();
final File[] internalModsFiles = internalModsRoot.listFiles();
final List<File> topLevelModsFolders = new ArrayList<>();
if (modsFiles != null && modsFiles.length > 0)
{
Collections.addAll(topLevelModsFolders, modsFiles);
}
if (internalModsFiles != null && internalModsFiles.length > 0)
{
Collections.addAll(topLevelModsFolders, internalModsFiles);
}
mModContainer = VCMIModContainer.createContainer(topLevelModsFolders);
final File modConfigFile = new File(dataRoot, "config/modSettings.json");
if (!modConfigFile.exists())
{
Log.w(this, "We don't have mods config");
return;
}
JSONObject rootConfigObj = new JSONObject(FileUtil.read(modConfigFile));
JSONObject activeMods = rootConfigObj.getJSONObject("activeMods");
mModContainer.updateContainerFromConfigJson(activeMods, rootConfigObj.optJSONObject("core"));
Log.i(this, "Loaded mods: " + mModContainer);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu)
{
final MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.menu_mods, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
if (item.getItemId() == R.id.menu_mods_download_repo)
{
Log.i(this, "Should download repo now...");
if (ENABLE_REPO_DOWNLOADING)
{
mProgress.setVisibility(View.VISIBLE);
mRepo.init(REPO_URL, new OnModsRepoInitialized()); // disabled because the json is broken anyway
}
else
{
Snackbar.make(findViewById(R.id.mods_data_root), "Loading repo is disabled for now, because .json can't be parsed anyway",
Snackbar.LENGTH_LONG).show();
}
}
return super.onOptionsItemSelected(item);
}
private void handleNoData()
{
mProgress.setVisibility(View.GONE);
mRecycler.setVisibility(View.GONE);
mErrorMessage.setVisibility(View.VISIBLE);
mErrorMessage.setText("Could not load local mods list");
}
private void saveModSettingsToFile()
{
mModContainer.saveToFile(
new File(
Storage.getVcmiDataDir(this),
"config/modSettings.json"));
}
private class OnModsRepoInitialized implements VCMIModsRepo.IOnModsRepoDownloaded
{
@Override
public void onSuccess(ServerResponse<List<VCMIMod>> response)
{
Log.i(this, "Initialized mods repo");
if (mModContainer == null)
{
handleNoData();
}
else
{
mModContainer.updateFromRepo(response.mContent);
mModsAdapter.updateModsList(mModContainer.submods());
mProgress.setVisibility(View.GONE);
}
}
@Override
public void onError(final int code)
{
Log.i(this, "Mods repo error: " + code);
}
}
private class AsyncLoadLocalMods extends AsyncTask<Void, Void, Void>
{
@Override
protected void onPreExecute()
{
mProgress.setVisibility(View.VISIBLE);
}
@Override
protected Void doInBackground(final Void... params)
{
try
{
loadLocalModData();
}
catch (IOException e)
{
Log.e(this, "Loading local mod data failed", e);
}
catch (JSONException e)
{
Log.e(this, "Parsing local mod data failed", e);
}
return null;
}
@Override
protected void onPostExecute(final Void aVoid)
{
if (mModContainer == null || !mModContainer.hasSubmods())
{
handleNoData();
}
else
{
mProgress.setVisibility(View.GONE);
mRecycler.setVisibility(View.VISIBLE);
mModsAdapter.updateModsList(mModContainer.submods());
}
}
}
private class OnAdapterItemAction implements ModsAdapter.IOnItemAction
{
@Override
public void onItemPressed(final ModsAdapter.ModItem mod, final RecyclerView.ViewHolder vh)
{
Log.i(this, "Mod pressed: " + mod);
if (mod.mMod.hasSubmods())
{
if (mod.mExpanded)
{
mModsAdapter.detachSubmods(mod, vh);
}
else
{
mModsAdapter.attachSubmods(mod, vh);
mRecycler.scrollToPosition(vh.getAdapterPosition() + 1);
}
mod.mExpanded = !mod.mExpanded;
}
}
@Override
public void onDownloadPressed(final ModsAdapter.ModItem mod, final RecyclerView.ViewHolder vh)
{
Log.i(this, "Mod download pressed: " + mod);
mModsAdapter.downloadProgress(mod, "0%");
installModAsync(mod);
}
@Override
public void onTogglePressed(final ModsAdapter.ModItem item, final ModBaseViewHolder holder)
{
if(!item.mMod.mSystem && item.mMod.mInstalled)
{
item.mMod.mActive = !item.mMod.mActive;
mModsAdapter.notifyItemChanged(holder.getAdapterPosition());
saveModSettingsToFile();
}
}
@Override
public void onUninstall(ModsAdapter.ModItem item, ModBaseViewHolder holder)
{
File installationFolder = item.mMod.installationFolder;
ActivityMods activity = ActivityMods.this;
if(installationFolder != null){
new AlertDialog.Builder(activity)
.setTitle(activity.getString(R.string.mods_removal_title, item.mMod.mName))
.setMessage(activity.getString(R.string.mods_removal_confirmation, item.mMod.mName))
.setIcon(android.R.drawable.ic_dialog_alert)
.setNegativeButton(android.R.string.no, null)
.setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
{
FileUtil.clearDirectory(installationFolder);
installationFolder.delete();
mModsAdapter.modRemoved(item);
})
.show();
}
}
}
private void installModAsync(ModsAdapter.ModItem mod){
File dataDir = Storage.getVcmiDataDir(this);
File modFolder = new File(
new File(dataDir, "Mods"),
mod.mMod.mId.toLowerCase(Locale.US));
InstallModAsync modInstaller = new InstallModAsync(
modFolder,
this,
new InstallModCallback(mod)
);
modInstaller.execute(mod.mMod.mArchiveUrl);
}
public class InstallModCallback implements InstallModAsync.PostDownload
{
private ModsAdapter.ModItem mod;
public InstallModCallback(ModsAdapter.ModItem mod)
{
this.mod = mod;
}
@Override
public void downloadDone(Boolean succeed, File modFolder)
{
if(succeed){
mModsAdapter.modInstalled(mod, modFolder);
}
}
@Override
public void downloadProgress(String... progress)
{
if(progress.length > 0)
{
mModsAdapter.downloadProgress(mod, progress[0]);
}
}
}
}

@ -1,53 +0,0 @@
package eu.vcmi.vcmi;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
import android.view.ViewStub;
/**
* @author F
*/
public abstract class ActivityWithToolbar extends ActivityBase
{
@Override
public void setContentView(final int layoutResId)
{
super.setContentView(R.layout.activity_toolbar_wrapper);
final ViewStub contentStub = (ViewStub) findViewById(R.id.toolbar_wrapper_content_stub);
contentStub.setLayoutResource(layoutResId);
contentStub.inflate();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item)
{
if (item.getItemId() == android.R.id.home)
{
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
protected void initToolbar(final int textResId)
{
initToolbar(textResId, false);
}
protected void initToolbar(final int textResId, final boolean isTopLevelActivity)
{
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbar.setTitle(textResId);
if (!isTopLevelActivity)
{
final ActionBar bar = getSupportActionBar();
if (bar != null)
{
bar.setDisplayHomeAsUpEnabled(true);
}
}
}
}

@ -1,217 +0,0 @@
package eu.vcmi.vcmi;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class Config
{
public static final String DEFAULT_LANGUAGE = "english";
public static final int DEFAULT_MUSIC_VALUE = 5;
public static final int DEFAULT_SOUND_VALUE = 5;
public String mLanguage;
public int mScreenScale;
public int mVolumeSound;
public int mVolumeMusic;
private String adventureAi;
private double mPointerSpeedMultiplier;
private boolean mUseRelativePointer;
private JSONObject mRawObject;
private boolean mIsModified;
private static JSONObject accessNode(final JSONObject baseObj, String type)
{
if (baseObj == null)
{
return null;
}
return baseObj.optJSONObject(type);
}
private static JSONObject accessResolutionNode(final JSONObject baseObj)
{
if (baseObj == null)
{
return null;
}
final JSONObject video = baseObj.optJSONObject("video");
if (video != null)
{
return video.optJSONObject("resolution");
}
return null;
}
private static double loadDouble(final JSONObject node, final String key, final double fallback)
{
if (node == null)
{
return fallback;
}
return node.optDouble(key, fallback);
}
@SuppressWarnings("unchecked")
private static <T> T loadEntry(final JSONObject node, final String key, final T fallback)
{
if (node == null)
{
return fallback;
}
final Object value = node.opt(key);
return value == null ? fallback : (T) value;
}
public static Config load(final JSONObject obj)
{
Log.v("loading config from json: " + obj.toString());
final Config config = new Config();
final JSONObject general = accessNode(obj, "general");
final JSONObject server = accessNode(obj, "server");
final JSONObject resolution = accessResolutionNode(obj);
config.mLanguage = loadEntry(general, "language", DEFAULT_LANGUAGE);
config.mScreenScale = loadEntry(resolution, "scaling", -1);
config.mVolumeSound = loadEntry(general, "sound", DEFAULT_SOUND_VALUE);
config.mVolumeMusic = loadEntry(general, "music", DEFAULT_MUSIC_VALUE);
config.adventureAi = loadEntry(server, "playerAI", "Nullkiller");
config.mUseRelativePointer = loadEntry(general, "userRelativePointer", false);
config.mPointerSpeedMultiplier = loadDouble(general, "relativePointerSpeedMultiplier", 1.0);
config.mRawObject = obj;
return config;
}
public void updateLanguage(final String s)
{
mLanguage = s;
mIsModified = true;
}
public void updateScreenScale(final int scale)
{
mScreenScale = scale;
mIsModified = true;
}
public void updateSound(final int i)
{
mVolumeSound = i;
mIsModified = true;
}
public void updateMusic(final int i)
{
mVolumeMusic = i;
mIsModified = true;
}
public void setAdventureAi(String ai)
{
adventureAi = ai;
mIsModified = true;
}
public String getAdventureAi()
{
return this.adventureAi == null ? "Nullkiller" : this.adventureAi;
}
public void setPointerSpeedMultiplier(float speedMultiplier)
{
mPointerSpeedMultiplier = speedMultiplier;
mIsModified = true;
}
public float getPointerSpeedMultiplier()
{
return (float)mPointerSpeedMultiplier;
}
public void setPointerMode(boolean isRelative)
{
mUseRelativePointer = isRelative;
mIsModified = true;
}
public boolean getPointerModeIsRelative()
{
return mUseRelativePointer;
}
public void save(final File location) throws IOException, JSONException
{
if (!needsSaving(location))
{
Log.d(this, "Config doesn't need saving");
return;
}
try
{
final String configString = toJson();
FileUtil.write(location, configString);
Log.v(this, "Saved config: " + configString);
}
catch (final Exception e)
{
Log.e(this, "Could not save config", e);
throw e;
}
}
private boolean needsSaving(final File location)
{
return mIsModified || !location.exists();
}
private String toJson() throws JSONException
{
final JSONObject generalNode = accessNode(mRawObject, "general");
final JSONObject serverNode = accessNode(mRawObject, "server");
final JSONObject resolutionNode = accessResolutionNode(mRawObject);
final JSONObject root = mRawObject == null ? new JSONObject() : mRawObject;
final JSONObject general = generalNode == null ? new JSONObject() : generalNode;
final JSONObject video = new JSONObject();
final JSONObject resolution = resolutionNode == null ? new JSONObject() : resolutionNode;
final JSONObject server = serverNode == null ? new JSONObject() : serverNode;
if (mLanguage != null)
{
general.put("language", mLanguage);
}
general.put("music", mVolumeMusic);
general.put("sound", mVolumeSound);
general.put("userRelativePointer", mUseRelativePointer);
general.put("relativePointerSpeedMultiplier", mPointerSpeedMultiplier);
root.put("general", general);
if(this.adventureAi != null)
{
server.put("playerAI", this.adventureAi);
root.put("server", server);
}
if (mScreenScale > 0)
{
resolution.put("scaling", mScreenScale);
video.put("resolution", resolution);
root.put("video", video);
}
return root.toString();
}
}

@ -1,15 +1,6 @@
package eu.vcmi.vcmi;
import android.content.Context;
import android.os.Build;
import android.os.Environment;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
/**
* @author F
@ -17,8 +8,6 @@ import java.io.OutputStreamWriter;
public class Const
{
public static final String JNI_METHOD_SUPPRESS = "unused"; // jni methods are marked as unused, because IDE doesn't understand jni calls
// used to disable lint errors about try-with-resources being unsupported on api <19 (it is supported, because retrolambda backports it)
public static final int SUPPRESS_TRY_WITH_RESOURCES_WARNING = Build.VERSION_CODES.KITKAT;
public static final String VCMI_DATA_ROOT_FOLDER_NAME = "vcmi-data";
}

@ -1,14 +1,8 @@
package eu.vcmi.vcmi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.VibrationEffect;
import android.os.Vibrator;
@ -17,9 +11,6 @@ import org.libsdl.app.SDLActivity;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.Locale;
import java.text.SimpleDateFormat;
import eu.vcmi.vcmi.util.Log;
@ -35,7 +26,7 @@ public class NativeMethods
}
public static native void initClassloader();
public static native void heroesDataUpdate();
public static native boolean tryToSaveTheGame();
public static void setupMsg(final Messenger msg)

@ -1,33 +1,14 @@
package eu.vcmi.vcmi;
import android.content.Context;
import java.io.File;
import java.io.IOException;
import eu.vcmi.vcmi.util.FileUtil;
public class Storage
{
public static File getVcmiDataDir(Context context)
{
File root = context.getExternalFilesDir(null);
return new File(root, Const.VCMI_DATA_ROOT_FOLDER_NAME);
}
public static boolean testH3DataFolder(Context context)
{
return testH3DataFolder(getVcmiDataDir(context));
}
public static boolean testH3DataFolder(final File baseDir)
{
final File testH3Data = new File(baseDir, "Data");
final File testH3data = new File(baseDir, "data");
final File testH3DATA = new File(baseDir, "DATA");
return testH3Data.exists() || testH3data.exists() || testH3DATA.exists();
}
public static String getH3DataFolder(Context context){
return getVcmiDataDir(context).getAbsolutePath();
}
}

@ -84,9 +84,7 @@ public class VcmiSDLActivity extends SDLActivity
@Override
protected String getMainSharedObject() {
String library = "libvcmiclient.so";
return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
return String.format("%s/lib%s.so", getContext().getApplicationInfo().nativeLibraryDir, LibsLoader.CLIENT_LIB);
}
@Override
@ -100,9 +98,6 @@ public class VcmiSDLActivity extends SDLActivity
{
super.onCreate(savedInstanceState);
if(mBrokenLibraries)
return;
final View outerLayout = getLayoutInflater().inflate(R.layout.activity_game, null, false);
final ViewGroup layout = (ViewGroup) outerLayout.findViewById(R.id.game_outer_frame);
mProgressBar = outerLayout.findViewById(R.id.game_progress);
@ -182,4 +177,4 @@ public class VcmiSDLActivity extends SDLActivity
mCallback = callback;
}
}
}
}

@ -1,171 +0,0 @@
package eu.vcmi.vcmi.content;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import androidx.core.app.ActivityCompat;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.Const;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.SharedPrefs;
/**
* @author F
*/
public class AsyncLauncherInitialization extends AsyncTask<Void, Void, AsyncLauncherInitialization.InitResult>
{
private final WeakReference<ILauncherCallbacks> mCallbackRef;
public AsyncLauncherInitialization(final ILauncherCallbacks callback)
{
mCallbackRef = new WeakReference<>(callback);
}
private InitResult init()
{
InitResult initResult = handleDataFoldersInitialization();
if (!initResult.mSuccess)
{
return initResult;
}
Log.d(this, "Folders check passed");
return initResult;
}
private InitResult handleDataFoldersInitialization()
{
final ILauncherCallbacks callbacks = mCallbackRef.get();
if (callbacks == null)
{
return InitResult.failure("Internal error");
}
final Context ctx = callbacks.ctx();
final File vcmiDir = Storage.getVcmiDataDir(ctx);
final File internalDir = ctx.getFilesDir();
final File vcmiInternalDir = new File(internalDir, Const.VCMI_DATA_ROOT_FOLDER_NAME);
Log.i(this, "Using " + vcmiDir.getAbsolutePath() + " as root vcmi dir");
if(!vcmiInternalDir.exists()) vcmiInternalDir.mkdir();
if(!vcmiDir.exists()) vcmiDir.mkdir();
if (!Storage.testH3DataFolder(ctx))
{
// no h3 data present -> instruct user where to put it
return InitResult.failure(
ctx.getString(
R.string.launcher_error_h3_data_missing,
Storage.getVcmiDataDir(ctx)));
}
final File testVcmiData = new File(vcmiInternalDir, "Mods/vcmi/mod.json");
final boolean internalVcmiDataExisted = testVcmiData.exists();
if (!internalVcmiDataExisted && !FileUtil.unpackVcmiDataToInternalDir(vcmiInternalDir, ctx.getAssets()))
{
// no h3 data present -> instruct user where to put it
return InitResult.failure(ctx.getString(R.string.launcher_error_vcmi_data_internal_missing));
}
final String previousInternalDataHash = callbacks.prefs().load(SharedPrefs.KEY_CURRENT_INTERNAL_ASSET_HASH, null);
final String currentInternalDataHash = FileUtil.readAssetsStream(ctx.getAssets(), "internalDataHash.txt");
if (currentInternalDataHash == null || previousInternalDataHash == null || !currentInternalDataHash.equals(previousInternalDataHash))
{
// we should update the data only if it existed previously (hash is bound to be empty if we have just created the data)
if (internalVcmiDataExisted)
{
Log.i(this, "Internal data needs to be created/updated; old hash=" + previousInternalDataHash
+ ", new hash=" + currentInternalDataHash);
if (!FileUtil.reloadVcmiDataToInternalDir(vcmiInternalDir, ctx.getAssets()))
{
return InitResult.failure(ctx.getString(R.string.launcher_error_vcmi_data_internal_update));
}
}
callbacks.prefs().save(SharedPrefs.KEY_CURRENT_INTERNAL_ASSET_HASH, currentInternalDataHash);
}
return InitResult.success();
}
@Override
protected InitResult doInBackground(final Void... params)
{
return init();
}
@Override
protected void onPostExecute(final InitResult initResult)
{
final ILauncherCallbacks callbacks = mCallbackRef.get();
if (callbacks == null)
{
return;
}
if (initResult.mSuccess)
{
callbacks.onInitSuccess();
}
else
{
callbacks.onInitFailure(initResult);
}
}
public interface ILauncherCallbacks
{
Activity ctx();
SharedPrefs prefs();
void onInitSuccess();
void onInitFailure(InitResult result);
}
public static final class InitResult
{
public final boolean mSuccess;
public final String mMessage;
public boolean mFailSilently;
public InitResult(final boolean success, final String message)
{
mSuccess = success;
mMessage = message;
}
@Override
public String toString()
{
return String.format("success: %s (%s)", mSuccess, mMessage);
}
public static InitResult failure(String message)
{
return new InitResult(false, message);
}
public static InitResult success()
{
return new InitResult(true, "");
}
}
}

@ -1,52 +0,0 @@
package eu.vcmi.vcmi.content;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import java.io.IOException;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class DialogAuthors extends DialogFragment
{
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState)
{
final LayoutInflater inflater = LayoutInflater.from(getActivity());
@SuppressLint("InflateParams") final View inflated = inflater.inflate(R.layout.dialog_authors, null, false);
final TextView vcmiAuthorsView = (TextView) inflated.findViewById(R.id.dialog_authors_vcmi);
final TextView launcherAuthorsView = (TextView) inflated.findViewById(R.id.dialog_authors_launcher);
loadAuthorsContent(vcmiAuthorsView, launcherAuthorsView);
return new AlertDialog.Builder(getActivity())
.setView(inflated)
.create();
}
private void loadAuthorsContent(final TextView vcmiAuthorsView, final TextView launcherAuthorsView)
{
try
{
// to be checked if this should be converted to async load (not really a file operation so it should be okay)
final String authorsContent = "See ingame credits";
vcmiAuthorsView.setText(authorsContent);
launcherAuthorsView.setText("Fay"); // TODO hardcoded for now
}
catch (final Exception e)
{
Log.e(this, "Could not load authors content", e);
}
}
}

@ -1,36 +0,0 @@
package eu.vcmi.vcmi.content;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ModBaseViewHolder extends RecyclerView.ViewHolder
{
final View mModNesting;
final TextView mModName;
ModBaseViewHolder(final View parentView)
{
this(
LayoutInflater.from(parentView.getContext()).inflate(
R.layout.mod_base_adapter_item,
(ViewGroup) parentView,
false),
true);
}
protected ModBaseViewHolder(final View v, final boolean internal)
{
super(v);
mModNesting = itemView.findViewById(R.id.mods_adapter_item_nesting);
mModName = (TextView) itemView.findViewById(R.id.mods_adapter_item_name);
}
}

@ -1,254 +0,0 @@
package eu.vcmi.vcmi.content;
import android.content.Context;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.mods.VCMIMod;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class ModsAdapter extends RecyclerView.Adapter<ModBaseViewHolder>
{
private static final int NESTING_WIDTH_PER_LEVEL = 16;
private static final int VIEWTYPE_MOD = 0;
private static final int VIEWTYPE_FAILED_MOD = 1;
private final List<ModItem> mDataset = new ArrayList<>();
private final IOnItemAction mItemListener;
public ModsAdapter(final IOnItemAction itemListener)
{
mItemListener = itemListener;
}
@Override
public ModBaseViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType)
{
switch (viewType)
{
case VIEWTYPE_MOD:
return new ModsViewHolder(parent);
case VIEWTYPE_FAILED_MOD:
return new ModBaseViewHolder(parent);
default:
Log.e(this, "Unhandled view type: " + viewType);
return null;
}
}
@Override
public void onBindViewHolder(final ModBaseViewHolder holder, final int position)
{
final ModItem item = mDataset.get(position);
final int viewType = getItemViewType(position);
final Context ctx = holder.itemView.getContext();
holder.mModNesting.getLayoutParams().width = item.mNestingLevel * NESTING_WIDTH_PER_LEVEL;
switch (viewType)
{
case VIEWTYPE_MOD:
final ModsViewHolder modHolder = (ModsViewHolder) holder;
modHolder.mModName.setText(item.mMod.mName + ", " + item.mMod.mVersion);
modHolder.mModType.setText(item.mMod.mModType);
if (item.mMod.mSize > 0)
{
modHolder.mModSize.setVisibility(View.VISIBLE);
// TODO unit conversion
modHolder.mModSize.setText(String.format(Locale.getDefault(), "%.1f kB", item.mMod.mSize / 1024.0f));
}
else
{
modHolder.mModSize.setVisibility(View.GONE);
}
modHolder.mModAuthor.setText(ctx.getString(R.string.mods_item_author_template, item.mMod.mAuthor));
modHolder.mStatusIcon.setImageResource(selectModStatusIcon(item.mMod.mActive));
modHolder.mDownloadBtn.setVisibility(View.GONE);
modHolder.mDownloadProgress.setVisibility(View.GONE);
modHolder.mUninstall.setVisibility(View.GONE);
if(!item.mMod.mSystem)
{
if (item.mDownloadProgress != null)
{
modHolder.mDownloadProgress.setText(item.mDownloadProgress);
modHolder.mDownloadProgress.setVisibility(View.VISIBLE);
}
else if (!item.mMod.mInstalled)
{
modHolder.mDownloadBtn.setVisibility(View.VISIBLE);
}
else if (item.mMod.installationFolder != null)
{
modHolder.mUninstall.setVisibility(View.VISIBLE);
}
modHolder.itemView.setOnClickListener(v -> mItemListener.onItemPressed(item, holder));
modHolder.mStatusIcon.setOnClickListener(v -> mItemListener.onTogglePressed(item, holder));
modHolder.mDownloadBtn.setOnClickListener(v -> mItemListener.onDownloadPressed(item, holder));
modHolder.mUninstall.setOnClickListener(v -> mItemListener.onUninstall(item, holder));
}
break;
case VIEWTYPE_FAILED_MOD:
holder.mModName.setText(ctx.getString(R.string.mods_failed_mod_loading, item.mMod.mName));
break;
default:
Log.e(this, "Unhandled view type: " + viewType);
break;
}
}
private int selectModStatusIcon(final boolean active)
{
// TODO distinguishing mods that aren't downloaded or have an update available
if (active)
{
return R.drawable.ic_star_full;
}
return R.drawable.ic_star_empty;
}
@Override
public int getItemViewType(final int position)
{
return mDataset.get(position).mMod.mLoadedCorrectly ? VIEWTYPE_MOD : VIEWTYPE_FAILED_MOD;
}
@Override
public int getItemCount()
{
return mDataset.size();
}
public void attachSubmods(final ModItem mod, final RecyclerView.ViewHolder vh)
{
int adapterPosition = vh.getAdapterPosition();
final List<ModItem> submods = new ArrayList<>();
for (VCMIMod v : mod.mMod.submods())
{
ModItem modItem = new ModItem(v, mod.mNestingLevel + 1);
submods.add(modItem);
}
mDataset.addAll(adapterPosition + 1, submods);
notifyItemRangeInserted(adapterPosition + 1, submods.size());
}
public void detachSubmods(final ModItem mod, final RecyclerView.ViewHolder vh)
{
final int adapterPosition = vh.getAdapterPosition();
final int checkedPosition = adapterPosition + 1;
int detachedElements = 0;
while (checkedPosition < mDataset.size() && mDataset.get(checkedPosition).mNestingLevel > mod.mNestingLevel)
{
++detachedElements;
mDataset.remove(checkedPosition);
}
notifyItemRangeRemoved(checkedPosition, detachedElements);
}
public void updateModsList(List<VCMIMod> mods)
{
mDataset.clear();
List<ModItem> list = new ArrayList<>();
for (VCMIMod mod : mods)
{
ModItem modItem = new ModItem(mod);
list.add(modItem);
}
mDataset.addAll(list);
notifyDataSetChanged();
}
public void modInstalled(ModItem mod, File modFolder)
{
try
{
mod.mMod.updateFromModInfo(modFolder);
mod.mMod.mLoadedCorrectly = true;
mod.mMod.mActive = true; // active by default
mod.mMod.mInstalled = true;
mod.mMod.installationFolder = modFolder;
mod.mDownloadProgress = null;
notifyItemChanged(mDataset.indexOf(mod));
}
catch (Exception ex)
{
Log.e("Failed to install mod", ex);
}
}
public void downloadProgress(ModItem mod, String progress)
{
mod.mDownloadProgress = progress;
notifyItemChanged(mDataset.indexOf(mod));
}
public void modRemoved(ModItem item)
{
int itemIndex = mDataset.indexOf(item);
if(item.mMod.mArchiveUrl != null && item.mMod.mArchiveUrl != "")
{
item.mMod.mInstalled = false;
item.mMod.installationFolder = null;
notifyItemChanged(itemIndex);
}
else
{
mDataset.remove(item);
notifyItemRemoved(itemIndex);
}
}
public interface IOnItemAction
{
void onItemPressed(final ModItem mod, final RecyclerView.ViewHolder vh);
void onDownloadPressed(final ModItem mod, final RecyclerView.ViewHolder vh);
void onTogglePressed(ModItem item, ModBaseViewHolder holder);
void onUninstall(ModItem item, ModBaseViewHolder holder);
}
public static class ModItem
{
public final VCMIMod mMod;
public int mNestingLevel;
public boolean mExpanded;
public String mDownloadProgress;
public ModItem(final VCMIMod mod)
{
this(mod, 0);
}
public ModItem(final VCMIMod mod, final int nestingLevel)
{
mMod = mod;
mNestingLevel = nestingLevel;
mExpanded = false;
}
@Override
public String toString()
{
return mMod.toString();
}
}
}

@ -1,35 +0,0 @@
package eu.vcmi.vcmi.content;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ModsViewHolder extends ModBaseViewHolder
{
final TextView mModAuthor;
final TextView mModType;
final TextView mModSize;
final ImageView mStatusIcon;
final View mDownloadBtn;
final TextView mDownloadProgress;
final View mUninstall;
ModsViewHolder(final View parentView)
{
super(LayoutInflater.from(parentView.getContext()).inflate(R.layout.mods_adapter_item, (ViewGroup) parentView, false), true);
mModAuthor = (TextView) itemView.findViewById(R.id.mods_adapter_item_author);
mModType = (TextView) itemView.findViewById(R.id.mods_adapter_item_modtype);
mModSize = (TextView) itemView.findViewById(R.id.mods_adapter_item_size);
mDownloadBtn = itemView.findViewById(R.id.mods_adapter_item_btn_download);
mStatusIcon = (ImageView) itemView.findViewById(R.id.mods_adapter_item_status);
mDownloadProgress = (TextView) itemView.findViewById(R.id.mods_adapter_item_install_progress);
mUninstall = itemView.findViewById(R.id.mods_adapter_item_btn_uninstall);
}
}

@ -1,258 +0,0 @@
package eu.vcmi.vcmi.mods;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import eu.vcmi.vcmi.BuildConfig;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class VCMIMod
{
protected final Map<String, VCMIMod> mSubmods;
public String mId;
public String mName;
public String mDesc;
public String mVersion;
public String mAuthor;
public String mContact;
public String mModType;
public String mArchiveUrl;
public long mSize;
public File installationFolder;
// config values
public boolean mActive;
public boolean mInstalled;
public boolean mValidated;
public String mChecksum;
// internal
public boolean mLoadedCorrectly;
public boolean mSystem;
protected VCMIMod()
{
mSubmods = new HashMap<>();
}
public static VCMIMod buildFromRepoJson(final String id,
final JSONObject obj,
JSONObject modDownloadData)
{
final VCMIMod mod = new VCMIMod();
mod.mId = id.toLowerCase(Locale.US);
mod.mName = obj.optString("name");
mod.mDesc = obj.optString("description");
mod.mVersion = obj.optString("version");
mod.mAuthor = obj.optString("author");
mod.mContact = obj.optString("contact");
mod.mModType = obj.optString("modType");
mod.mArchiveUrl = modDownloadData.optString("download");
mod.mSize = obj.optLong("size");
mod.mLoadedCorrectly = true;
return mod;
}
public static VCMIMod buildFromConfigJson(final String id, final JSONObject obj) throws JSONException
{
final VCMIMod mod = new VCMIMod();
mod.updateFromConfigJson(id, obj);
mod.mInstalled = true;
return mod;
}
public static VCMIMod buildFromModInfo(final File modPath) throws IOException, JSONException
{
final VCMIMod mod = new VCMIMod();
if (!mod.updateFromModInfo(modPath))
{
return mod;
}
mod.mLoadedCorrectly = true;
mod.mActive = true; // active by default
mod.mInstalled = true;
mod.installationFolder = modPath;
return mod;
}
protected static Map<String, VCMIMod> loadSubmods(final List<File> modsList) throws IOException, JSONException
{
final Map<String, VCMIMod> submods = new HashMap<>();
for (final File f : modsList)
{
if (!f.isDirectory())
{
Log.w("VCMI", "Non-directory encountered in mods dir: " + f.getName());
continue;
}
final VCMIMod submod = buildFromModInfo(f);
if (submod == null)
{
Log.w(null, "Could not build mod in folder " + f + "; ignoring");
continue;
}
submods.put(submod.mId, submod);
}
return submods;
}
public void updateFromConfigJson(final String id, final JSONObject obj) throws JSONException
{
if(mSystem)
{
return;
}
mId = id.toLowerCase(Locale.US);
mActive = obj.optBoolean("active");
mValidated = obj.optBoolean("validated");
mChecksum = obj.optString("checksum");
final JSONObject submods = obj.optJSONObject("mods");
if (submods != null)
{
updateChildrenFromConfigJson(submods);
}
}
protected void updateChildrenFromConfigJson(final JSONObject submods) throws JSONException
{
final JSONArray names = submods.names();
for (int i = 0; i < names.length(); ++i)
{
final String modId = names.getString(i);
final String normalizedModId = modId.toLowerCase(Locale.US);
if (!mSubmods.containsKey(normalizedModId))
{
Log.w(this, "Mod present in config but not found in /Mods; ignoring: " + modId);
continue;
}
mSubmods.get(normalizedModId).updateFromConfigJson(modId, submods.getJSONObject(modId));
}
}
public boolean updateFromModInfo(final File modPath) throws IOException, JSONException
{
final File modInfoFile = new File(modPath, "mod.json");
if (!modInfoFile.exists())
{
Log.w(this, "Mod info doesn't exist");
mName = modPath.getAbsolutePath();
return false;
}
try
{
final JSONObject modInfoContent = new JSONObject(FileUtil.read(modInfoFile));
mId = modPath.getName().toLowerCase(Locale.US);
mName = modInfoContent.optString("name");
mDesc = modInfoContent.optString("description");
mVersion = modInfoContent.optString("version");
mAuthor = modInfoContent.optString("author");
mContact = modInfoContent.optString("contact");
mModType = modInfoContent.optString("modType");
mSystem = mId.equals("vcmi");
final File submodsDir = new File(modPath, "Mods");
if (submodsDir.exists())
{
final List<File> submodsFiles = new ArrayList<>();
Collections.addAll(submodsFiles, submodsDir.listFiles());
mSubmods.putAll(loadSubmods(submodsFiles));
}
return true;
}
catch (final JSONException ex)
{
mName = modPath.getAbsolutePath();
return false;
}
}
@Override
public String toString()
{
if (!BuildConfig.DEBUG)
{
return "";
}
return String.format("mod:[id:%s,active:%s,submods:[%s]]", mId, mActive, TextUtils.join(",", mSubmods.values()));
}
protected void submodsToJson(final JSONObject modsRoot) throws JSONException
{
for (final VCMIMod submod : mSubmods.values())
{
final JSONObject submodEntry = new JSONObject();
submod.toJsonInternal(submodEntry);
modsRoot.put(submod.mId, submodEntry);
}
}
protected void toJsonInternal(final JSONObject root) throws JSONException
{
root.put("active", mActive);
root.put("validated", mValidated);
if (!TextUtils.isEmpty(mChecksum))
{
root.put("checksum", mChecksum);
}
if (!mSubmods.isEmpty())
{
JSONObject submods = new JSONObject();
submodsToJson(submods);
root.put("mods", submods);
}
}
public boolean hasSubmods()
{
return !mSubmods.isEmpty();
}
public List<VCMIMod> submods()
{
final ArrayList<VCMIMod> ret = new ArrayList<>();
ret.addAll(mSubmods.values());
Collections.sort(ret, new Comparator<VCMIMod>()
{
@Override
public int compare(VCMIMod left, VCMIMod right)
{
return left.mName.compareTo(right.mName);
}
});
return ret;
}
protected void updateFrom(VCMIMod other)
{
this.mModType = other.mModType;
this.mAuthor = other.mAuthor;
this.mDesc = other.mDesc;
this.mArchiveUrl = other.mArchiveUrl;
}
}

@ -1,106 +0,0 @@
package eu.vcmi.vcmi.mods;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.BuildConfig;
import eu.vcmi.vcmi.util.FileUtil;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public class VCMIModContainer extends VCMIMod
{
private VCMIMod mCoreStatus; // kept here to correctly save core object to modSettings
private VCMIModContainer()
{
}
public static VCMIModContainer createContainer(final List<File> modsList) throws IOException, JSONException
{
final VCMIModContainer mod = new VCMIModContainer();
mod.mSubmods.putAll(loadSubmods(modsList));
return mod;
}
public void updateContainerFromConfigJson(final JSONObject modsList, final JSONObject coreStatus) throws JSONException
{
updateChildrenFromConfigJson(modsList);
if (coreStatus != null)
{
mCoreStatus = VCMIMod.buildFromConfigJson("core", coreStatus);
}
}
public void updateFromRepo(List<VCMIMod> repoMods){
for (VCMIMod mod : repoMods)
{
final String normalizedModId = mod.mId.toLowerCase(Locale.US);
if(mSubmods.containsKey(normalizedModId)){
VCMIMod existing = mSubmods.get(normalizedModId);
existing.updateFrom(mod);
}
else{
mSubmods.put(normalizedModId, mod);
}
}
}
@Override
public String toString()
{
if (!BuildConfig.DEBUG)
{
return "";
}
return String.format("mods:[%s]", TextUtils.join(",", mSubmods.values()));
}
public void saveToFile(final File location)
{
try
{
FileUtil.write(location, toJson());
}
catch (Exception e)
{
Log.e(this, "Could not save mod settings", e);
}
}
protected String toJson() throws JSONException
{
final JSONObject root = new JSONObject();
final JSONObject activeMods = new JSONObject();
final JSONObject coreStatus = new JSONObject();
root.put("activeMods", activeMods);
submodsToJson(activeMods);
coreStatusToJson(coreStatus);
root.put("core", coreStatus);
return root.toString();
}
private void coreStatusToJson(final JSONObject coreStatus) throws JSONException
{
if (mCoreStatus == null)
{
mCoreStatus = new VCMIMod();
mCoreStatus.mId = "core";
mCoreStatus.mActive = true;
}
mCoreStatus.toJsonInternal(coreStatus);
}
}

@ -1,108 +0,0 @@
package eu.vcmi.vcmi.mods;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.util.AsyncRequest;
import eu.vcmi.vcmi.util.Log;
import eu.vcmi.vcmi.util.ServerResponse;
/**
* @author F
*/
public class VCMIModsRepo
{
private final List<VCMIMod> mModsList;
private IOnModsRepoDownloaded mCallback;
public VCMIModsRepo()
{
mModsList = new ArrayList<>();
}
public void init(final String url, final IOnModsRepoDownloaded callback)
{
mCallback = callback;
new AsyncLoadRepo().execute(url);
}
public interface IOnModsRepoDownloaded
{
void onSuccess(ServerResponse<List<VCMIMod>> response);
void onError(final int code);
}
private class AsyncLoadRepo extends AsyncRequest<List<VCMIMod>>
{
@Override
protected ServerResponse<List<VCMIMod>> doInBackground(final String... params)
{
ServerResponse<List<VCMIMod>> serverResponse = sendRequest(params[0]);
if (serverResponse.isValid())
{
final List<VCMIMod> mods = new ArrayList<>();
try
{
JSONObject jsonContent = new JSONObject(serverResponse.mRawContent);
final JSONArray names = jsonContent.names();
for (int i = 0; i < names.length(); ++i)
{
try
{
String name = names.getString(i);
JSONObject modDownloadData = jsonContent.getJSONObject(name);
if(modDownloadData.has("mod"))
{
String modFileAddress = modDownloadData.getString("mod");
ServerResponse<List<VCMIMod>> modFile = sendRequest(modFileAddress);
if (!modFile.isValid())
{
continue;
}
JSONObject modJson = new JSONObject(modFile.mRawContent);
mods.add(VCMIMod.buildFromRepoJson(name, modJson, modDownloadData));
}
else
{
mods.add(VCMIMod.buildFromRepoJson(name, modDownloadData, modDownloadData));
}
}
catch (JSONException e)
{
Log.e(this, "Could not parse the response as json", e);
}
}
serverResponse.mContent = mods;
}
catch (JSONException e)
{
Log.e(this, "Could not parse the response as json", e);
serverResponse.mCode = ServerResponse.LOCAL_ERROR_PARSING;
}
}
return serverResponse;
}
@Override
protected void onPostExecute(final ServerResponse<List<VCMIMod>> response)
{
if (response.isValid())
{
mModsList.clear();
mModsList.addAll(response.mContent);
mCallback.onSuccess(response);
}
else
{
mCallback.onError(response.mCode);
}
}
}
}

@ -1,46 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class AdventureAiController extends LauncherSettingWithDialogController<String, Config>
{
public AdventureAiController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<String> dialog()
{
return new AdventureAiSelectionDialog();
}
@Override
public void onItemChosen(final String item)
{
mConfig.setAdventureAi(item);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_adventure_ai_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mConfig.getAdventureAi();
}
}

@ -1,37 +0,0 @@
package eu.vcmi.vcmi.settings;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class AdventureAiSelectionDialog extends LauncherSettingDialog<String>
{
private static final List<String> AVAILABLE_AI = new ArrayList<>();
static
{
AVAILABLE_AI.add("VCAI");
AVAILABLE_AI.add("Nullkiller");
}
public AdventureAiSelectionDialog()
{
super(AVAILABLE_AI);
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_adventure_ai_title;
}
@Override
protected CharSequence itemName(final String item)
{
return item;
}
}

@ -1,193 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.view.View;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import androidx.loader.content.AsyncTaskLoader;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
public class CopyDataController extends LauncherSettingController<Void, Void>
{
public static final int PICK_EXTERNAL_VCMI_DATA_TO_COPY = 3;
private String progress;
public CopyDataController(final AppCompatActivity act)
{
super(act);
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_import_title);
}
@Override
protected String subText()
{
if (progress != null)
{
return progress;
}
return mActivity.getString(R.string.launcher_btn_import_description);
}
@Override
public void onClick(final View v)
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data")));
mActivity.startActivityForResult(intent, PICK_EXTERNAL_VCMI_DATA_TO_COPY);
}
public void copyData(Uri folderToCopy)
{
AsyncCopyData copyTask = new AsyncCopyData(mActivity, folderToCopy);
copyTask.execute();
}
private class AsyncCopyData extends AsyncTask<String, String, Boolean>
{
private Activity owner;
private Uri folderToCopy;
public AsyncCopyData(Activity owner, Uri folderToCopy)
{
this.owner = owner;
this.folderToCopy = folderToCopy;
}
@Override
protected Boolean doInBackground(final String... params)
{
File targetDir = Storage.getVcmiDataDir(owner);
DocumentFile sourceDir = DocumentFile.fromTreeUri(owner, folderToCopy);
ArrayList<String> allowedFolders = new ArrayList<String>();
allowedFolders.add("Data");
allowedFolders.add("Mp3");
allowedFolders.add("Maps");
allowedFolders.add("Saves");
allowedFolders.add("Mods");
allowedFolders.add("config");
return copyDirectory(targetDir, sourceDir, allowedFolders);
}
@Override
protected void onPostExecute(Boolean result)
{
super.onPostExecute(result);
if (result)
{
CopyDataController.this.progress = null;
CopyDataController.this.updateContent();
}
}
@Override
protected void onProgressUpdate(String... values)
{
CopyDataController.this.progress = values[0];
CopyDataController.this.updateContent();
}
private boolean copyDirectory(File targetDir, DocumentFile sourceDir, List<String> allowed)
{
if (!targetDir.exists())
{
targetDir.mkdir();
}
for (DocumentFile child : sourceDir.listFiles())
{
if (allowed != null)
{
boolean fileAllowed = false;
for (String str : allowed)
{
if (str.equalsIgnoreCase(child.getName()))
{
fileAllowed = true;
break;
}
}
if (!fileAllowed)
continue;
}
File exported = new File(targetDir, child.getName());
if (child.isFile())
{
publishProgress(owner.getString(R.string.launcher_progress_copy,
child.getName()));
if (!exported.exists())
{
try
{
exported.createNewFile();
}
catch (IOException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
try (
final OutputStream targetStream = new FileOutputStream(exported, false);
final InputStream sourceStream = owner.getContentResolver()
.openInputStream(child.getUri()))
{
FileUtil.copyStream(sourceStream, targetStream);
}
catch (IOException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
if (child.isDirectory() && !copyDirectory(exported, child, null))
{
return false;
}
}
return true;
}
}
}

@ -1,19 +0,0 @@
package eu.vcmi.vcmi.settings;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.util.SharedPrefs;
/**
* @author F
*/
public class DoubleConfig
{
public final Config mConfig;
public final SharedPrefs mPrefs;
public DoubleConfig(final Config config, final SharedPrefs prefs)
{
mConfig = config;
mPrefs = prefs;
}
}

@ -1,174 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.view.View;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import androidx.appcompat.app.AppCompatActivity;
import androidx.documentfile.provider.DocumentFile;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
public class ExportDataController extends LauncherSettingController<Void, Void>
{
public static final int PICK_DIRECTORY_TO_EXPORT = 4;
private String progress;
public ExportDataController(final AppCompatActivity act)
{
super(act);
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_export_title);
}
@Override
protected String subText()
{
if (progress != null)
{
return progress;
}
return mActivity.getString(R.string.launcher_btn_export_description);
}
@Override
public void onClick(final View v)
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra(
DocumentsContract.EXTRA_INITIAL_URI,
Uri.fromFile(new File(Environment.getExternalStorageDirectory(), "vcmi-data")));
mActivity.startActivityForResult(intent, PICK_DIRECTORY_TO_EXPORT);
}
public void copyData(Uri targetFolder)
{
AsyncCopyData copyTask = new AsyncCopyData(mActivity, targetFolder);
copyTask.execute();
}
private class AsyncCopyData extends AsyncTask<String, String, Boolean>
{
private Activity owner;
private Uri targetFolder;
public AsyncCopyData(Activity owner, Uri targetFolder)
{
this.owner = owner;
this.targetFolder = targetFolder;
}
@Override
protected Boolean doInBackground(final String... params)
{
File targetDir = Storage.getVcmiDataDir(owner);
DocumentFile sourceDir = DocumentFile.fromTreeUri(owner, targetFolder);
return copyDirectory(targetDir, sourceDir);
}
@Override
protected void onPostExecute(Boolean result)
{
super.onPostExecute(result);
if (result)
{
ExportDataController.this.progress = null;
ExportDataController.this.updateContent();
}
}
@Override
protected void onProgressUpdate(String... values)
{
ExportDataController.this.progress = values[0];
ExportDataController.this.updateContent();
}
private boolean copyDirectory(File sourceDir, DocumentFile targetDir)
{
for (File child : sourceDir.listFiles())
{
DocumentFile exported = targetDir.findFile(child.getName());
if (child.isFile())
{
publishProgress(owner.getString(R.string.launcher_progress_copy,
child.getName()));
if (exported == null)
{
try
{
exported = targetDir.createFile(
"application/octet-stream",
child.getName());
}
catch (UnsupportedOperationException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
if (exported == null)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
try(
final OutputStream targetStream = owner.getContentResolver()
.openOutputStream(exported.getUri());
final InputStream sourceStream = new FileInputStream(child))
{
FileUtil.copyStream(sourceStream, targetStream);
}
catch (IOException e)
{
publishProgress("Failed to copy file " + child.getName());
return false;
}
}
if (child.isDirectory())
{
if (exported == null)
{
exported = targetDir.createDirectory(child.getName());
}
if(!copyDirectory(child, exported))
{
return false;
}
}
}
return true;
}
}
}

@ -1,48 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class LanguageSettingController extends LauncherSettingWithDialogController<String, Config>
{
public LanguageSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<String> dialog()
{
return new LanguageSettingDialog();
}
@Override
public void onItemChosen(final String item)
{
mConfig.updateLanguage(item);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_language_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mConfig.mLanguage == null || mConfig.mLanguage.isEmpty()
? mActivity.getString(R.string.launcher_btn_language_subtitle_unknown)
: mActivity.getString(R.string.launcher_btn_language_subtitle, mConfig.mLanguage);
}
}

@ -1,55 +0,0 @@
package eu.vcmi.vcmi.settings;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class LanguageSettingDialog extends LauncherSettingDialog<String>
{
private static final List<String> AVAILABLE_LANGUAGES = new ArrayList<>();
static
{
AVAILABLE_LANGUAGES.add("english");
AVAILABLE_LANGUAGES.add("czech");
AVAILABLE_LANGUAGES.add("chinese");
AVAILABLE_LANGUAGES.add("finnish");
AVAILABLE_LANGUAGES.add("french");
AVAILABLE_LANGUAGES.add("german");
AVAILABLE_LANGUAGES.add("hungarian");
AVAILABLE_LANGUAGES.add("italian");
AVAILABLE_LANGUAGES.add("korean");
AVAILABLE_LANGUAGES.add("polish");
AVAILABLE_LANGUAGES.add("portuguese");
AVAILABLE_LANGUAGES.add("russian");
AVAILABLE_LANGUAGES.add("spanish");
AVAILABLE_LANGUAGES.add("swedish");
AVAILABLE_LANGUAGES.add("turkish");
AVAILABLE_LANGUAGES.add("ukrainian");
AVAILABLE_LANGUAGES.add("vietnamese");
AVAILABLE_LANGUAGES.add("other_cp1250");
AVAILABLE_LANGUAGES.add("other_cp1251");
AVAILABLE_LANGUAGES.add("other_cp1252");
}
public LanguageSettingDialog()
{
super(AVAILABLE_LANGUAGES);
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_language_title;
}
@Override
protected CharSequence itemName(final String item)
{
return item;
}
}

@ -1,75 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public abstract class LauncherSettingController<TSetting, TConf> implements View.OnClickListener
{
protected AppCompatActivity mActivity;
protected TConf mConfig;
private View mSettingViewRoot;
private TextView mSettingsTextMain;
private TextView mSettingsTextSub;
LauncherSettingController(final AppCompatActivity act)
{
mActivity = act;
}
public final LauncherSettingController<TSetting, TConf> init(final int rootViewResId)
{
return init(rootViewResId, null);
}
public final LauncherSettingController<TSetting, TConf> init(final int rootViewResId, final TConf config)
{
mSettingViewRoot = mActivity.findViewById(rootViewResId);
mSettingViewRoot.setOnClickListener(this);
mSettingsTextMain = (TextView) mSettingViewRoot.findViewById(R.id.inc_launcher_btn_main);
mSettingsTextSub = (TextView) mSettingViewRoot.findViewById(R.id.inc_launcher_btn_sub);
childrenInit(mSettingViewRoot);
updateConfig(config);
updateContent();
return this;
}
protected void childrenInit(final View root)
{
}
public void updateConfig(final TConf conf)
{
mConfig = conf;
updateContent();
}
public void updateContent()
{
mSettingsTextMain.setText(mainText());
if (mSettingsTextSub != null)
{
mSettingsTextSub.setText(subText());
}
}
protected abstract String mainText();
protected abstract String subText();
public void show()
{
mSettingViewRoot.setVisibility(View.VISIBLE);
}
public void hide()
{
mSettingViewRoot.setVisibility(View.GONE);
}
}

@ -1,73 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.appcompat.app.AlertDialog;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public abstract class LauncherSettingDialog<T> extends DialogFragment
{
protected final List<T> mDataset;
private IOnItemChosen<T> mObserver;
protected LauncherSettingDialog(final List<T> dataset)
{
mDataset = dataset;
}
public void observe(final IOnItemChosen<T> observer)
{
mObserver = observer;
}
protected abstract CharSequence itemName(T item);
protected abstract int dialogTitleResId();
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState)
{
List<CharSequence> list = new ArrayList<>();
for (T t : mDataset)
{
CharSequence charSequence = itemName(t);
list.add(charSequence);
}
return new AlertDialog.Builder(getActivity())
.setTitle(dialogTitleResId())
.setItems(
list.toArray(new CharSequence[0]),
this::onItemChosenInternal)
.create();
}
private void onItemChosenInternal(final DialogInterface dialog, final int index)
{
final T chosenItem = mDataset.get(index);
Log.d(this, "Chosen item: " + chosenItem);
dialog.dismiss();
if (mObserver != null)
{
mObserver.onItemChosen(chosenItem);
}
}
public interface IOnItemChosen<V>
{
void onItemChosen(V item);
}
}

@ -1,31 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import eu.vcmi.vcmi.util.Log;
/**
* @author F
*/
public abstract class LauncherSettingWithDialogController<T, Conf> extends LauncherSettingController<T, Conf>
implements LauncherSettingDialog.IOnItemChosen<T>
{
public static final String SETTING_DIALOG_ID = "settings.dialog";
protected LauncherSettingWithDialogController(final AppCompatActivity act)
{
super(act);
}
@Override
public void onClick(final View v)
{
Log.i(this, "Showing dialog");
final LauncherSettingDialog<T> dialog = dialog();
dialog.observe(this); // TODO rebinding dialogs on activity config changes
dialog.show(mActivity.getSupportFragmentManager(), SETTING_DIALOG_ID);
}
protected abstract LauncherSettingDialog<T> dialog();
}

@ -1,83 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatSeekBar;
import android.view.View;
import android.widget.SeekBar;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public abstract class LauncherSettingWithSliderController<T, Conf> extends LauncherSettingController<T, Conf>
{
private AppCompatSeekBar mSlider;
private final int mSliderMin;
private final int mSliderMax;
protected LauncherSettingWithSliderController(final AppCompatActivity act, final int min, final int max)
{
super(act);
mSliderMin = min;
mSliderMax = max;
}
@Override
protected void childrenInit(final View root)
{
mSlider = (AppCompatSeekBar) root.findViewById(R.id.inc_launcher_btn_slider);
if (mSliderMax <= mSliderMin)
{
throw new IllegalArgumentException("slider min>=max");
}
mSlider.setMax(mSliderMax - mSliderMin);
mSlider.setOnSeekBarChangeListener(new OnValueChangedListener());
}
protected abstract void onValueChanged(final int v);
protected abstract int currentValue();
@Override
public void updateContent()
{
super.updateContent();
mSlider.setProgress(currentValue() + mSliderMin);
}
@Override
protected String subText()
{
return null; // not used with slider settings
}
@Override
public void onClick(final View v)
{
// not used with slider settings
}
private class OnValueChangedListener implements SeekBar.OnSeekBarChangeListener
{
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress, final boolean fromUser)
{
if (fromUser)
{
onValueChanged(progress);
}
}
@Override
public void onStartTrackingTouch(final SeekBar seekBar)
{
}
@Override
public void onStopTrackingTouch(final SeekBar seekBar)
{
}
}
}

@ -1,38 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ModsBtnController extends LauncherSettingController<Void, Void>
{
private View.OnClickListener mOnSelectedAction;
public ModsBtnController(final AppCompatActivity act, final View.OnClickListener onSelectedAction)
{
super(act);
mOnSelectedAction = onSelectedAction;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_mods_title);
}
@Override
protected String subText()
{
return mActivity.getString(R.string.launcher_btn_mods_subtitle);
}
@Override
public void onClick(final View v)
{
mOnSelectedAction.onClick(v);
}
}

@ -1,40 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class MusicSettingController extends LauncherSettingWithSliderController<Integer, Config>
{
public MusicSettingController(final AppCompatActivity act)
{
super(act, 0, 100);
}
@Override
protected void onValueChanged(final int v)
{
mConfig.updateMusic(v);
updateContent();
}
@Override
protected int currentValue()
{
if (mConfig == null)
{
return Config.DEFAULT_MUSIC_VALUE;
}
return mConfig.mVolumeMusic;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_music_title);
}
}

@ -1,63 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerModeSettingController
extends LauncherSettingWithDialogController<PointerModeSettingController.PointerMode, Config>
{
public PointerModeSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<PointerMode> dialog()
{
return new PointerModeSettingDialog();
}
@Override
public void onItemChosen(final PointerMode item)
{
mConfig.setPointerMode(item == PointerMode.RELATIVE);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_pointermode_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mActivity.getString(R.string.launcher_btn_pointermode_subtitle,
PointerModeSettingDialog.pointerModeToUserString(mActivity, getPointerMode()));
}
private PointerMode getPointerMode()
{
if(mConfig.getPointerModeIsRelative())
{
return PointerMode.RELATIVE;
}
return PointerMode.NORMAL;
}
public enum PointerMode
{
NORMAL,
RELATIVE;
}
}

@ -1,55 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerModeSettingDialog extends LauncherSettingDialog<PointerModeSettingController.PointerMode>
{
private static final List<PointerModeSettingController.PointerMode> POINTER_MODES = new ArrayList<>();
static
{
POINTER_MODES.add(PointerModeSettingController.PointerMode.NORMAL);
POINTER_MODES.add(PointerModeSettingController.PointerMode.RELATIVE);
}
public PointerModeSettingDialog()
{
super(POINTER_MODES);
}
public static String pointerModeToUserString(
final Context ctx,
final PointerModeSettingController.PointerMode pointerMode)
{
switch (pointerMode)
{
default:
return "";
case NORMAL:
return ctx.getString(R.string.misc_pointermode_normal);
case RELATIVE:
return ctx.getString(R.string.misc_pointermode_relative);
}
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_pointermode_title;
}
@Override
protected CharSequence itemName(final PointerModeSettingController.PointerMode item)
{
return pointerModeToUserString(getContext(), item);
}
}

@ -1,51 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerMultiplierSettingController
extends LauncherSettingWithDialogController<Float, Config>
{
public PointerMultiplierSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<Float> dialog()
{
return new PointerMultiplierSettingDialog();
}
@Override
public void onItemChosen(final Float item)
{
mConfig.setPointerSpeedMultiplier(item);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_pointermulti_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
String pointerModeString = PointerMultiplierSettingDialog.pointerMultiplierToUserString(
mConfig.getPointerSpeedMultiplier());
return mActivity.getString(R.string.launcher_btn_pointermulti_subtitle, pointerModeString);
}
}

@ -1,48 +0,0 @@
package eu.vcmi.vcmi.settings;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class PointerMultiplierSettingDialog extends LauncherSettingDialog<Float>
{
private static final List<Float> AVAILABLE_MULTIPLIERS = new ArrayList<>();
static
{
AVAILABLE_MULTIPLIERS.add(1.0f);
AVAILABLE_MULTIPLIERS.add(1.25f);
AVAILABLE_MULTIPLIERS.add(1.5f);
AVAILABLE_MULTIPLIERS.add(1.75f);
AVAILABLE_MULTIPLIERS.add(2.0f);
AVAILABLE_MULTIPLIERS.add(2.5f);
AVAILABLE_MULTIPLIERS.add(3.0f);
}
public PointerMultiplierSettingDialog()
{
super(AVAILABLE_MULTIPLIERS);
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_pointermode_title;
}
@Override
protected CharSequence itemName(final Float item)
{
return pointerMultiplierToUserString(item);
}
public static String pointerMultiplierToUserString(final float multiplier)
{
return String.format(Locale.US, "%.2fx", multiplier);
}
}

@ -1,64 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class ScreenScaleSettingController extends LauncherSettingWithDialogController<ScreenScaleSettingController.ScreenScale, Config>
{
public ScreenScaleSettingController(final AppCompatActivity activity)
{
super(activity);
}
@Override
protected LauncherSettingDialog<ScreenScale> dialog()
{
return new ScreenScaleSettingDialog(mActivity);
}
@Override
public void onItemChosen(final ScreenScale item)
{
mConfig.updateScreenScale(item.mScreenScale);
updateContent();
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_scale_title);
}
@Override
protected String subText()
{
if (mConfig == null)
{
return "";
}
return mConfig.mScreenScale <= 0
? mActivity.getString(R.string.launcher_btn_scale_subtitle_unknown)
: mActivity.getString(R.string.launcher_btn_scale_subtitle, mConfig.mScreenScale);
}
public static class ScreenScale
{
public int mScreenScale;
public ScreenScale(final int scale)
{
mScreenScale = scale;
}
@Override
public String toString()
{
return mScreenScale + "%";
}
}
}

@ -1,98 +0,0 @@
package eu.vcmi.vcmi.settings;
import android.app.Activity;
import android.graphics.Point;
import android.view.WindowMetrics;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.Storage;
import eu.vcmi.vcmi.util.FileUtil;
/**
* @author F
*/
public class ScreenScaleSettingDialog extends LauncherSettingDialog<ScreenScaleSettingController.ScreenScale>
{
public ScreenScaleSettingDialog(Activity mActivity)
{
super(loadScales(mActivity));
}
@Override
protected int dialogTitleResId()
{
return R.string.launcher_btn_scale_title;
}
@Override
protected CharSequence itemName(final ScreenScaleSettingController.ScreenScale item)
{
return item.toString();
}
public static int[] getSupportedScalingRange(Activity activity) {
Point screenRealSize = new Point();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
WindowMetrics windowMetrics = activity.getWindowManager().getCurrentWindowMetrics();
screenRealSize.x = windowMetrics.getBounds().width();
screenRealSize.y = windowMetrics.getBounds().height();
} else {
activity.getWindowManager().getDefaultDisplay().getRealSize(screenRealSize);
}
if (screenRealSize.x < screenRealSize.y) {
int tmp = screenRealSize.x;
screenRealSize.x = screenRealSize.y;
screenRealSize.y = tmp;
}
// H3 resolution, any resolution smaller than that is not correctly supported
Point minResolution = new Point(800, 600);
// arbitrary limit on *downscaling*. Allow some downscaling, if requested by user. Should be generally limited to 100+ for all but few devices
double minimalScaling = 50;
Point renderResolution = screenRealSize;
double maximalScalingWidth = 100.0 * renderResolution.x / minResolution.x;
double maximalScalingHeight = 100.0 * renderResolution.y / minResolution.y;
double maximalScaling = Math.min(maximalScalingWidth, maximalScalingHeight);
return new int[] { (int)minimalScaling, (int)maximalScaling };
}
private static List<ScreenScaleSettingController.ScreenScale> loadScales(Activity activity)
{
List<ScreenScaleSettingController.ScreenScale> availableScales = new ArrayList<>();
try
{
int[] supportedScalingRange = getSupportedScalingRange(activity);
for (int i = 0; i <= supportedScalingRange[1] + 10 - 1; i += 10)
{
if (i >= supportedScalingRange[0])
availableScales.add(new ScreenScaleSettingController.ScreenScale(i));
}
if(availableScales.isEmpty())
{
availableScales.add(new ScreenScaleSettingController.ScreenScale(100));
}
}
catch(Exception ex)
{
ex.printStackTrace();
availableScales.clear();
availableScales.add(new ScreenScaleSettingController.ScreenScale(100));
}
return availableScales;
}
}

@ -1,40 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import eu.vcmi.vcmi.Config;
import eu.vcmi.vcmi.R;
/**
* @author F
*/
public class SoundSettingController extends LauncherSettingWithSliderController<Integer, Config>
{
public SoundSettingController(final AppCompatActivity act)
{
super(act, 0, 100);
}
@Override
protected void onValueChanged(final int v)
{
mConfig.updateSound(v);
updateContent();
}
@Override
protected int currentValue()
{
if (mConfig == null)
{
return Config.DEFAULT_SOUND_VALUE;
}
return mConfig.mVolumeSound;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_sound_title);
}
}

@ -1,39 +0,0 @@
package eu.vcmi.vcmi.settings;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import eu.vcmi.vcmi.R;
import eu.vcmi.vcmi.util.GeneratedVersion;
/**
* @author F
*/
public class StartGameController extends LauncherSettingController<Void, Void>
{
private View.OnClickListener mOnSelectedAction;
public StartGameController(final AppCompatActivity act, final View.OnClickListener onSelectedAction)
{
super(act);
mOnSelectedAction = onSelectedAction;
}
@Override
protected String mainText()
{
return mActivity.getString(R.string.launcher_btn_start_title);
}
@Override
protected String subText()
{
return mActivity.getString(R.string.launcher_btn_start_subtitle, GeneratedVersion.VCMI_VERSION);
}
@Override
public void onClick(final View v)
{
mOnSelectedAction.onClick(v);
}
}

@ -1,49 +0,0 @@
package eu.vcmi.vcmi.util;
import android.annotation.TargetApi;
import android.os.AsyncTask;
import android.os.Build;
import androidx.annotation.RequiresApi;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
import eu.vcmi.vcmi.Const;
/**
* @author F
*/
public abstract class AsyncRequest<T> extends AsyncTask<String, Void, ServerResponse<T>>
{
@TargetApi(Const.SUPPRESS_TRY_WITH_RESOURCES_WARNING)
protected ServerResponse<T> sendRequest(final String url)
{
try
{
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
final int responseCode = conn.getResponseCode();
if (!ServerResponse.isResponseCodeValid(responseCode))
{
return new ServerResponse<>(responseCode, null);
}
try (Scanner s = new java.util.Scanner(conn.getInputStream()).useDelimiter("\\A"))
{
final String response = s.hasNext() ? s.next() : "";
return new ServerResponse<>(responseCode, response);
}
catch (final Exception e)
{
Log.e(this, "Request failed: ", e);
}
}
catch (final Exception e)
{
Log.e(this, "Request failed: ", e);
}
return new ServerResponse<>(ServerResponse.LOCAL_ERROR_IO, null);
}
}

@ -1,177 +1,101 @@
package eu.vcmi.vcmi.util;
import android.annotation.TargetApi;
import android.content.res.AssetManager;
import android.os.Environment;
import android.text.TextUtils;
import android.app.Activity;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.List;
import eu.vcmi.vcmi.Const;
import eu.vcmi.vcmi.Storage;
/**
* @author F
*/
@TargetApi(Const.SUPPRESS_TRY_WITH_RESOURCES_WARNING)
public class FileUtil
{
private static final int BUFFER_SIZE = 4096;
public static String read(final InputStream stream) throws IOException
public static boolean copyData(Uri folderToCopy, Activity activity)
{
try (InputStreamReader reader = new InputStreamReader(stream))
{
return readInternal(reader);
}
File targetDir = Storage.getVcmiDataDir(activity);
DocumentFile sourceDir = DocumentFile.fromTreeUri(activity, folderToCopy);
return copyDirectory(targetDir, sourceDir, List.of("Data", "Maps", "Mp3"), activity);
}
public static String read(final File file) throws IOException
private static boolean copyDirectory(File targetDir, DocumentFile sourceDir, @Nullable List<String> allowed, Activity activity)
{
try (FileReader reader = new FileReader(file))
if (!targetDir.exists())
{
return readInternal(reader);
}
catch (final FileNotFoundException ignored)
{
Log.w("Could not load file: " + file);
return null;
}
}
private static String readInternal(final InputStreamReader reader) throws IOException
{
final char[] buffer = new char[BUFFER_SIZE];
int currentRead;
final StringBuilder content = new StringBuilder();
while ((currentRead = reader.read(buffer, 0, BUFFER_SIZE)) >= 0)
{
content.append(buffer, 0, currentRead);
}
return content.toString();
}
public static void write(final File file, final String data) throws IOException
{
if (!ensureWriteable(file))
{
Log.e("Couldn't write " + data + " to " + file);
return;
}
try (final FileWriter fw = new FileWriter(file, false))
{
Log.v(null, "Saving data: " + data + " to " + file.getAbsolutePath());
fw.write(data);
}
}
private static boolean ensureWriteable(final File file)
{
if (file == null)
{
Log.e("Broken path given to fileutil::ensureWriteable");
return false;
targetDir.mkdir();
}
final File dir = file.getParentFile();
if (dir.exists() || dir.mkdirs())
for (DocumentFile child : sourceDir.listFiles())
{
return true;
}
Log.e("Couldn't create dir " + dir);
return false;
}
public static boolean clearDirectory(final File dir)
{
if (dir == null || dir.listFiles() == null)
{
Log.e("Broken path given to fileutil::clearDirectory");
return false;
}
for (final File f : dir.listFiles())
{
if (f.isDirectory() && !clearDirectory(f))
if (allowed != null)
{
return false;
boolean fileAllowed = false;
for (String str : allowed)
{
if (str.equalsIgnoreCase(child.getName()))
{
fileAllowed = true;
break;
}
}
if (!fileAllowed)
continue;
}
if (!f.delete())
File exported = new File(targetDir, child.getName());
if (child.isFile())
{
if (!exported.exists())
{
try
{
exported.createNewFile();
}
catch (IOException e)
{
Log.e(activity, "createNewFile failed: " + e);
return false;
}
}
try (
final OutputStream targetStream = new FileOutputStream(exported, false);
final InputStream sourceStream = activity.getContentResolver()
.openInputStream(child.getUri()))
{
copyStream(sourceStream, targetStream);
}
catch (IOException e)
{
Log.e(activity, "copyStream failed: " + e);
return false;
}
}
if (child.isDirectory() && !copyDirectory(exported, child, null, activity))
{
return false;
}
}
return true;
}
public static void copyDir(final File srcFile, final File dstFile)
{
File[] files = srcFile.listFiles();
if(!dstFile.exists()) dstFile.mkdir();
if(files == null)
return;
for (File child : files){
File childTarget = new File(dstFile, child.getName());
if(child.isDirectory()){
copyDir(child, childTarget);
}
else{
copyFile(child, childTarget);
}
}
}
public static boolean copyFile(final File srcFile, final File dstFile)
{
if (!srcFile.exists())
{
return false;
}
final File dstDir = dstFile.getParentFile();
if (!dstDir.exists())
{
if (!dstDir.mkdirs())
{
Log.w("Couldn't create dir to copy file: " + dstFile);
return false;
}
}
try (final FileInputStream input = new FileInputStream(srcFile);
final FileOutputStream output = new FileOutputStream(dstFile))
{
copyStream(input, output);
return true;
}
catch (final Exception ex)
{
Log.e("Couldn't copy " + srcFile + " to " + dstFile, ex);
return false;
}
}
public static void copyStream(InputStream source, OutputStream target) throws IOException
private static void copyStream(InputStream source, OutputStream target) throws IOException
{
final byte[] buffer = new byte[BUFFER_SIZE];
int read;
@ -180,171 +104,4 @@ public class FileUtil
target.write(buffer, 0, read);
}
}
// (when internal data have changed)
public static boolean reloadVcmiDataToInternalDir(final File vcmiInternalDir, final AssetManager assets)
{
return clearDirectory(vcmiInternalDir) && unpackVcmiDataToInternalDir(vcmiInternalDir, assets);
}
public static boolean unpackVcmiDataToInternalDir(final File vcmiInternalDir, final AssetManager assets)
{
try
{
final InputStream inputStream = assets.open("internalData.zip");
final boolean success = unpackZipFile(inputStream, vcmiInternalDir, null);
inputStream.close();
return success;
}
catch (final Exception e)
{
Log.e("Couldn't extract vcmi data to internal dir", e);
return false;
}
}
public static boolean unpackZipFile(
final File inputFile,
final File destDir,
IZipProgressReporter progressReporter)
{
try
{
final InputStream inputStream = new FileInputStream(inputFile);
final boolean success = unpackZipFile(
inputStream,
destDir,
progressReporter);
inputStream.close();
return success;
}
catch (final Exception e)
{
Log.e("Couldn't extract file to " + destDir, e);
return false;
}
}
public static int countFilesInZip(final File zipFile)
{
int totalEntries = 0;
try
{
final InputStream inputStream = new FileInputStream(zipFile);
ZipInputStream is = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while ((zipEntry = is.getNextEntry()) != null)
{
totalEntries++;
}
is.closeEntry();
is.close();
inputStream.close();
}
catch (final Exception e)
{
Log.e("Couldn't count items in zip", e);
}
return totalEntries;
}
public static boolean unpackZipFile(
final InputStream inputStream,
final File destDir,
final IZipProgressReporter progressReporter)
{
try
{
int unpackedEntries = 0;
final byte[] buffer = new byte[BUFFER_SIZE];
ZipInputStream is = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while ((zipEntry = is.getNextEntry()) != null)
{
final String fileName = zipEntry.getName();
final File newFile = new File(destDir, fileName);
if (newFile.exists())
{
Log.d("Already exists: " + newFile.getName());
continue;
}
else if (zipEntry.isDirectory())
{
Log.v("Creating new dir: " + zipEntry);
if (!newFile.mkdirs())
{
Log.e("Couldn't create directory " + newFile.getAbsolutePath());
return false;
}
continue;
}
final File parentFile = new File(newFile.getParent());
if (!parentFile.exists() && !parentFile.mkdirs())
{
Log.e("Couldn't create directory " + parentFile.getAbsolutePath());
return false;
}
final FileOutputStream fos = new FileOutputStream(newFile, false);
int currentRead;
while ((currentRead = is.read(buffer)) > 0)
{
fos.write(buffer, 0, currentRead);
}
fos.flush();
fos.close();
++unpackedEntries;
if(progressReporter != null)
{
progressReporter.onUnpacked(newFile);
}
}
Log.d("Unpacked data (" + unpackedEntries + " entries)");
is.closeEntry();
is.close();
return true;
}
catch (final Exception e)
{
Log.e("Couldn't extract vcmi data to " + destDir, e);
return false;
}
}
public static String configFileLocation(File filesDir)
{
return filesDir + "/config/settings.json";
}
public static String readAssetsStream(final AssetManager assets, final String assetPath)
{
if (assets == null || TextUtils.isEmpty(assetPath))
{
return null;
}
try (java.util.Scanner s = new java.util.Scanner(assets.open(assetPath), "UTF-8").useDelimiter("\\A"))
{
return s.hasNext() ? s.next() : null;
}
catch (final IOException e)
{
Log.e("Couldn't read stream data", e);
return null;
}
}
}

@ -1,8 +0,0 @@
package eu.vcmi.vcmi.util;
import java.io.File;
public interface IZipProgressReporter
{
void onUnpacked(File newFile);
}

@ -1,198 +0,0 @@
package eu.vcmi.vcmi.util;
import android.content.Context;
import android.nfc.FormatException;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
public class InstallModAsync
extends AsyncTask<String, String, Boolean>
implements IZipProgressReporter
{
private static final String TAG = "DOWNLOADFILE";
private static final int DOWNLOAD_PERCENT = 70;
private PostDownload callback;
private File downloadLocation;
private File extractLocation;
private Context context;
private int totalFiles;
private int unpackedFiles;
public InstallModAsync(File extractLocation, Context context, PostDownload callback)
{
this.context = context;
this.callback = callback;
this.extractLocation = extractLocation;
}
@Override
protected Boolean doInBackground(String... args)
{
int count;
try
{
File modsFolder = extractLocation.getParentFile();
if (!modsFolder.exists()) modsFolder.mkdir();
this.downloadLocation = File.createTempFile("tmp", ".zip", modsFolder);
URL url = new URL(args[0]);
URLConnection connection = url.openConnection();
connection.connect();
long lengthOfFile = -1;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
{
lengthOfFile = connection.getContentLengthLong();
}
if(lengthOfFile == -1)
{
try
{
lengthOfFile = Long.parseLong(connection.getHeaderField("Content-Length"));
Log.d(TAG, "Length of the file: " + lengthOfFile);
} catch (NumberFormatException ex)
{
Log.d(TAG, "Failed to parse content length", ex);
}
}
if(lengthOfFile == -1)
{
lengthOfFile = 100000000;
Log.d(TAG, "Using dummy length of file");
}
InputStream input = new BufferedInputStream(url.openStream());
FileOutputStream output = new FileOutputStream(downloadLocation); //context.openFileOutput("content.zip", Context.MODE_PRIVATE);
Log.d(TAG, "file saved at " + downloadLocation.getAbsolutePath());
byte data[] = new byte[1024];
long total = 0;
while ((count = input.read(data)) != -1)
{
total += count;
output.write(data, 0, count);
this.publishProgress((int) ((total * DOWNLOAD_PERCENT) / lengthOfFile) + "%");
}
output.flush();
output.close();
input.close();
File tempDir = File.createTempFile("tmp", "", modsFolder);
tempDir.delete();
tempDir.mkdir();
if (!extractLocation.exists()) extractLocation.mkdir();
try
{
totalFiles = FileUtil.countFilesInZip(downloadLocation);
unpackedFiles = 0;
FileUtil.unpackZipFile(downloadLocation, tempDir, this);
return moveModToExtractLocation(tempDir);
}
finally
{
downloadLocation.delete();
FileUtil.clearDirectory(tempDir);
tempDir.delete();
}
} catch (Exception e)
{
Log.e(TAG, "Unhandled exception while installing mod", e);
}
return false;
}
@Override
protected void onProgressUpdate(String... values)
{
callback.downloadProgress(values);
}
@Override
protected void onPostExecute(Boolean result)
{
if (callback != null) callback.downloadDone(result, extractLocation);
}
private boolean moveModToExtractLocation(File tempDir)
{
return moveModToExtractLocation(tempDir, 0);
}
private boolean moveModToExtractLocation(File tempDir, int level)
{
File[] modJson = tempDir.listFiles(new FileFilter()
{
@Override
public boolean accept(File file)
{
return file.getName().equalsIgnoreCase("Mod.json");
}
});
if (modJson != null && modJson.length > 0)
{
File modFolder = modJson[0].getParentFile();
if (!modFolder.renameTo(extractLocation))
{
FileUtil.copyDir(modFolder, extractLocation);
}
return true;
}
if (level <= 1)
{
for (File child : tempDir.listFiles())
{
if (child.isDirectory() && moveModToExtractLocation(child, level + 1))
{
return true;
}
}
}
return false;
}
@Override
public void onUnpacked(File newFile)
{
unpackedFiles++;
int progress = DOWNLOAD_PERCENT
+ (unpackedFiles * (100 - DOWNLOAD_PERCENT) / totalFiles);
publishProgress(progress + "%");
}
public interface PostDownload
{
void downloadDone(Boolean succeed, File modFolder);
void downloadProgress(String... progress);
}
}

@ -12,9 +12,12 @@ import eu.vcmi.vcmi.NativeMethods;
*/
public final class LibsLoader
{
public static final String CLIENT_LIB = "vcmiclient_"
+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI);
public static void loadClientLibs(Context ctx)
{
SDL.loadLibrary("vcmiclient");
SDL.loadLibrary(CLIENT_LIB);
SDL.setContext(ctx);
}

@ -1,15 +1,6 @@
package eu.vcmi.vcmi.util;
import android.os.Environment;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import eu.vcmi.vcmi.BuildConfig;
import eu.vcmi.vcmi.Const;
/**
* @author F
@ -18,8 +9,6 @@ import eu.vcmi.vcmi.Const;
public class Log
{
private static final boolean LOGGING_ENABLED_CONSOLE = BuildConfig.DEBUG;
private static final boolean LOGGING_ENABLED_FILE = true;
private static final String FILELOG_PATH = "/" + Const.VCMI_DATA_ROOT_FOLDER_NAME + "/cache/VCMI_launcher.log";
private static final String TAG_PREFIX = "VCMI/";
private static final String STATIC_TAG = "static";
@ -34,19 +23,6 @@ public class Log
{
android.util.Log.println(priority, TAG_PREFIX + tagString, msg);
}
if (LOGGING_ENABLED_FILE) // this is probably very inefficient, but should be enough for now...
{
try
{
final BufferedWriter fileWriter = new BufferedWriter(new FileWriter(Environment.getExternalStorageDirectory() + FILELOG_PATH, true));
fileWriter.write(String.format("[%s] %s: %s\n", formatPriority(priority), tagString, msg));
fileWriter.flush();
fileWriter.close();
}
catch (IOException ignored)
{
}
}
}
private static String formatPriority(final int priority)
@ -77,23 +53,6 @@ public class Log
return obj.getClass().getSimpleName();
}
public static void init()
{
if (LOGGING_ENABLED_FILE) // clear previous log
{
try
{
final BufferedWriter fileWriter = new BufferedWriter(new FileWriter(Environment.getExternalStorageDirectory() + FILELOG_PATH, false));
fileWriter.write("Starting VCMI launcher log, " + DateFormat.getDateTimeInstance().format(new Date()) + "\n");
fileWriter.flush();
fileWriter.close();
}
catch (IOException ignored)
{
}
}
}
public static void v(final String msg)
{
logInternal(android.util.Log.VERBOSE, STATIC_TAG, msg);

@ -1,30 +0,0 @@
package eu.vcmi.vcmi.util;
/**
* @author F
*/
public class ServerResponse<T>
{
public static final int LOCAL_ERROR_IO = -1;
public static final int LOCAL_ERROR_PARSING = -2;
public int mCode;
public String mRawContent;
public T mContent;
public ServerResponse(final int code, final String content)
{
mCode = code;
mRawContent = content;
}
public static boolean isResponseCodeValid(final int responseCode)
{
return responseCode >= 200 && responseCode < 300;
}
public boolean isValid()
{
return isResponseCodeValid(mCode);
}
}

@ -1,92 +0,0 @@
package eu.vcmi.vcmi.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
/**
* simple shared preferences wrapper
*
* @author F
*/
public class SharedPrefs
{
public static final String KEY_CURRENT_INTERNAL_ASSET_HASH = "KEY_CURRENT_INTERNAL_ASSET_HASH"; // [string]
private static final String VCMI_PREFS_NAME = "VCMIPrefs";
private final SharedPreferences mPrefs;
public SharedPrefs(final Context ctx)
{
mPrefs = ctx.getSharedPreferences(VCMI_PREFS_NAME, Context.MODE_PRIVATE);
}
public void save(final String name, final String value)
{
mPrefs.edit().putString(name, value).apply();
log(name, value, true);
}
public String load(final String name, final String defaultValue)
{
return log(name, mPrefs.getString(name, defaultValue), false);
}
public void save(final String name, final int value)
{
mPrefs.edit().putInt(name, value).apply();
log(name, value, true);
}
public int load(final String name, final int defaultValue)
{
return log(name, mPrefs.getInt(name, defaultValue), false);
}
public void save(final String name, final float value)
{
mPrefs.edit().putFloat(name, value).apply();
log(name, value, true);
}
public float load(final String name, final float defaultValue)
{
return log(name, mPrefs.getFloat(name, defaultValue), false);
}
public void save(final String name, final boolean value)
{
mPrefs.edit().putBoolean(name, value).apply();
log(name, value, true);
}
public boolean load(final String name, final boolean defaultValue)
{
return log(name, mPrefs.getBoolean(name, defaultValue), false);
}
public <T extends Enum<T>> void saveEnum(final String name, final T value)
{
mPrefs.edit().putInt(name, value.ordinal()).apply();
log(name, value, true);
}
@SuppressWarnings("unchecked")
public <T extends Enum<T>> T loadEnum(final String name, @NonNull final T defaultValue)
{
final int rawValue = mPrefs.getInt(name, defaultValue.ordinal());
return (T) log(name, defaultValue.getClass().getEnumConstants()[rawValue], false);
}
private <T> T log(final String key, final T value, final boolean saving)
{
if (saving)
{
Log.v(this, "[prefs saving] " + key + " => " + value);
}
else
{
Log.v(this, "[prefs loading] " + key + " => " + value);
}
return value;
}
}

@ -1,58 +0,0 @@
package eu.vcmi.vcmi.util;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.util.DisplayMetrics;
/**
* @author F
*/
public final class Utils
{
private static String sAppVersionCache;
private Utils()
{
}
public static String appVersionName(final Context ctx)
{
if (sAppVersionCache == null)
{
final PackageManager pm = ctx.getPackageManager();
try
{
final PackageInfo info = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_META_DATA);
return sAppVersionCache = info.versionName;
}
catch (final PackageManager.NameNotFoundException e)
{
Log.e(ctx, "Couldn't resolve app version", e);
}
}
return sAppVersionCache;
}
public static float convertDpToPx(final Context ctx, final float dp)
{
return convertDpToPx(ctx.getResources(), dp);
}
public static float convertDpToPx(final Resources res, final float dp)
{
return dp * res.getDisplayMetrics().density;
}
public static float convertPxToDp(final Context ctx, final float px)
{
return convertPxToDp(ctx.getResources(), px);
}
public static float convertPxToDp(final Resources res, final float px)
{
return px / res.getDisplayMetrics().density;
}
}

@ -1,52 +0,0 @@
package eu.vcmi.vcmi.viewmodels;
import android.view.View;
import androidx.lifecycle.ViewModel;
import androidx.databinding.PropertyChangeRegistry;
import androidx.databinding.Observable;
/**
* @author F
*/
public class ObservableViewModel extends ViewModel implements Observable
{
private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();
@Override
public void addOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback)
{
callbacks.add(callback);
}
@Override
public void removeOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback)
{
callbacks.remove(callback);
}
public int visible(boolean isVisible)
{
return isVisible ? View.VISIBLE : View.GONE;
}
/**
* Notifies observers that all properties of this instance have changed.
*/
void notifyChange() {
callbacks.notifyCallbacks(this, 0, null);
}
/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
void notifyPropertyChanged(int fieldId) {
callbacks.notifyCallbacks(this, fieldId, null);
}
}

@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
private static final String TAG = "SDL";
private static final int SDL_MAJOR_VERSION = 2;
private static final int SDL_MINOR_VERSION = 26;
private static final int SDL_MICRO_VERSION = 1;
private static final int SDL_MICRO_VERSION = 5;
/*
// Display InputType.SOURCE/CLASS of events and devices
//
@ -241,7 +241,14 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
* It can be overridden by derived classes.
*/
protected String getMainSharedObject() {
return null;
String library;
String[] libraries = SDLActivity.mSingleton.getLibraries();
if (libraries.length > 0) {
library = "lib" + libraries[libraries.length - 1] + ".so";
} else {
library = "libmain.so";
}
return getContext().getApplicationInfo().nativeLibraryDir + "/" + library;
}
/**

@ -168,6 +168,32 @@ class SDLJoystickHandler_API16 extends SDLJoystickHandler {
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;
}
}

Binary file not shown.

Before

(image error) Size: 156 B

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:startColor="#000000" android:endColor="#00000000" android:angle="270"/>
</shape>

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z" />
</vector>

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" />
</vector>

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
</vector>

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4V6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
</vector>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="#A0000000" />
<stroke android:color="@color/accent" android:width="1dp" />
</shape>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="1px"
android:height="1px" />
<solid android:color="@color/accent" />
</shape>

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout>
<com.google.android.material.appbar.AppBarLayout
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="?attr/actionBarSize"
android:background="@color/bgMain">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/bgMain"
android:elevation="6dp"
app:elevation="6dp"
app:title="@string/launcher_title" />
</com.google.android.material.appbar.AppBarLayout>
</layout>

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar_include">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/side_margin">
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.Header"
android:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_version_app"
style="@style/VCMI.Text"
android:text="@string/about_version_app" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_version_launcher"
style="@style/VCMI.Text"
android:text="@string/about_version_launcher" />
</LinearLayout>
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/about_section_project" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_link_portal"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_links_main" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_link_repo_main"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_links_repo" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_link_repo_launcher"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_links_repo_launcher" />
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/about_section_legal" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_btn_authors"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_btn_authors" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/about_btn_privacy"
style="@style/VCMI.Entry.Clickable.AboutSimpleEntry"
android:text="@string/about_btn_privacy" />
</LinearLayout>
</ScrollView>

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/error_message"
style="@style/VCMI.Text"
android:layout_margin="@dimen/side_margin"
android:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/error_btn_try_again"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:layout_margin="@dimen/side_margin"
android:layout_marginTop="20dp"
android:text="@string/misc_try_again" />
</LinearLayout>

@ -16,4 +16,4 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
</FrameLayout>

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
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:layout_below="@+id/toolbar_include"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/launcher_version_info"
style="@style/VCMI.Text"
android:padding="@dimen/side_margin"
android:text="@string/app_name" />
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
android:text="@string/launcher_section_init"
app:elevation="2dp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/launcher_btn_start"
layout="@layout/inc_launcher_btn" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/launcher_error"
style="@style/VCMI.Text"
android:drawableLeft="@drawable/ic_error"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:minHeight="80dp"
android:padding="@dimen/side_margin"
android:text="@string/app_name" />
<ProgressBar
android:id="@+id/launcher_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
<include
android:id="@+id/launcher_btn_copy"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_export"
layout="@layout/inc_launcher_btn" />
<include layout="@layout/inc_separator" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/launcher_section_settings" />
<include
android:id="@+id/launcher_btn_mods"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_scale"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_adventure_ai"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_cp"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_pointer_mode"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_pointer_multi"
layout="@layout/inc_launcher_btn" />
<include
android:id="@+id/launcher_btn_volume_sound"
layout="@layout/inc_launcher_slider" />
<include
android:id="@+id/launcher_btn_volume_music"
layout="@layout/inc_launcher_slider" />
</LinearLayout>
</ScrollView>

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="@+id/mods_data_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/toolbar_include"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mods_recycler"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/mods_adapter_item" />
<TextView
android:id="@+id/mods_error_text"
style="@style/VCMI.Text"
android:layout_marginTop="30dp"
android:gravity="center" />
<ProgressBar
android:id="@+id/mods_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/toolbar_include"
layout="@layout/inc_toolbar" />
<ViewStub
android:id="@+id/toolbar_wrapper_content_stub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar_include" />
</RelativeLayout>
</layout>

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/dialog_authors_vcmi" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_authors_vcmi"
style="@style/VCMI.Text"
android:padding="@dimen/side_margin" />
<include layout="@layout/inc_separator" />
<!-- TODO should this be separate or just merged with vcmi authors? -->
<androidx.appcompat.widget.AppCompatTextView
style="@style/VCMI.Text.LauncherSection"
android:text="@string/dialog_authors_launcher" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/dialog_authors_launcher"
style="@style/VCMI.Text"
android:padding="@dimen/side_margin" />
</LinearLayout>
</ScrollView>

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="title"
type="java.lang.String" />
<variable
name="description"
type="java.lang.String" />
</data>
<RelativeLayout
style="@style/VCMI.Entry.Clickable"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/inc_launcher_btn_main"
style="@style/VCMI.Text.LauncherEntry"
android:text="@{title}" />
<TextView
android:id="@+id/inc_launcher_btn_sub"
style="@style/VCMI.Text.LauncherEntry.Sub"
android:text="@{description}" />
</LinearLayout>
</RelativeLayout>
</layout>

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
style="@style/VCMI.Entry"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical">
<TextView
android:id="@+id/inc_launcher_btn_main"
style="@style/VCMI.Text.LauncherEntry"
android:text="@string/app_name" />
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/inc_launcher_btn_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
</LinearLayout>
</RelativeLayout>
</layout>

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