mirror of
https://github.com/vcmi/vcmi.git
synced 2025-03-29 21:56:54 +02:00
commit
48f32aa5e2
.github/workflows
AI
BattleAI
Nullkiller
VCAI
CI
CMakeLists.txtCMakePresets.jsonChangeLog.mdGlobal.hMods/vcmi/config/vcmi
android
.gitignoreAndroidManifest.xmlGeneratedVersion.java.inandroiddeployqt.json.indefs.gradlegradle.properties
vcmi-app
.gitignorebuild.gradle
src/main
AndroidManifest.xml
java
eu/vcmi/vcmi
ActivityAbout.javaActivityBase.javaActivityError.javaActivityLauncher.javaActivityMods.javaActivityWithToolbar.javaConfig.javaConst.javaNativeMethods.javaStorage.javaVcmiSDLActivity.java
content
AsyncLauncherInitialization.javaDialogAuthors.javaModBaseViewHolder.javaModsAdapter.javaModsViewHolder.java
mods
settings
AdventureAiController.javaAdventureAiSelectionDialog.javaCopyDataController.javaDoubleConfig.javaExportDataController.javaLanguageSettingController.javaLanguageSettingDialog.javaLauncherSettingController.javaLauncherSettingDialog.javaLauncherSettingWithDialogController.javaLauncherSettingWithSliderController.javaModsBtnController.javaMusicSettingController.javaPointerModeSettingController.javaPointerModeSettingDialog.javaPointerMultiplierSettingController.javaPointerMultiplierSettingDialog.javaScreenScaleSettingController.javaScreenScaleSettingDialog.javaSoundSettingController.javaStartGameController.java
util
AsyncRequest.javaFileUtil.javaIZipProgressReporter.javaInstallModAsync.javaLibsLoader.javaLog.javaServerResponse.javaSharedPrefs.javaUtils.java
viewmodels
org/libsdl/app
res
drawable-nodpi
drawable-v24
drawable
compat_toolbar_shadow.xmlic_error.xmlic_info.xmlic_launcher_background.xmlic_star_empty.xmlic_star_full.xmlic_star_half.xmloverlay_edittext_background.xmlrecycler_divider_drawable.xml
layout-v21
layout
64
.github/workflows/github.yml
vendored
64
.github/workflows/github.yml
vendored
@ -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
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
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": [
|
||||
|
76
ChangeLog.md
76
ChangeLog.md
@ -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
|
||||
|
23
Global.h
23
Global.h
@ -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
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
|
||||
|
98
android/AndroidManifest.xml
Normal file
98
android/AndroidManifest.xml
Normal file
@ -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@";
|
||||
}
|
17
android/androiddeployqt.json.in
Normal file
17
android/androiddeployqt.json.in
Normal file
@ -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
|
||||
|
8
android/vcmi-app/.gitignore
vendored
8
android/vcmi-app/.gitignore
vendored
@ -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
Loading…
x
Reference in New Issue
Block a user