diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..083abce26 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot#example-dependabotyml-file-for-github-actions +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every day + interval: "daily" diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 955cdbe55..3f50f3368 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -6,9 +6,8 @@ on: - features/* - beta - master + - develop pull_request: - schedule: - - cron: '0 2 * * *' workflow_dispatch: env: @@ -16,55 +15,7 @@ env: BUILD_TYPE: Release jobs: - check_last_build: - if: github.event.schedule != '' - runs-on: ubuntu-latest - outputs: - skip_build: ${{ steps.check_if_built.outputs.skip_build }} - defaults: - run: - shell: bash - steps: - - name: Get repo name - id: get_repo_name - run: echo "::set-output name=value::${GITHUB_REPOSITORY#*/}" - - name: Get last successful build for ${{ github.sha }} - uses: octokit/request-action@v2.1.0 - id: get_last_scheduled_run - with: - route: GET /repos/{owner}/{repo}/actions/runs - owner: ${{ github.repository_owner }} - repo: ${{ steps.get_repo_name.outputs.value }} - status: success - per_page: 1 - head_sha: ${{ github.sha }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Check if successful build of the current commit exists - id: check_if_built - run: | - if [ ${{ fromJson(steps.get_last_scheduled_run.outputs.data).total_count }} -gt 0 ]; then - echo '::set-output name=skip_build::1' - else - echo '::set-output name=skip_build::0' - fi - - name: Cancel current run - if: steps.check_if_built.outputs.skip_build == 1 - uses: octokit/request-action@v2.1.0 - with: - route: POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel - owner: ${{ github.repository_owner }} - repo: ${{ steps.get_repo_name.outputs.value }} - run_id: ${{ github.run_id }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Wait for the run to be cancelled - if: steps.check_if_built.outputs.skip_build == 1 - run: sleep 60 - build: - needs: check_last_build - if: always() && needs.check_last_build.skip_build != 1 strategy: matrix: include: @@ -73,8 +24,8 @@ jobs: test: 0 preset: linux-clang-test - platform: linux - os: ubuntu-20.04 - test: 0 + os: ubuntu-22.04 + test: 1 preset: linux-gcc-test - platform: linux os: ubuntu-20.04 @@ -84,6 +35,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: dmg preset: macos-conan-ninja-release conan_profile: macos-intel @@ -93,6 +45,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: dmg preset: macos-arm-conan-ninja-release conan_profile: macos-arm @@ -102,6 +55,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: ipa preset: ios-release-conan-ccache conan_profile: ios-arm64 @@ -110,17 +64,29 @@ jobs: os: windows-latest test: 0 pack: 1 + pack_type: RelWithDebInfo extension: exe preset: windows-msvc-release-ccache - - platform: mingw-ubuntu + - platform: mingw os: ubuntu-22.04 test: 0 pack: 1 + pack_type: Release extension: exe cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis` cmake_args: -G Ninja preset: windows-mingw-conan-linux conan_profile: mingw64-linux.jinja + - platform: mingw-32 + os: ubuntu-22.04 + test: 0 + pack: 1 + pack_type: Release + extension: exe + cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis` + cmake_args: -G Ninja + preset: windows-mingw-conan-linux + conan_profile: mingw32-linux.jinja - platform: android-32 os: ubuntu-22.04 extension: apk @@ -141,7 +107,7 @@ jobs: shell: bash steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -150,7 +116,7 @@ jobs: run: | find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \ -o -path ./osx -prune -o -type f \ - -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ + -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.webm' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ { ! xargs -0 grep -l -z -P '\r\n'; } - name: Validate JSON @@ -158,7 +124,7 @@ jobs: # also, running it on multiple presets is redundant and slightly increases already long CI built times if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} run: | - pip3 install json5 jstyleson + pip3 install jstyleson python3 CI/linux-qt6/validate_json.py - name: Dependencies @@ -168,7 +134,7 @@ jobs: # ensure the ccache for each PR is separate so they don't interfere with each other # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found - - name: Ccache for PRs + - name: ccache for PRs uses: hendrikmuhs/ccache-action@v1.2 if: ${{ github.event.number != '' }} with: @@ -180,9 +146,9 @@ jobs: max-size: "5G" verbose: 2 - - name: Ccache for everything but PRs + - name: ccache for everything but PRs uses: hendrikmuhs/ccache-action@v1.2 - if: ${{ github.event.number == '' }} + if: ${{ (github.repository == 'vcmi/vcmi' && github.event.number == '' && github.ref == 'refs/heads/develop') || github.repository != 'vcmi/vcmi' }} with: key: ${{ matrix.preset }}-no-PR restore-keys: | @@ -191,7 +157,17 @@ jobs: max-size: "5G" verbose: 2 - - uses: actions/setup-python@v4 + - name: Prepare Heroes 3 data + env: + HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }} + if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }} + run: | + wget --progress=dot:giga https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip + 7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD + mkdir -p ~/.local/share/vcmi/ + mv h3_assets/* ~/.local/share/vcmi/ + + - uses: actions/setup-python@v5 if: "${{ matrix.conan_profile != '' }}" with: python-version: '3.10' @@ -211,10 +187,6 @@ jobs: env: GENERATE_ONLY_BUILT_CONFIG: 1 - - name: Git branch name - id: git-branch-name - uses: EthanSK/git-branch-name-action@v1 - - name: Build Number run: | source '${{github.workspace}}/CI/get_package_name.sh' @@ -227,31 +199,42 @@ jobs: env: PULL_REQUEST: ${{ github.event.pull_request.number }} - - name: CMake Preset with ccache + - name: Configure run: | - cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }} + if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC13=1; fi + cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC13:+-DCMAKE_C_COMPILER=gcc-13 -DCMAKE_CXX_COMPILER=g++-13} - - name: Build Preset + - name: Build run: | cmake --build --preset ${{matrix.preset}} - name: Test - if: ${{ matrix.test == 1 }} + env: + HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }} + if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }} run: | ctest --preset ${{matrix.preset}} + - 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 + echo Killing...; sudo pkill -9 XProtect >/dev/null || true; + echo "Waiting..."; counter=0; while pgrep XProtect && ((counter < 20)); do sleep 3; ((counter++)); done + pgrep XProtect || true + - name: Pack id: cpack if: ${{ matrix.pack == 1 }} run: | cd '${{github.workspace}}/out/build/${{matrix.preset}}' CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey` - "$CPACK_PATH" -C ${{env.BUILD_TYPE}} ${{ matrix.cpack_args }} + counter=0; until "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} || ((counter > 20)); do sleep 3; ((counter++)); done test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \ && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)" rm -rf _CPack_Packages - - name: Create android package + - name: Create Android package if: ${{ startsWith(matrix.platform, 'android') }} run: | cd android @@ -266,7 +249,7 @@ jobs: - name: Artifacts if: ${{ matrix.pack == 1 }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} path: | @@ -274,22 +257,30 @@ jobs: - name: Android artifacts if: ${{ startsWith(matrix.platform, 'android') }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} path: | ${{ env.ANDROID_APK_PATH }} + + - name: Symbols + if: ${{ matrix.platform == 'msvc' }} + uses: actions/upload-artifact@v4 + with: + name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols + path: | + ${{github.workspace}}/**/*.pdb - name: Android JNI ${{matrix.platform}} if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Android JNI ${{matrix.platform}} path: | ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs - name: Upload build - 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' }} + 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 @@ -301,14 +292,6 @@ jobs: env: DEPLOY_RSA: ${{ secrets.DEPLOY_RSA }} PACKAGE_EXTENSION: ${{ matrix.extension }} - - - uses: act10ns/slack@v1 - with: - status: ${{ job.status }} - channel: '#notifications' - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - if: always() # copy-pasted mostly bundle_release: @@ -331,7 +314,7 @@ jobs: shell: bash steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -340,10 +323,11 @@ jobs: env: VCMI_BUILD_PLATFORM: x64 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 if: "${{ matrix.conan_profile != '' }}" with: python-version: '3.10' + - name: Conan setup if: "${{ matrix.conan_profile != '' }}" run: | @@ -359,10 +343,6 @@ jobs: env: GENERATE_ONLY_BUILT_CONFIG: 1 - - name: Git branch name - id: git-branch-name - uses: EthanSK/git-branch-name-action@v1 - - name: Build Number run: | source '${{github.workspace}}/CI/get_package_name.sh' @@ -384,12 +364,12 @@ jobs: cmake --build --preset ${{matrix.preset}} - name: Download libs x64 - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: Android JNI android-64 path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/ - - name: Create android package + - name: Create Android package run: | cd android ./gradlew bundleRelease --info @@ -399,16 +379,8 @@ jobs: ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - name: Android artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} path: | ${{ env.ANDROID_APK_PATH }} - - - uses: act10ns/slack@v1 - with: - status: ${{ job.status }} - channel: '#notifications' - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - if: always() diff --git a/.gitignore b/.gitignore index 0e9d67a1c..28ba6ab2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,15 @@ /client/vcmiclient /server/vcmiserver +/launcher/.lupdate /launcher/vcmilauncher +/mapeditor/.lupdate /launcher/vcmilauncher_automoc.cpp /conan-* build/ .cache/* out/ +/.qt *.dll *.exe *.depend @@ -41,6 +44,8 @@ VCMI_VS11.sdf VCMI_VS11.opensdf .DS_Store CMakeUserPresets.json +compile_commands.json +fuzzylite.pc # Visual Studio *.suo @@ -61,5 +66,8 @@ CMakeUserPresets.json /deps .vs/ +# Visual Studio Code +/.vscode/ + # CLion .idea/ diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 73dfd12af..b2d4c769f 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -33,7 +33,8 @@ void DamageCache::buildDamageCache(std::shared_ptr hb, int sid return u->isValidTarget(); }); - std::vector ourUnits, enemyUnits; + std::vector ourUnits; + std::vector enemyUnits; for(auto stack : stacks) { @@ -61,16 +62,12 @@ void DamageCache::buildDamageCache(std::shared_ptr hb, int sid int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) { - auto damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); + bool wasComputedBefore = damageCache[attacker->unitId()].count(defender->unitId()); - if(damage == 0) - { + if (!wasComputedBefore) cacheDamage(attacker, defender, hb); - damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); - } - - return static_cast(damage); + return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); } int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) @@ -295,8 +292,10 @@ AttackPossibility AttackPossibility::evaluate( for(int i = 0; i < totalAttacks; i++) { - int64_t damageDealt, damageReceived; - float defenderDamageReduce, attackerDamageReduce; + int64_t damageDealt; + int64_t damageReceived; + float defenderDamageReduce; + float attackerDamageReduce; DamageEstimation retaliation; auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation); diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index 2181d883a..b8ff77218 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -10,7 +10,6 @@ #pragma once #include "../../lib/battle/CUnitState.h" #include "../../CCallback.h" -#include "common.h" #include "StackWithBonuses.h" #define BATTLE_TRACE_LEVEL 0 diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 3a428e122..37ba4f270 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -49,7 +49,6 @@ CBattleAI::~CBattleAI() void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) { - setCbc(CB); env = ENV; cb = CB; playerID = *CB->getPlayerID(); @@ -90,7 +89,8 @@ void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance) static float getStrengthRatio(std::shared_ptr cb, int side) { auto stacks = cb->battleGetAllStacks(); - auto our = 0, enemy = 0; + auto our = 0; + auto enemy = 0; for(auto stack : stacks) { @@ -120,7 +120,6 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) }; BattleAction result = BattleAction::makeDefend(stack); - setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) auto start = std::chrono::high_resolution_clock::now(); @@ -145,7 +144,7 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) result = evaluator.selectStackAction(stack); - if(!skipCastUntilNextBattle && evaluator.canCastSpell()) + if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell()) { auto spelCasted = evaluator.attemptCastingSpell(stack); diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 3e53f8ad7..3a5a4d154 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -22,6 +22,8 @@ #include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/BattleAction.h" +#include "../../lib/CRandomGenerator.h" + // TODO: remove // Eventually only IBattleInfoCallback and battle::Unit should be used, @@ -70,8 +72,9 @@ std::vector BattleEvaluator::getBrokenWallMoatHexes() const std::optional BattleEvaluator::findBestCreatureSpell(const CStack *stack) { //TODO: faerie dragon type spell should be selected by server - SpellID creatureSpellToCast = cb->getBattle(battleID)->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); - if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) + SpellID creatureSpellToCast = cb->getBattle(battleID)->getRandomCastedSpell(CRandomGenerator::getDefault(), stack); + + if(stack->canCast() && creatureSpellToCast != SpellID::NONE) { const CSpell * spell = creatureSpellToCast.toSpell(); @@ -146,7 +149,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex, bestAttack.attack.chargeDistance, - bestAttack.attack.attacker->speed(0, true), + bestAttack.attack.attacker->getMovementRange(0), bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, score @@ -224,7 +227,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) } } - return BattleAction::makeDefend(stack); + return stack->waited() ? BattleAction::makeDefend(stack) : BattleAction::makeWait(stack); } uint64_t timeElapsed(std::chrono::time_point start) @@ -349,10 +352,11 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) LOGL("Casting spells sounds like fun. Let's see..."); //Get all spells we can cast std::vector possibleSpells; - vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool - { - return s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero); - }); + + for (auto const & s : VLC->spellh->objects) + if (s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero)) + possibleSpells.push_back(s.get()); + LOGFL("I can cast %d spells.", possibleSpells.size()); vstd::erase_if(possibleSpells, [](const CSpell *s) @@ -427,33 +431,36 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) state->nextTurn(unit->unitId()); - PotentialTargets pt(unit, damageCache, state); + PotentialTargets potentialTargets(unit, damageCache, state); - if(!pt.possibleAttacks.empty()) + if(!potentialTargets.possibleAttacks.empty()) { - AttackPossibility ap = pt.bestAction(); + AttackPossibility attackPossibility = potentialTargets.bestAction(); - auto swb = state->getForUpdate(unit->unitId()); - *swb = *ap.attackerState; + auto stackWithBonuses = state->getForUpdate(unit->unitId()); + *stackWithBonuses = *attackPossibility.attackerState; - if(ap.defenderDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilAttack); - if(ap.attackerDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilBeingAttacked); - - for(auto affected : ap.affectedUnits) + if(attackPossibility.defenderDamageReduce > 0) { - swb = state->getForUpdate(affected->unitId()); - *swb = *affected; + stackWithBonuses->removeUnitBonus(Bonus::UntilAttack); + stackWithBonuses->removeUnitBonus(Bonus::UntilOwnAttack); + } + if(attackPossibility.attackerDamageReduce > 0) + stackWithBonuses->removeUnitBonus(Bonus::UntilBeingAttacked); - if(ap.defenderDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilBeingAttacked); - if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId()) - swb->removeUnitBonus(Bonus::UntilAttack); + for(auto affected : attackPossibility.affectedUnits) + { + stackWithBonuses = state->getForUpdate(affected->unitId()); + *stackWithBonuses = *affected; + + if(attackPossibility.defenderDamageReduce > 0) + stackWithBonuses->removeUnitBonus(Bonus::UntilBeingAttacked); + if(attackPossibility.attackerDamageReduce > 0 && attackPossibility.attack.defender->unitId() == affected->unitId()) + stackWithBonuses->removeUnitBonus(Bonus::UntilAttack); } } - auto bav = pt.bestActionValue(); + auto bav = potentialTargets.bestActionValue(); //best action is from effective owner`s point if view, we need to convert to our point if view if(state->battleGetOwner(unit) != playerID) @@ -549,7 +556,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool { auto original = cb->getBattle(battleID)->battleGetUnitByID(u->unitId()); - return !original || u->speed() != original->speed(); + return !original || u->getMovementRange() != original->getMovementRange(); }); DamageCache safeCopy = damageCache; @@ -605,7 +612,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) if(ourUnit * goodEffect == 1) { - if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost() || !unit->unitSlot().validSlot())) + if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) continue; ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 9d1f4b584..d7902f878 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -258,7 +258,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( updateReachabilityMap(hb); - if(result.bestAttack.attack.shooting && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) + if(result.bestAttack.attack.shooting + && !activeStack->waited() + && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) { if(!canBeHitThisTurn(result.bestAttack)) return result; // lets wait @@ -268,7 +270,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( { float score = evaluateExchange(ap, 0, targets, damageCache, hb); - if(score > result.score || (score == result.score && result.wait)) + if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait)) { result.score = score; result.bestAttack = ap; @@ -295,7 +297,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( if(targets.unreachableEnemies.empty()) return result; - auto speed = activeStack->speed(); + auto speed = activeStack->getMovementRange(); if(speed == 0) return result; @@ -322,7 +324,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( auto turnsToRich = (distance - 1) / speed + 1; auto hexes = closestStack->getSurroundingHexes(); - auto enemySpeed = closestStack->speed(); + auto enemySpeed = closestStack->getMovementRange(); auto speedRatio = speed / static_cast(enemySpeed); auto multiplier = speedRatio > 1 ? 1 : speedRatio; @@ -481,11 +483,6 @@ float BattleExchangeEvaluator::evaluateExchange( DamageCache & damageCache, std::shared_ptr hb) { - if(ap.from.hex == 127) - { - logAi->trace("x"); - } - BattleScore score = calculateExchange(ap, turn, targets, damageCache, hb); #if BATTLE_TRACE_LEVEL >= 1 @@ -687,11 +684,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange( for(auto hex : hexes) reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); - if(!ap.attack.shooting) - { - v.adjustPositions(melleeAttackers, ap, reachabilityMap); - } - #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); #endif @@ -699,69 +691,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange( return v.getScore(); } -void BattleExchangeVariant::adjustPositions( - std::vector attackers, - const AttackPossibility & ap, - std::map & reachabilityMap) -{ - auto hexes = ap.attack.defender->getSurroundingHexes(); - - boost::sort(attackers, [&](const battle::Unit * u1, const battle::Unit * u2) -> bool - { - if(attackerValue[u1->unitId()].isRetalitated && !attackerValue[u2->unitId()].isRetalitated) - return true; - - if(attackerValue[u2->unitId()].isRetalitated && !attackerValue[u1->unitId()].isRetalitated) - return false; - - return attackerValue[u1->unitId()].value > attackerValue[u2->unitId()].value; - }); - - vstd::erase_if_present(hexes, ap.from); - vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); - - float notRealizedDamage = 0; - - for(auto unit : attackers) - { - if(unit->unitId() == ap.attack.attacker->unitId()) - continue; - - if(!vstd::contains_if(hexes, [&](BattleHex h) -> bool - { - return vstd::contains(reachabilityMap[h], unit); - })) - { - notRealizedDamage += attackerValue[unit->unitId()].value; - continue; - } - - auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float - { - auto score = vstd::contains(reachabilityMap[h], unit) - ? reachabilityMap[h].size() - : 0; - - if(unit->doubleWide()) - { - auto backHex = unit->occupiedHex(h); - - if(vstd::contains(hexes, backHex)) - score += reachabilityMap[backHex].size(); - } - - return score; - }); - - hexes.erase(desiredPosition); - } - - if(notRealizedDamage > ap.attackValue() && notRealizedDamage > attackerValue[ap.attack.attacker->unitId()].value) - { - dpsScore = BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0); - } -} - bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) { for(auto pos : ap.attack.attacker->getSurroundingHexes()) @@ -824,7 +753,7 @@ std::vector BattleExchangeEvaluator::getOneTurnReachableUn continue; } - auto unitSpeed = unit->speed(turn); + auto unitSpeed = unit->getMovementRange(turn); auto radius = unitSpeed * (turn + 1); ReachabilityInfo unitReachability = vstd::getOrCompute( @@ -887,14 +816,15 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb continue; auto blockedUnitDamage = unit->getMinDamage(hb.battleCanShoot(unit)) * unit->getCount(); - auto ratio = blockedUnitDamage / (blockedUnitDamage + activeUnitDamage); + float ratio = blockedUnitDamage / (float)(blockedUnitDamage + activeUnitDamage + 0.01); auto unitReachability = turnBattle.getReachability(unit); + auto unitSpeed = unit->getMovementRange(turn); // Cached value, to avoid performance hit for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) { bool enemyUnit = false; - bool reachable = unitReachability.distances[hex] <= unit->speed(turn); + bool reachable = unitReachability.distances[hex] <= unitSpeed; if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) { @@ -906,14 +836,14 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb for(BattleHex neighbor : hex.neighbouringTiles()) { - reachable = unitReachability.distances[neighbor] <= unit->speed(turn); + reachable = unitReachability.distances[neighbor] <= unitSpeed; if(reachable) break; } } } - if(!reachable && vstd::contains(reachabilityMap[hex], unit)) + if(!reachable && std::count(reachabilityMap[hex].begin(), reachabilityMap[hex].end(), unit) > 1) { blockingScore += ratio * (enemyUnit ? BLOCKING_OWN_ATTACK_PENALTY : BLOCKING_OWN_MOVE_PENALTY); } diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index cca98ae12..97af9a3f7 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -106,11 +106,6 @@ public: const BattleScore & getScore() const { return dpsScore; } - void adjustPositions( - std::vector attackers, - const AttackPossibility & ap, - std::map & reachabilityMap); - private: BattleScore dpsScore; std::map attackerValue; diff --git a/AI/BattleAI/CMakeLists.txt b/AI/BattleAI/CMakeLists.txt index 335c92f5c..b30be26ce 100644 --- a/AI/BattleAI/CMakeLists.txt +++ b/AI/BattleAI/CMakeLists.txt @@ -2,7 +2,6 @@ set(battleAI_SRCS AttackPossibility.cpp BattleAI.cpp BattleEvaluator.cpp - common.cpp EnemyInfo.cpp PossibleSpellcast.cpp PotentialTargets.cpp @@ -17,7 +16,6 @@ set(battleAI_HEADERS AttackPossibility.h BattleAI.h BattleEvaluator.h - common.h EnemyInfo.h PotentialTargets.h PossibleSpellcast.h @@ -26,12 +24,12 @@ set(battleAI_HEADERS BattleExchangeVariant.h ) -if(NOT ENABLE_STATIC_AI_LIBS) +if(NOT ENABLE_STATIC_LIBS) list(APPEND battleAI_SRCS main.cpp StdInc.cpp) endif() assign_source_group(${battleAI_SRCS} ${battleAI_HEADERS}) -if(ENABLE_STATIC_AI_LIBS) +if(ENABLE_STATIC_LIBS) add_library(BattleAI STATIC ${battleAI_SRCS} ${battleAI_HEADERS}) else() add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS}) @@ -39,7 +37,7 @@ else() endif() target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET} TBB::tbb) +target_link_libraries(BattleAI PRIVATE vcmi TBB::tbb) vcmi_set_output_dir(BattleAI "AI") enable_pch(BattleAI) diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 7beabf514..752c84ca1 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -134,7 +134,7 @@ SlotID StackWithBonuses::unitSlot() const TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit, const CBonusSystemNode * root, const std::string & cachingStr) const { - TBonusListPtr ret = std::make_shared(); + auto ret = std::make_shared(); TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, root, cachingStr); vstd::copy_if(*originalList, std::back_inserter(*ret), [this](const std::shared_ptr & b) @@ -298,7 +298,7 @@ std::shared_ptr HypotheticBattle::getForUpdate(uint32_t id) } } -battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const +battle::Units HypotheticBattle::getUnitsIf(const battle::UnitFilter & predicate) const { battle::Units proxyed = BattleProxy::getUnitsIf(predicate); @@ -356,7 +356,7 @@ void HypotheticBattle::addUnit(uint32_t id, const JsonNode & data) { battle::UnitInfo info; info.load(id, data); - std::shared_ptr newUnit = std::make_shared(this, info); + auto newUnit = std::make_shared(this, info); stackStates[newUnit->unitId()] = newUnit; } diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index a69bbb887..6a50f6f84 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -114,7 +114,7 @@ public: int32_t getActiveStackID() const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; void nextRound() override; void nextTurn(uint32_t unitId) override; diff --git a/AI/BattleAI/common.h b/AI/BattleAI/common.h deleted file mode 100644 index dfc9b4623..000000000 --- a/AI/BattleAI/common.h +++ /dev/null @@ -1,26 +0,0 @@ -/* - * common.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -class CBattleCallback; - -template -const Val getValOr(const std::map &Map, const Key &key, const Val2 defaultValue) -{ - //returning references here won't work: defaultValue must be converted into Val, creating temporary - auto i = Map.find(key); - if(i != Map.end()) - return i->second; - else - return defaultValue; -} - -void setCbc(std::shared_ptr cb); -std::shared_ptr getCbc(); diff --git a/AI/BattleAI/main.cpp b/AI/BattleAI/main.cpp index 101491d93..7e8b93c05 100644 --- a/AI/BattleAI/main.cpp +++ b/AI/BattleAI/main.cpp @@ -15,7 +15,7 @@ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif -static const char *g_cszAiName = "Battle AI"; +static const char * const g_cszAiName = "Battle AI"; extern "C" DLL_EXPORT int GetGlobalAiVersion() { diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 2812b7140..0ca26e8c4 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -14,11 +14,11 @@ #include "../../lib/CStack.h" #include "../../lib/battle/BattleAction.h" -void CEmptyAI::saveGame(BinarySerializer & h, const int version) +void CEmptyAI::saveGame(BinarySerializer & h) { } -void CEmptyAI::loadGame(BinaryDeserializer & h, const int version) +void CEmptyAI::loadGame(BinaryDeserializer & h) { } diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index a2f125fbe..eb2935f83 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -19,8 +19,8 @@ class CEmptyAI : public CGlobalAI std::shared_ptr cb; public: - virtual void saveGame(BinarySerializer & h, const int version) override; - virtual void loadGame(BinaryDeserializer & h, const int version) override; + void saveGame(BinarySerializer & h) override; + void loadGame(BinaryDeserializer & h) override; void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void yourTurn(QueryID queryID) override; diff --git a/AI/EmptyAI/CMakeLists.txt b/AI/EmptyAI/CMakeLists.txt index cd594b0f3..3988f41a7 100644 --- a/AI/EmptyAI/CMakeLists.txt +++ b/AI/EmptyAI/CMakeLists.txt @@ -8,12 +8,12 @@ set(emptyAI_HEADERS CEmptyAI.h ) -if(NOT ENABLE_STATIC_AI_LIBS) +if(NOT ENABLE_STATIC_LIBS) list(APPEND emptyAI_SRCS main.cpp StdInc.cpp) endif() assign_source_group(${emptyAI_SRCS} ${emptyAI_HEADERS}) -if(ENABLE_STATIC_AI_LIBS) +if(ENABLE_STATIC_LIBS) add_library(EmptyAI STATIC ${emptyAI_SRCS} ${emptyAI_HEADERS}) else() add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS}) @@ -21,7 +21,7 @@ else() endif() target_include_directories(EmptyAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(EmptyAI PRIVATE ${VCMI_LIB_TARGET}) +target_link_libraries(EmptyAI PRIVATE vcmi) vcmi_set_output_dir(EmptyAI "AI") enable_pch(EmptyAI) diff --git a/AI/EmptyAI/main.cpp b/AI/EmptyAI/main.cpp index e6ae9483c..e67974b9e 100644 --- a/AI/EmptyAI/main.cpp +++ b/AI/EmptyAI/main.cpp @@ -11,7 +11,6 @@ #include "CEmptyAI.h" -std::set ais; extern "C" DLL_EXPORT int GetGlobalAiVersion() { return AI_INTERFACE_VER; diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 55d8a59b7..9ad7c8421 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" +#include "../../lib/ArtifactUtils.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" @@ -63,7 +64,7 @@ struct SetGlobalState }; -#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai); +#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai) #define NET_EVENT_HANDLER SET_GLOBAL_STATE(this) #define MAKING_TURN SET_GLOBAL_STATE(this) @@ -100,7 +101,7 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose) if(!hero) validateObject(details.id); //enemy hero may have left visible area - const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; + const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0)); const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose)); @@ -420,14 +421,14 @@ void AIGateway::requestRealized(PackageApplied * pa) NET_EVENT_HANDLER; if(status.haveTurn()) { - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { if(pa->result) status.madeTurn(); } } - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { status.receivedAnswerConfirmation(pa->requestID, pa->result); } @@ -480,7 +481,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop) NET_EVENT_HANDLER; if(sop->what == ObjProperty::OWNER) { - auto relations = myCb->getPlayerRelations(playerID, (PlayerColor)sop->val); + auto relations = myCb->getPlayerRelations(playerID, sop->identifier.as()); auto obj = myCb->getObj(sop->id, false); if(!nullkiller) // crash protection @@ -585,11 +586,18 @@ void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, s requestActionASAP([=]() { + int sel = 0; + if(hPtr.validAndSet()) { + std::unique_lock lockGuard(nullkiller->aiStateMutex); + nullkiller->heroManager->update(); - answerQuery(queryID, nullkiller->heroManager->selectBestSkill(hPtr, skills)); + + sel = nullkiller->heroManager->selectBestSkill(hPtr, skills); } + + answerQuery(queryID, sel); }); } @@ -624,7 +632,8 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectorid == hero->id ? objects.back() : objects.front(); auto objType = topObj->ID; // top object should be our hero auto goalObjectID = nullkiller->getTargetObject(); - auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength(); + auto danger = nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()); + auto ratio = static_cast(danger) / hero->getTotalStrength(); answer = topObj->id == goalObjectID; // no if we do not aim to visit this object logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name, ratio); @@ -640,7 +649,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector (1 / SAFE_ATTACK_CONSTANT); answer = !dangerUnknown && !dangerTooHigh; @@ -660,14 +669,18 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector take the last one (they're indexed [1-size]) sel = components.size(); - // TODO: Find better way to understand it is Chest of Treasures - if(hero.validAndSet() - && components.size() == 2 - && components.front().id == Component::EComponentType::RESOURCE - && (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN - || nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE)) { - sel = 1; // for now lets pick gold from a chest. + std::unique_lock mxLock(nullkiller->aiStateMutex); + + // TODO: Find better way to understand it is Chest of Treasures + if(hero.validAndSet() + && components.size() == 2 + && components.front().type == ComponentType::RESOURCE + && (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN + || nullkiller->buildAnalyzer->isGoldPreasureHigh())) + { + sel = 1; + } } answerQuery(askID, sel); @@ -746,27 +759,25 @@ void AIGateway::showMapObjectSelectDialog(QueryID askID, const Component & icon, requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); } -void AIGateway::saveGame(BinarySerializer & h, const int version) +void AIGateway::saveGame(BinarySerializer & h) { - LOG_TRACE_PARAMS(logAi, "version '%i'", version); NET_EVENT_HANDLER; nullkiller->memory->removeInvisibleObjects(myCb.get()); - CAdventureAI::saveGame(h, version); - serializeInternal(h, version); + CAdventureAI::saveGame(h); + serializeInternal(h); } -void AIGateway::loadGame(BinaryDeserializer & h, const int version) +void AIGateway::loadGame(BinaryDeserializer & h) { - LOG_TRACE_PARAMS(logAi, "version '%i'", version); //NET_EVENT_HANDLER; #if 0 //disabled due to issue 2890 registerGoals(h); #endif // 0 - CAdventureAI::loadGame(h, version); - serializeInternal(h, version); + CAdventureAI::loadGame(h); + serializeInternal(h); } bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj) @@ -858,6 +869,8 @@ void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h { makePossibleUpgrades(h.get()); + std::unique_lock lockGuard(nullkiller->aiStateMutex); + if(!h->visitedTown->garrisonHero || !nullkiller->isHeroLocked(h->visitedTown->garrisonHero)) moveCreaturesToHero(h->visitedTown); @@ -995,21 +1008,21 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance for(auto p : h->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(h, p.first)); + allArtifacts.push_back(ArtifactLocation(h->id, p.first)); } } for(auto slot : h->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact))); if(otherh) { for(auto p : otherh->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(otherh, p.first)); + allArtifacts.push_back(ArtifactLocation(otherh->id, p.first)); } for(auto slot : otherh->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact))); } //we give stuff to one hero or another, depending on giveStuffToFirstHero @@ -1021,13 +1034,13 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance for(auto location : allArtifacts) { - if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) + if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot)) continue; //don't reequip artifact we already wear if(location.slot == ArtifactPosition::MACH4) // don't attempt to move catapult continue; - auto s = location.getSlot(); + auto s = cb->getHero(location.artHolder)->getSlot(location.slot); if(!s || s->locked) //we can't move locks continue; auto artifact = s->artifact; @@ -1038,9 +1051,9 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance bool emptySlotFound = false; for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) { - ArtifactLocation destLocation(target, slot); - if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { + ArtifactLocation destLocation(target->id, slot); cb->swapArtifacts(location, destLocation); //just put into empty slot emptySlotFound = true; changeMade = true; @@ -1054,11 +1067,11 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one { - ArtifactLocation destLocation(target, slot); //if that artifact is better than what we have, pick it - if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { - cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); + ArtifactLocation destLocation(target->id, slot); + cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact))); changeMade = true; break; } @@ -1119,15 +1132,6 @@ void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, Qu logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); battlename.clear(); - if (queryID != QueryID::NONE) - { - status.addQuery(queryID, "Combat result dialog"); - const int confirmAction = 0; - requestActionASAP([=]() - { - answerQuery(queryID, confirmAction); - }); - } CAdventureAI::battleEnd(battleID, br, queryID); } @@ -1403,7 +1407,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade int accquiredResources = 0; if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) { - if(const IMarket * m = IMarket::castFrom(obj, false)) + if(const auto * m = dynamic_cast(obj)) { auto freeRes = cb->getResourceAmount(); //trade only resources which are not reserved for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++) @@ -1412,13 +1416,14 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade if(res.getNum() == g.resID) //sell any other resource continue; - int toGive, toGet; + int toGive; + int toGet; m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); toGive = static_cast(toGive * (it->resVal / toGive)); //round down //TODO trade only as much as needed if (toGive) //don't try to sell 0 resources { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive); + cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); accquiredResources = static_cast(toGet * (it->resVal / toGive)); logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); } diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 63d7b1a44..e4aa16ab5 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -62,7 +62,7 @@ public: void heroVisit(const CGObjectInstance * obj, bool started); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battle; h & remainingQueries; @@ -119,8 +119,8 @@ public: void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void saveGame(BinarySerializer & h, const int version) override; //saving - void loadGame(BinaryDeserializer & h, const int version) override; //loading + void saveGame(BinarySerializer & h) override; //saving + void loadGame(BinaryDeserializer & h) override; //loading void finish() override; void availableCreaturesChanged(const CGDwelling * town) override; @@ -203,7 +203,7 @@ public: //special function that can be called ONLY from game events handling thread and will send request ASAP void requestActionASAP(std::function whatToDo); - template void serializeInternal(Handler & h, const int version) + template void serializeInternal(Handler & h) { h & nullkiller->memory->knownTeleportChannels; h & nullkiller->memory->knownSubterraneanGates; diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 501d882c8..1fa07a404 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -276,12 +276,10 @@ creInfo infoFromDC(const dwellingContent & dc) ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed if (ci.creID != CreatureID::NONE) { - ci.cre = VLC->creatures()->getById(ci.creID); - ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. + ci.level = ci.creID.toCreature()->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. } else { - ci.cre = nullptr; ci.level = 0; } return ci; @@ -439,7 +437,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject case Obj::MAGIC_WELL: return h->mana < h->manaLimit(); case Obj::PRISON: - return ai->cb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + return !ai->heroManager->heroCapReached(); case Obj::TAVERN: case Obj::EYE_OF_MAGI: case Obj::BOAT: diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index 646616f1a..b4bacdcce 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -60,7 +60,9 @@ struct creInfo; class AIGateway; class Nullkiller; -const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; +const int GOLD_MINE_PRODUCTION = 1000; +const int WOOD_ORE_MINE_PRODUCTION = 2; +const int RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; const int ALLOWED_ROAMING_HEROES = 8; @@ -113,7 +115,7 @@ public: bool validAndSet() const; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & this->h; h & hid; @@ -145,7 +147,7 @@ struct ObjectIdRef bool operator<(const ObjectIdRef & rhs) const; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; } @@ -161,7 +163,6 @@ struct creInfo { int count; CreatureID creID; - const Creature * cre; int level; }; creInfo infoFromDC(const dwellingContent & dc); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index e0eb6333b..c1827547f 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -63,9 +63,9 @@ std::vector ArmyManager::toSlotInfo(std::vector army) const { SlotInfo slot; - slot.creature = VLC->creh->objects[i.cre->getId()]; + slot.creature = i.creID.toCreature(); slot.count = i.count; - slot.power = evaluateStackPower(i.cre, i.count); + slot.power = evaluateStackPower(i.creID.toCreature(), i.count); result.push_back(slot); } @@ -117,7 +117,7 @@ std::vector::iterator ArmyManager::getWeakestCreature(std::vectorgetLevel() != right.creature->getLevel()) return left.creature->getLevel() < right.creature->getLevel(); - return left.creature->speed() > right.creature->speed(); + return left.creature->getMovementRange() > right.creature->getMovementRange(); }); return weakest; @@ -128,7 +128,7 @@ class TemporaryArmy : public CArmedInstance public: void armyChanged() override {} TemporaryArmy() - :CArmedInstance(true) + :CArmedInstance(nullptr, true) { } }; @@ -259,7 +259,7 @@ std::shared_ptr ArmyManager::getArmyAvailableToBuyAsCCreatureSet( if(!ci.count || ci.creID == CreatureID::NONE) continue; - vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford + vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford if(!ci.count) continue; @@ -270,7 +270,7 @@ std::shared_ptr ArmyManager::getArmyAvailableToBuyAsCCreatureSet( break; army->setCreature(dst, ci.creID, ci.count); - availableRes -= ci.cre->getFullRecruitCost() * ci.count; + availableRes -= ci.creID.toCreature()->getFullRecruitCost() * ci.count; } return army; @@ -287,7 +287,7 @@ ui64 ArmyManager::howManyReinforcementsCanBuy( for(const creInfo & ci : army) { - aivalue += ci.count * ci.cre->getAIValue(); + aivalue += ci.count * ci.creID.toCreature()->getAIValue(); } return aivalue; @@ -320,7 +320,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( if(i < GameConstants::CREATURES_PER_TOWN && countGrowth) { - ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth(); + ci.count += town ? town->creatureGrowth(i) : ci.creID.toCreature()->getGrowth(); } if(!ci.count) continue; @@ -334,13 +334,13 @@ std::vector ArmyManager::getArmyAvailableToBuy( freeHeroSlots--; //new slot will be occupied } - vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford + vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford if(!ci.count) continue; ci.level = i; //this is important for Dungeon Summoning Portal creaturesInDwellings.push_back(ci); - availableRes -= ci.cre->getFullRecruitCost() * ci.count; + availableRes -= ci.creID.toCreature()->getFullRecruitCost() * ci.count; } return creaturesInDwellings; diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 386e71779..74e5a3d70 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -120,6 +120,11 @@ TResources BuildAnalyzer::getTotalResourcesRequired() const return result; } +bool BuildAnalyzer::isGoldPreasureHigh() const +{ + return goldPreasure > ai->settings->getMaxGoldPreasure(); +} + void BuildAnalyzer::update() { logAi->trace("Start analysing build"); @@ -318,7 +323,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const { for(auto tdi : developmentInfos) { - if(tdi.town->subID == alignment && tdi.town->hasBuilt(bid)) + if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid)) return true; } diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.h b/AI/Nullkiller/Analyzers/BuildAnalyzer.h index 43049b295..754225070 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.h +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.h @@ -96,6 +96,7 @@ public: const std::vector & getDevelopmentInfo() const { return developmentInfos; } TResources getDailyIncome() const { return dailyIncome; } float getGoldPreasure() const { return goldPreasure; } + bool isGoldPreasureHigh() const; bool hasAnyBuilding(int32_t alignment, BuildingID bid) const; private: diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 1ee4e57f5..76ac640f7 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -11,11 +11,12 @@ #include "DangerHitMapAnalyzer.h" #include "../Engine/Nullkiller.h" +#include "../../../lib/CRandomGenerator.h" namespace NKAI { -HitMapInfo HitMapInfo::NoThreat; +const HitMapInfo HitMapInfo::NoThreat; double HitMapInfo::value() const { @@ -165,7 +166,7 @@ void DangerHitMapAnalyzer::calculateTileOwners() auto addTownHero = [&](const CGTownInstance * town) { - auto townHero = new CGHeroInstance(); + auto townHero = new CGHeroInstance(town->cb); CRandomGenerator rng; auto visitablePos = town->visitablePos(); @@ -225,7 +226,7 @@ void DangerHitMapAnalyzer::calculateTileOwners() } } - if(ourDistance == enemyDistance) + if(vstd::isAlmostEqual(ourDistance, enemyDistance)) { hitMap[pos.x][pos.y][pos.z].closestTown = nullptr; } @@ -265,8 +266,9 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & { int3 tile = path.targetTile(); int turn = path.turn(); - const HitMapNode & info = hitMap[tile.x][tile.y][tile.z]; + const auto& info = getTileThreat(tile); + return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger)) || (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger)); } @@ -280,13 +282,9 @@ const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance const HitMapNode & DangerHitMapAnalyzer::getTileThreat(const int3 & tile) const { - const HitMapNode & info = hitMap[tile.x][tile.y][tile.z]; - - return info; + return hitMap[tile.x][tile.y][tile.z]; } -const std::set empty = {}; - std::set DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const { std::set result; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 45538c99b..fc2890846 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -18,7 +18,7 @@ struct AIPath; struct HitMapInfo { - static HitMapInfo NoThreat; + static const HitMapInfo NoThreat; uint64_t danger; uint8_t turn; diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index bd43d4df3..fdb0f72ae 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -17,7 +17,7 @@ namespace NKAI { -SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator( +const SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator( { std::make_shared( std::map @@ -46,7 +46,7 @@ SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluato std::make_shared() }); -SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator( +const SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator( { std::make_shared( std::map @@ -187,7 +187,9 @@ bool HeroManager::heroCapReached() const int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); return heroCount >= ALLOWED_ROAMING_HEROES - || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + || heroCount >= ai->settings->getMaxRoamingHeroes() + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); } float HeroManager::getMagicStrength(const CGHeroInstance * hero) const @@ -331,7 +333,7 @@ void WisdomRule::evaluateScore(const CGHeroInstance * hero, SecondarySkill skill score += 1.5; } -std::vector AtLeastOneMagicRule::magicSchools = { +const std::vector AtLeastOneMagicRule::magicSchools = { SecondarySkill::AIR_MAGIC, SecondarySkill::EARTH_MAGIC, SecondarySkill::FIRE_MAGIC, diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index a7744ad1f..fa44198b7 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -58,8 +58,8 @@ public: class DLL_EXPORT HeroManager : public IHeroManager { private: - static SecondarySkillEvaluator wariorSkillsScores; - static SecondarySkillEvaluator scountSkillsScores; + static const SecondarySkillEvaluator wariorSkillsScores; + static const SecondarySkillEvaluator scountSkillsScores; CCallback * cb; //this is enough, but we downcast from CCallback const Nullkiller * ai; @@ -114,7 +114,7 @@ public: class AtLeastOneMagicRule : public ISecondarySkillRule { private: - static std::vector magicSchools; + static const std::vector magicSchools; public: void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const override; diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index d21b92965..8cf713954 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -47,13 +47,13 @@ Goals::TGoalVec BuildingBehavior::decompose() const totalDevelopmentCost.toString()); auto & developmentInfos = ai->nullkiller->buildAnalyzer->getDevelopmentInfo(); - auto goldPreasure = ai->nullkiller->buildAnalyzer->getGoldPreasure(); + auto isGoldPreasureLow = !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh(); for(auto & developmentInfo : developmentInfos) { for(auto & buildingInfo : developmentInfo.toBuild) { - if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) + if(isGoldPreasureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0) { if(buildingInfo.notEnoughRes) { diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.h b/AI/Nullkiller/Behaviors/BuildingBehavior.h index 813a37619..4836d1cb0 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.h +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.h @@ -25,9 +25,9 @@ namespace Goals { } - virtual Goals::TGoalVec decompose() const override; - virtual std::string toString() const override; - virtual bool operator==(const BuildingBehavior & other) const override + Goals::TGoalVec decompose() const override; + std::string toString() const override; + bool operator==(const BuildingBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index b5260ac3a..3e8251dfd 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -46,8 +46,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose() const for(const CGHeroInstance * targetHero : heroes) { - if(ai->nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE - && !town->hasBuilt(BuildingID::CITY_HALL)) + if(ai->nullkiller->buildAnalyzer->isGoldPreasureHigh() && !town->hasBuilt(BuildingID::CITY_HALL)) { continue; } diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.h b/AI/Nullkiller/Behaviors/BuyArmyBehavior.h index a6bf681c7..909e7221c 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.h +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.h @@ -24,9 +24,9 @@ namespace Goals { } - virtual Goals::TGoalVec decompose() const override; - virtual std::string toString() const override; - virtual bool operator==(const BuyArmyBehavior & other) const override + Goals::TGoalVec decompose() const override; + std::string toString() const override; + bool operator==(const BuyArmyBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h index ce075c858..09bdd8e0a 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.h @@ -48,8 +48,8 @@ namespace Goals specificObjects = true; } - virtual Goals::TGoalVec decompose() const override; - virtual std::string toString() const override; + Goals::TGoalVec decompose() const override; + std::string toString() const override; CaptureObjectsBehavior & ofType(int type) { @@ -65,7 +65,7 @@ namespace Goals return *this; } - virtual bool operator==(const CaptureObjectsBehavior & other) const override; + bool operator==(const CaptureObjectsBehavior & other) const override; static Goals::TGoalVec getVisitGoals(const std::vector & paths, const CGObjectInstance * objToVisit = nullptr); diff --git a/AI/Nullkiller/Behaviors/ClusterBehavior.h b/AI/Nullkiller/Behaviors/ClusterBehavior.h index e5a52f73c..c19642a8d 100644 --- a/AI/Nullkiller/Behaviors/ClusterBehavior.h +++ b/AI/Nullkiller/Behaviors/ClusterBehavior.h @@ -28,10 +28,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; - virtual std::string toString() const override; + TGoalVec decompose() const override; + std::string toString() const override; - virtual bool operator==(const ClusterBehavior & other) const override + bool operator==(const ClusterBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.h b/AI/Nullkiller/Behaviors/DefenceBehavior.h index fab4745f5..18d577c66 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.h +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.h @@ -29,10 +29,10 @@ namespace Goals { } - virtual Goals::TGoalVec decompose() const override; - virtual std::string toString() const override; + Goals::TGoalVec decompose() const override; + std::string toString() const override; - virtual bool operator==(const DefenceBehavior & other) const override + bool operator==(const DefenceBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index 935e782f5..de1fab7f4 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -246,7 +246,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) { auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero); - if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + if(heroRole == HeroRole::MAIN && path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit()) hasMainAround = true; } @@ -335,7 +335,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) if(!upgrade.upgradeValue && armyToGetOrBuy.upgradeValue > 20000 && ai->nullkiller->heroManager->canRecruitHero(town) - && path.turn() < SCOUT_TURN_DISTANCE_LIMIT) + && path.turn() < ai->nullkiller->settings->getScoutHeroTurnDistanceLimit()) { for(auto hero : cb->getAvailableHeroes(town)) { @@ -344,7 +344,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader) if(scoutReinforcement >= armyToGetOrBuy.upgradeValue && ai->nullkiller->getFreeGold() >20000 - && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE) + && !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh()) { Composition recruitHero; diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.h b/AI/Nullkiller/Behaviors/GatherArmyBehavior.h index b933575ba..b2ef06113 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.h +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; - virtual std::string toString() const override; + TGoalVec decompose() const override; + std::string toString() const override; - virtual bool operator==(const GatherArmyBehavior & other) const override + bool operator==(const GatherArmyBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 885cc7af2..91c384ff1 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -85,8 +85,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const continue; if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 - || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 - && ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)) + || (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->nullkiller->buildAnalyzer->isGoldPreasureHigh())) { tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3))); } diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h index 4d1102b4d..e45c16e67 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; - virtual std::string toString() const override; + TGoalVec decompose() const override; + std::string toString() const override; - virtual bool operator==(const RecruitHeroBehavior & other) const override + bool operator==(const RecruitHeroBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.cpp b/AI/Nullkiller/Behaviors/StartupBehavior.cpp index 84abb41fe..3a7f59f72 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StartupBehavior.cpp @@ -71,7 +71,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown) for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) { - if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD)) + if((obj->ID == Obj::RESOURCE && dynamic_cast(obj)->resourceID() == EGameResID::GOLD) || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE || obj->ID == Obj::WATER_WHEEL) diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.h b/AI/Nullkiller/Behaviors/StartupBehavior.h index 12cfb33c2..0386b60a0 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.h +++ b/AI/Nullkiller/Behaviors/StartupBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; - virtual std::string toString() const override; + TGoalVec decompose() const override; + std::string toString() const override; - virtual bool operator==(const StartupBehavior & other) const override + bool operator==(const StartupBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.h b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h index 260cf136a..6287b85b0 100644 --- a/AI/Nullkiller/Behaviors/StayAtTownBehavior.h +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h @@ -25,10 +25,10 @@ namespace Goals { } - virtual TGoalVec decompose() const override; - virtual std::string toString() const override; + TGoalVec decompose() const override; + std::string toString() const override; - virtual bool operator==(const StayAtTownBehavior & other) const override + bool operator==(const StayAtTownBehavior & other) const override { return true; } diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index 042cb5a0d..c84fa0fd6 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -17,6 +17,7 @@ set(Nullkiller_SRCS AIUtility.cpp Analyzers/ArmyManager.cpp Analyzers/HeroManager.cpp + Engine/Settings.cpp Engine/FuzzyEngines.cpp Engine/FuzzyHelper.cpp Engine/AIMemory.cpp @@ -80,6 +81,7 @@ set(Nullkiller_HEADERS AIUtility.h Analyzers/ArmyManager.h Analyzers/HeroManager.h + Engine/Settings.h Engine/FuzzyEngines.h Engine/FuzzyHelper.h Engine/AIMemory.h @@ -125,12 +127,12 @@ set(Nullkiller_HEADERS AIGateway.h ) -if(NOT ENABLE_STATIC_AI_LIBS) +if(NOT ENABLE_STATIC_LIBS) list(APPEND Nullkiller_SRCS main.cpp StdInc.cpp) endif() assign_source_group(${Nullkiller_SRCS} ${Nullkiller_HEADERS}) -if(ENABLE_STATIC_AI_LIBS) +if(ENABLE_STATIC_LIBS) add_library(Nullkiller STATIC ${Nullkiller_SRCS} ${Nullkiller_HEADERS}) else() add_library(Nullkiller SHARED ${Nullkiller_SRCS} ${Nullkiller_HEADERS}) @@ -138,7 +140,7 @@ else() endif() target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(Nullkiller PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite TBB::tbb) +target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb) vcmi_set_output_dir(Nullkiller "AI") enable_pch(Nullkiller) diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index c20b39143..a7fb1b26c 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.cpp +++ b/AI/Nullkiller/Engine/FuzzyEngines.cpp @@ -39,7 +39,9 @@ void engineBase::addRule(const std::string & txt) struct armyStructure { - float walkers, shooters, flyers; + float walkers; + float shooters; + float flyers; ui32 maxSpeed; }; diff --git a/AI/Nullkiller/Engine/FuzzyEngines.h b/AI/Nullkiller/Engine/FuzzyEngines.h index 1873b97a5..d57a4a6c6 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.h +++ b/AI/Nullkiller/Engine/FuzzyEngines.h @@ -41,8 +41,14 @@ public: TacticalAdvantageEngine(); float getTacticalAdvantage(const CArmedInstance * we, const CArmedInstance * enemy); //returns factor how many times enemy is stronger than us private: - fl::InputVariable * ourWalkers, *ourShooters, *ourFlyers, *enemyWalkers, *enemyShooters, *enemyFlyers; - fl::InputVariable * ourSpeed, *enemySpeed; + fl::InputVariable * ourWalkers; + fl::InputVariable * ourShooters; + fl::InputVariable * ourFlyers; + fl::InputVariable * enemyWalkers; + fl::InputVariable * enemyShooters; + fl::InputVariable * enemyFlyers; + fl::InputVariable * ourSpeed; + fl::InputVariable * enemySpeed; fl::InputVariable * bankPresent; fl::InputVariable * castleWalls; fl::OutputVariable * threat; diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index e550c47ba..25ca12951 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -24,13 +24,13 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) { //this one is not fuzzy anymore, just calculate weighted average - auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance); + auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); ui64 totalStrength = 0; ui8 totalChance = 0; - for(auto config : bankInfo->getPossibleGuards()) + for(auto config : bankInfo->getPossibleGuards(bank->cb)) { totalStrength += config.second.totalStrength * config.first; totalChance += config.first; @@ -161,10 +161,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) } case Obj::PYRAMID: { - if(obj->subID == 0) - return estimateBankDanger(dynamic_cast(obj)); - else - return 0; + return estimateBankDanger(dynamic_cast(obj)); } default: return 0; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 79c21d1ab..f9620e011 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -27,15 +27,11 @@ namespace NKAI using namespace Goals; -#if NKAI_TRACE_LEVEL >= 1 -#define MAXPASS 1000000 -#else -#define MAXPASS 30 -#endif - Nullkiller::Nullkiller() + :activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true) { - memory.reset(new AIMemory()); + memory = std::make_unique(); + settings = std::make_unique(); } void Nullkiller::init(std::shared_ptr cb, PlayerColor playerID) @@ -115,6 +111,8 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi void Nullkiller::resetAiState() { + std::unique_lock lockGuard(aiStateMutex); + lockedResources = TResources(); scanDepth = ScanDepth::MAIN_FULL; playerID = ai->playerID; @@ -127,6 +125,8 @@ void Nullkiller::updateAiState(int pass, bool fast) { boost::this_thread::interruption_point(); + std::unique_lock lockGuard(aiStateMutex); + auto start = std::chrono::high_resolution_clock::now(); activeHero = nullptr; @@ -162,12 +162,12 @@ void Nullkiller::updateAiState(int pass, bool fast) if(scanDepth == ScanDepth::SMALL) { - cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT; + cfg.mainTurnDistanceLimit = ai->nullkiller->settings->getMainHeroTurnDistanceLimit(); } if(scanDepth != ScanDepth::ALL_FULL) { - cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT; + cfg.scoutTurnDistanceLimit = ai->nullkiller->settings->getScoutHeroTurnDistanceLimit(); } boost::this_thread::interruption_point(); @@ -231,13 +231,13 @@ void Nullkiller::makeTurn() resetAiState(); - for(int i = 1; i <= MAXPASS; i++) + for(int i = 1; i <= settings->getMaxPass(); i++) { updateAiState(i); Goals::TTask bestTask = taskptr(Goals::Invalid()); - for(;i <= MAXPASS; i++) + for(;i <= settings->getMaxPass(); i++) { Goals::TTaskVec fastTasks = { choseBestTask(sptr(BuyArmyBehavior()), 1), @@ -324,9 +324,9 @@ void Nullkiller::makeTurn() executeTask(bestTask); - if(i == MAXPASS) + if(i == settings->getMaxPass()) { - logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription); + logAi->warn("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription); } } } diff --git a/AI/Nullkiller/Engine/Nullkiller.h b/AI/Nullkiller/Engine/Nullkiller.h index 36f3504fd..5f1ccebb8 100644 --- a/AI/Nullkiller/Engine/Nullkiller.h +++ b/AI/Nullkiller/Engine/Nullkiller.h @@ -11,6 +11,7 @@ #include "PriorityEvaluator.h" #include "FuzzyHelper.h" +#include "Settings.h" #include "AIMemory.h" #include "DeepDecomposer.h" #include "../Analyzers/DangerHitMapAnalyzer.h" @@ -23,7 +24,6 @@ namespace NKAI { -const float MAX_GOLD_PEASURE = 0.3f; const float MIN_PRIORITY = 0.01f; const float SMALL_SCAN_MIN_PRIORITY = 0.4f; @@ -71,8 +71,10 @@ public: std::unique_ptr dangerEvaluator; std::unique_ptr decomposer; std::unique_ptr armyFormation; + std::unique_ptr settings; PlayerColor playerID; std::shared_ptr cb; + std::mutex aiStateMutex; Nullkiller(); void init(std::shared_ptr cb, PlayerColor playerID); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 98ce1a86f..f9db14725 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -69,7 +69,7 @@ PriorityEvaluator::~PriorityEvaluator() void PriorityEvaluator::initVisitTile() { - auto file = CResourceHandler::get()->load(ResourcePath("config/ai/object-priorities.txt"))->readAll(); + auto file = CResourceHandler::get()->load(ResourcePath("config/ai/nkai/object-priorities.txt"))->readAll(); std::string str = std::string((char *)file.first.get(), file.second); engine = fl::FllImporter().fromString(str); armyLossPersentageVariable = engine->getInputVariable("armyLoss"); @@ -122,7 +122,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer { //Fixme: unused variable hero - auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance); + auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); auto resources = bankInfo->getPossibleResourcesReward(); TResources result = TResources(); @@ -137,11 +137,23 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer return sum > 1 ? result / sum : result; } +uint64_t getResourcesGoldReward(const TResources & res) +{ + int nonGoldResources = res[EGameResID::GEMS] + + res[EGameResID::SULFUR] + + res[EGameResID::WOOD] + + res[EGameResID::ORE] + + res[EGameResID::CRYSTAL] + + res[EGameResID::MERCURY]; + + return res[EGameResID::GOLD] + 100 * nonGoldResources; +} + uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero) { - auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance); + auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); - auto creatures = bankInfo->getPossibleCreaturesReward(); + auto creatures = bankInfo->getPossibleCreaturesReward(target->cb); uint64_t result = 0; const auto& slots = hero->Slots(); @@ -236,7 +248,7 @@ int getDwellingArmyCost(const CGObjectInstance * target) return cost; } -uint64_t evaluateArtifactArmyValue(CArtifactInstance * art) +static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art) { if(art->artType->getId() == ArtifactID::SPELL_SCROLL) return 1500; @@ -467,14 +479,20 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons switch(target->ID) { case Obj::MINE: - return target->subID == GameResID(EGameResID::GOLD) + { + auto mine = dynamic_cast(target); + return mine->producedResource == EGameResID::GOLD ? 0.5f - : 0.4f * getTotalResourceRequirementStrength(target->subID) + 0.1f * getResourceRequirementStrength(target->subID); + : 0.4f * getTotalResourceRequirementStrength(mine->producedResource) + 0.1f * getResourceRequirementStrength(mine->producedResource); + } case Obj::RESOURCE: - return target->subID == GameResID(EGameResID::GOLD) + { + auto resource = dynamic_cast(target); + return resource->resourceID() == EGameResID::GOLD ? 0 - : 0.2f * getTotalResourceRequirementStrength(target->subID) + 0.4f * getResourceRequirementStrength(target->subID); + : 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID()); + } case Obj::CREATURE_BANK: { @@ -485,7 +503,7 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons //Evaluate resources used for construction. Gold is evaluated separately. if (it->resType != EGameResID::GOLD) { - sum += 0.1f * getResourceRequirementStrength(it->resType); + sum += 0.1f * it->resVal * getResourceRequirementStrength(it->resType); } } return sum; @@ -523,6 +541,9 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons ? getEnemyHeroStrategicalValue(dynamic_cast(target)) : 0; + case Obj::KEYMASTER: + return 0.6f; + default: return 0; } @@ -582,6 +603,8 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::PANDORAS_BOX: //Can contains experience, spells, or skills (only on custom maps) return 2.5f; + case Obj::PYRAMID: + return 3.0f; case Obj::HERO: return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES ? enemyHeroEliminationSkillRewardRatio * dynamic_cast(target)->level @@ -626,12 +649,14 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG const int dailyIncomeMultiplier = 5; const float enemyArmyEliminationGoldRewardRatio = 0.2f; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; - auto isGold = target->subID == GameResID(EGameResID::GOLD); // TODO: other resorces could be sold but need to evaluate market power switch(target->ID) { case Obj::RESOURCE: - return isGold ? 600 : 100; + { + auto * res = dynamic_cast(target); + return res->resourceID() == GameResID::GOLD ? 600 : 100; + } case Obj::TREASURE_CHEST: return 1500; case Obj::WATER_WHEEL: @@ -640,7 +665,10 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG return dailyIncomeMultiplier * estimateTownIncome(ai->cb.get(), target, hero); case Obj::MINE: case Obj::ABANDONED_MINE: - return dailyIncomeMultiplier * (isGold ? 1000 : 75); + { + auto * mine = dynamic_cast(target); + return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75); + } case Obj::MYSTICAL_GARDEN: case Obj::WINDMILL: return 100; @@ -649,7 +677,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG case Obj::WAGON: return 100; case Obj::CREATURE_BANK: - return getCreatureBankResources(target, hero)[EGameResID::GOLD]; + return getResourcesGoldReward(getCreatureBankResources(target, hero)); case Obj::CRYPT: case Obj::DERELICT_SHIP: return 3000; @@ -674,7 +702,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG class HeroExchangeEvaluator : public IEvaluationContextBuilder { public: - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::HERO_EXCHANGE) return; @@ -691,7 +719,7 @@ public: class ArmyUpgradeEvaluator : public IEvaluationContextBuilder { public: - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::ARMY_UPGRADE) return; @@ -708,7 +736,7 @@ public: class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder { public: - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::STAY_AT_TOWN) return; @@ -743,7 +771,7 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin class DefendTownEvaluator : public IEvaluationContextBuilder { public: - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::DEFEND_TOWN) return; @@ -793,7 +821,7 @@ private: public: ExecuteHeroChainEvaluationContextBuilder(const Nullkiller * ai) : ai(ai) {} - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::EXECUTE_HERO_CHAIN) return; @@ -851,7 +879,7 @@ class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder public: ClusterEvaluationContextBuilder(const Nullkiller * ai) {} - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::UNLOCK_CLUSTER) return; @@ -898,7 +926,7 @@ public: class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder { public: - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES) return; @@ -926,7 +954,7 @@ private: public: DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {} - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::DISMISS_HERO) return; @@ -946,7 +974,7 @@ public: class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder { public: - virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override { if(task->goalType != Goals::BUILD_STRUCTURE) return; @@ -1005,7 +1033,7 @@ public: uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const { - if(ai->buildAnalyzer->hasAnyBuilding(town->subID, bi.id)) + if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id)) return 0; auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID); diff --git a/AI/Nullkiller/Engine/Settings.cpp b/AI/Nullkiller/Engine/Settings.cpp new file mode 100644 index 000000000..d004db24c --- /dev/null +++ b/AI/Nullkiller/Engine/Settings.cpp @@ -0,0 +1,78 @@ +/* +* Settings.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#include "StdInc.h" +#include + +#include "Settings.h" +#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h" +#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" +#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" +#include "../../../lib/mapObjects/MapObjects.h" +#include "../../../lib/modding/CModHandler.h" +#include "../../../lib/VCMI_Lib.h" +#include "../../../lib/filesystem/Filesystem.h" +#include "../../../lib/json/JsonNode.h" + +namespace NKAI +{ + Settings::Settings() + : maxRoamingHeroes(8), + mainHeroTurnDistanceLimit(10), + scoutHeroTurnDistanceLimit(5), + maxGoldPreasure(0.3f), + maxpass(30) + { + ResourcePath resource("config/ai/nkai/nkai-settings", EResType::JSON); + + loadFromMod("core", resource); + + for(const auto & modName : VLC->modh->getActiveMods()) + { + if(CResourceHandler::get(modName)->existsResource(resource)) + loadFromMod(modName, resource); + } + } + + void Settings::loadFromMod(const std::string & modName, const ResourcePath & resource) + { + if(!CResourceHandler::get(modName)->existsResource(resource)) + { + logGlobal->error("Failed to load font %s from mod %s", resource.getName(), modName); + return; + } + + JsonNode node(JsonPath::fromResource(resource), modName); + + if(node.Struct()["maxRoamingHeroes"].isNumber()) + { + maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer(); + } + + if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber()) + { + mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer(); + } + + if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber()) + { + scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer(); + } + + if(node.Struct()["maxpass"].isNumber()) + { + maxpass = node.Struct()["maxpass"].Integer(); + } + + if(node.Struct()["maxGoldPreasure"].isNumber()) + { + maxGoldPreasure = node.Struct()["maxGoldPreasure"].Float(); + } + } +} \ No newline at end of file diff --git a/AI/Nullkiller/Engine/Settings.h b/AI/Nullkiller/Engine/Settings.h new file mode 100644 index 000000000..1be6aac19 --- /dev/null +++ b/AI/Nullkiller/Engine/Settings.h @@ -0,0 +1,42 @@ +/* +* Settings.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +class ResourcePath; + +VCMI_LIB_NAMESPACE_END + +namespace NKAI +{ + class Settings + { + private: + int maxRoamingHeroes; + int mainHeroTurnDistanceLimit; + int scoutHeroTurnDistanceLimit; + int maxpass; + float maxGoldPreasure; + + public: + Settings(); + + int getMaxPass() const { return maxpass; } + float getMaxGoldPreasure() const { return maxGoldPreasure; } + int getMaxRoamingHeroes() const { return maxRoamingHeroes; } + int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; } + int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; } + + private: + void loadFromMod(const std::string & modName, const ResourcePath & resource); + }; +} \ No newline at end of file diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index d089083bf..2579c1e12 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -180,11 +180,7 @@ public: { } - virtual ~cannotFulfillGoalException() throw () - { - }; - - const char * what() const throw () override + const char * what() const noexcept override { return msg.c_str(); } @@ -203,11 +199,7 @@ public: msg = goal->toString(); } - virtual ~goalFulfilledException() throw () - { - }; - - const char * what() const throw () override + const char * what() const noexcept override { return msg.c_str(); } diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.h b/AI/Nullkiller/Goals/AdventureSpellCast.h index 23cfc4a97..de403905a 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.h +++ b/AI/Nullkiller/Goals/AdventureSpellCast.h @@ -35,7 +35,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const AdventureSpellCast & other) const override; + bool operator==(const AdventureSpellCast & other) const override; }; } diff --git a/AI/Nullkiller/Goals/Build.h b/AI/Nullkiller/Goals/Build.h index 8ff8d3228..cb69dc9b1 100644 --- a/AI/Nullkiller/Goals/Build.h +++ b/AI/Nullkiller/Goals/Build.h @@ -32,7 +32,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const Build & other) const override + bool operator==(const Build & other) const override { return true; } diff --git a/AI/Nullkiller/Goals/BuildBoat.h b/AI/Nullkiller/Goals/BuildBoat.h index 31383d256..1a2432081 100644 --- a/AI/Nullkiller/Goals/BuildBoat.h +++ b/AI/Nullkiller/Goals/BuildBoat.h @@ -29,7 +29,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const BuildBoat & other) const override; + bool operator==(const BuildBoat & other) const override; }; } diff --git a/AI/Nullkiller/Goals/BuildThis.h b/AI/Nullkiller/Goals/BuildThis.h index f4bacbc5e..02b93c2ab 100644 --- a/AI/Nullkiller/Goals/BuildThis.h +++ b/AI/Nullkiller/Goals/BuildThis.h @@ -39,8 +39,8 @@ namespace Goals } BuildThis(BuildingID Bid, const CGTownInstance * tid); - virtual bool operator==(const BuildThis & other) const override; - virtual std::string toString() const override; + bool operator==(const BuildThis & other) const override; + std::string toString() const override; void accept(AIGateway * ai) override; }; } diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index 55a8ec16e..9e2f52bff 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -54,12 +54,12 @@ void BuyArmy::accept(AIGateway * ai) if(objid != CreatureID::NONE && ci.creID.getNum() != objid) continue; - vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); + vstd::amin(ci.count, res / ci.creID.toCreature()->getFullRecruitCost()); if(ci.count) { cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level); - valueBought += ci.count * ci.cre->getAIValue(); + valueBought += ci.count * ci.creID.toCreature()->getAIValue(); } } diff --git a/AI/Nullkiller/Goals/BuyArmy.h b/AI/Nullkiller/Goals/BuyArmy.h index d4fdc6166..1de8f57ce 100644 --- a/AI/Nullkiller/Goals/BuyArmy.h +++ b/AI/Nullkiller/Goals/BuyArmy.h @@ -36,11 +36,11 @@ namespace Goals priority = 3;//TODO: evaluate? } - virtual bool operator==(const BuyArmy & other) const override; + bool operator==(const BuyArmy & other) const override; - virtual std::string toString() const override; + std::string toString() const override; - virtual void accept(AIGateway * ai) override; + void accept(AIGateway * ai) override; }; } diff --git a/AI/Nullkiller/Goals/CGoal.h b/AI/Nullkiller/Goals/CGoal.h index 722c7ebd2..92e4a92a2 100644 --- a/AI/Nullkiller/Goals/CGoal.h +++ b/AI/Nullkiller/Goals/CGoal.h @@ -37,14 +37,14 @@ namespace Goals { return new T(static_cast(*this)); //casting enforces template instantiation } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); //h & goalType & isElementar & isAbstract & priority; //h & value & resID & objid & aid & tile & hero & town & bid; } - virtual bool operator==(const AbstractGoal & g) const override + bool operator==(const AbstractGoal & g) const override { if(goalType != g.goalType) return false; @@ -54,7 +54,7 @@ namespace Goals virtual bool operator==(const T & other) const = 0; - virtual TGoalVec decompose() const override + TGoalVec decompose() const override { TSubgoal single = decomposeSingle(); @@ -90,11 +90,11 @@ namespace Goals return *((T *)this); } - virtual bool isElementar() const override { return true; } + bool isElementar() const override { return true; } - virtual HeroPtr getHero() const override { return AbstractGoal::hero; } + HeroPtr getHero() const override { return AbstractGoal::hero; } - virtual int getHeroExchangeCount() const override { return 0; } + int getHeroExchangeCount() const override { return 0; } }; } diff --git a/AI/Nullkiller/Goals/CaptureObject.h b/AI/Nullkiller/Goals/CaptureObject.h index e78993638..2b3c339d6 100644 --- a/AI/Nullkiller/Goals/CaptureObject.h +++ b/AI/Nullkiller/Goals/CaptureObject.h @@ -34,11 +34,11 @@ namespace Goals name = obj->getObjectName(); } - virtual bool operator==(const CaptureObject & other) const override; - virtual Goals::TGoalVec decompose() const override; - virtual std::string toString() const override; - virtual bool hasHash() const override { return true; } - virtual uint64_t getHash() const override; + bool operator==(const CaptureObject & other) const override; + Goals::TGoalVec decompose() const override; + std::string toString() const override; + bool hasHash() const override { return true; } + uint64_t getHash() const override; }; } diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index d0e981a78..4b64b5f34 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -98,7 +98,7 @@ std::string CompleteQuest::questToString() const return "inactive quest"; MetaString ms; - q.quest->getRolloverText(ms, false); + q.quest->getRolloverText(q.obj->cb, ms, false); return ms.toString(); } @@ -210,7 +210,7 @@ TGoalVec CompleteQuest::missionResources() const TGoalVec CompleteQuest::missionDestroyObj() const { - auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); + auto obj = cb->getObj(q.quest->killTarget); if(!obj) return CaptureObjectsBehavior(q.obj).decompose(); diff --git a/AI/Nullkiller/Goals/CompleteQuest.h b/AI/Nullkiller/Goals/CompleteQuest.h index 59ed0dc31..57b9fb236 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.h +++ b/AI/Nullkiller/Goals/CompleteQuest.h @@ -29,12 +29,12 @@ namespace Goals { } - virtual Goals::TGoalVec decompose() const override; - virtual std::string toString() const override; - virtual bool hasHash() const override { return true; } - virtual uint64_t getHash() const override; + Goals::TGoalVec decompose() const override; + std::string toString() const override; + bool hasHash() const override { return true; } + uint64_t getHash() const override; - virtual bool operator==(const CompleteQuest & other) const override; + bool operator==(const CompleteQuest & other) const override; private: TGoalVec tryCompleteQuest() const; diff --git a/AI/Nullkiller/Goals/Composition.h b/AI/Nullkiller/Goals/Composition.h index ecb4a1ab9..a45d1327a 100644 --- a/AI/Nullkiller/Goals/Composition.h +++ b/AI/Nullkiller/Goals/Composition.h @@ -26,15 +26,15 @@ namespace Goals { } - virtual bool operator==(const Composition & other) const override; - virtual std::string toString() const override; + bool operator==(const Composition & other) const override; + std::string toString() const override; void accept(AIGateway * ai) override; Composition & addNext(const AbstractGoal & goal); Composition & addNext(TSubgoal goal); Composition & addNextSequence(const TGoalVec & taskSequence); - virtual TGoalVec decompose() const override; - virtual bool isElementar() const override; - virtual int getHeroExchangeCount() const override; + TGoalVec decompose() const override; + bool isElementar() const override; + int getHeroExchangeCount() const override; }; } diff --git a/AI/Nullkiller/Goals/DigAtTile.h b/AI/Nullkiller/Goals/DigAtTile.h index b50708c9b..c99d61d0b 100644 --- a/AI/Nullkiller/Goals/DigAtTile.h +++ b/AI/Nullkiller/Goals/DigAtTile.h @@ -33,7 +33,7 @@ namespace Goals { tile = Tile; } - virtual bool operator==(const DigAtTile & other) const override; + bool operator==(const DigAtTile & other) const override; private: //TSubgoal decomposeSingle() const override; diff --git a/AI/Nullkiller/Goals/DismissHero.h b/AI/Nullkiller/Goals/DismissHero.h index a6d180300..98bfdf1fa 100644 --- a/AI/Nullkiller/Goals/DismissHero.h +++ b/AI/Nullkiller/Goals/DismissHero.h @@ -26,7 +26,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const DismissHero & other) const override; + bool operator==(const DismissHero & other) const override; }; } diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h index ca25f280b..8a86c364e 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.h @@ -31,7 +31,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const ExchangeSwapTownHeroes & other) const override; + bool operator==(const ExchangeSwapTownHeroes & other) const override; const CGHeroInstance * getGarrisonHero() const { return garrisonHero; } HeroLockedReason getLockingReason() const { return lockingReason; } diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.h b/AI/Nullkiller/Goals/ExecuteHeroChain.h index b22e18499..a0e0ff39e 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.h +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.h @@ -30,10 +30,10 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const ExecuteHeroChain & other) const override; + bool operator==(const ExecuteHeroChain & other) const override; const AIPath & getPath() const { return chainPath; } - virtual int getHeroExchangeCount() const override { return chainPath.exchangeCount; } + int getHeroExchangeCount() const override { return chainPath.exchangeCount; } private: bool moveHeroToTile(const CGHeroInstance * hero, const int3 & tile); diff --git a/AI/Nullkiller/Goals/GatherArmy.h b/AI/Nullkiller/Goals/GatherArmy.h index 9dbb81ec3..804a159c3 100644 --- a/AI/Nullkiller/Goals/GatherArmy.h +++ b/AI/Nullkiller/Goals/GatherArmy.h @@ -36,7 +36,7 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const GatherArmy & other) const override; + bool operator==(const GatherArmy & other) const override; }; } diff --git a/AI/Nullkiller/Goals/Invalid.h b/AI/Nullkiller/Goals/Invalid.h index 6c2d7afa4..5447b34b4 100644 --- a/AI/Nullkiller/Goals/Invalid.h +++ b/AI/Nullkiller/Goals/Invalid.h @@ -32,17 +32,17 @@ namespace Goals return TGoalVec(); } - virtual bool operator==(const Invalid & other) const override + bool operator==(const Invalid & other) const override { return true; } - virtual std::string toString() const override + std::string toString() const override { return "Invalid"; } - virtual void accept(AIGateway * ai) override + void accept(AIGateway * ai) override { throw cannotFulfillGoalException("Can not fulfill Invalid goal!"); } diff --git a/AI/Nullkiller/Goals/RecruitHero.h b/AI/Nullkiller/Goals/RecruitHero.h index 78c6b0867..101588f19 100644 --- a/AI/Nullkiller/Goals/RecruitHero.h +++ b/AI/Nullkiller/Goals/RecruitHero.h @@ -38,12 +38,12 @@ namespace Goals { } - virtual bool operator==(const RecruitHero & other) const override + bool operator==(const RecruitHero & other) const override { return true; } - virtual std::string toString() const override; + std::string toString() const override; void accept(AIGateway * ai) override; }; } diff --git a/AI/Nullkiller/Goals/SaveResources.h b/AI/Nullkiller/Goals/SaveResources.h index eb0fe3b5b..09438b488 100644 --- a/AI/Nullkiller/Goals/SaveResources.h +++ b/AI/Nullkiller/Goals/SaveResources.h @@ -28,7 +28,7 @@ namespace Goals void accept(AIGateway * ai) override; std::string toString() const override; - virtual bool operator==(const SaveResources & other) const override; + bool operator==(const SaveResources & other) const override; }; } diff --git a/AI/Nullkiller/Goals/StayAtTown.h b/AI/Nullkiller/Goals/StayAtTown.h index 880881386..9d90037b2 100644 --- a/AI/Nullkiller/Goals/StayAtTown.h +++ b/AI/Nullkiller/Goals/StayAtTown.h @@ -26,8 +26,8 @@ namespace Goals public: StayAtTown(const CGTownInstance * town, AIPath & path); - virtual bool operator==(const StayAtTown & other) const override; - virtual std::string toString() const override; + bool operator==(const StayAtTown & other) const override; + std::string toString() const override; void accept(AIGateway * ai) override; float getMovementWasted() const { return movementWasted; } }; diff --git a/AI/Nullkiller/Goals/Trade.h b/AI/Nullkiller/Goals/Trade.h index 4360c6700..f29ed8c8f 100644 --- a/AI/Nullkiller/Goals/Trade.h +++ b/AI/Nullkiller/Goals/Trade.h @@ -34,7 +34,7 @@ namespace Goals value = val; objid = Objid; } - virtual bool operator==(const Trade & other) const override; + bool operator==(const Trade & other) const override; }; } diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.h b/AI/Nullkiller/Markers/ArmyUpgrade.h index 1af41a5bf..5ede01e80 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.h +++ b/AI/Nullkiller/Markers/ArmyUpgrade.h @@ -29,8 +29,8 @@ namespace Goals ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade); - virtual bool operator==(const ArmyUpgrade & other) const override; - virtual std::string toString() const override; + bool operator==(const ArmyUpgrade & other) const override; + std::string toString() const override; uint64_t getUpgradeValue() const { return upgradeValue; } uint64_t getInitialArmyValue() const { return initialValue; } diff --git a/AI/Nullkiller/Markers/DefendTown.h b/AI/Nullkiller/Markers/DefendTown.h index 34b8b3427..f03f84036 100644 --- a/AI/Nullkiller/Markers/DefendTown.h +++ b/AI/Nullkiller/Markers/DefendTown.h @@ -30,8 +30,8 @@ namespace Goals DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false); DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender); - virtual bool operator==(const DefendTown & other) const override; - virtual std::string toString() const override; + bool operator==(const DefendTown & other) const override; + std::string toString() const override; const HitMapInfo & getTreat() const { return treat; } diff --git a/AI/Nullkiller/Markers/HeroExchange.h b/AI/Nullkiller/Markers/HeroExchange.h index 532d62666..95546716d 100644 --- a/AI/Nullkiller/Markers/HeroExchange.h +++ b/AI/Nullkiller/Markers/HeroExchange.h @@ -28,8 +28,8 @@ namespace Goals sethero(targetHero); } - virtual bool operator==(const HeroExchange & other) const override; - virtual std::string toString() const override; + bool operator==(const HeroExchange & other) const override; + std::string toString() const override; uint64_t getReinforcementArmyStrength() const; }; diff --git a/AI/Nullkiller/Markers/UnlockCluster.h b/AI/Nullkiller/Markers/UnlockCluster.h index 3318fc7bc..e504a3318 100644 --- a/AI/Nullkiller/Markers/UnlockCluster.h +++ b/AI/Nullkiller/Markers/UnlockCluster.h @@ -36,8 +36,8 @@ namespace Goals sethero(pathToCenter.targetHero); } - virtual bool operator==(const UnlockCluster & other) const override; - virtual std::string toString() const override; + bool operator==(const UnlockCluster & other) const override; + std::string toString() const override; std::shared_ptr getCluster() const { return cluster; } const AIPath & getPathToCenter() { return pathToCenter; } }; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index f40aa89dd..310f9d867 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -35,6 +35,8 @@ const uint64_t MIN_ARMY_STRENGTH_FOR_CHAIN = 5000; const uint64_t MIN_ARMY_STRENGTH_FOR_NEXT_ACTOR = 1000; const uint64_t CHAIN_MAX_DEPTH = 4; +const bool DO_NOT_SAVE_TO_COMMITED_TILES = false; + AISharedStorage::AISharedStorage(int3 sizes) { if(!shared){ @@ -90,7 +92,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta //TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline const PlayerColor fowPlayer = ai->playerID; - const auto fow = static_cast(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap; + const auto & fow = static_cast(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap; const int3 sizes = gs->getMapSize(); //Each thread gets different x, but an array of y located next to each other in memory @@ -234,6 +236,7 @@ void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, EPath heroNode.specialAction.reset(); heroNode.armyLoss = 0; heroNode.chainOther = nullptr; + heroNode.dayFlags = DayFlags::NONE; heroNode.update(coord, layer, accessibility); } } @@ -265,7 +268,8 @@ void AINodeStorage::commit( EPathNodeAction action, int turn, int movementLeft, - float cost) const + float cost, + bool saveToCommited) const { destination->action = action; destination->setCost(cost); @@ -291,10 +295,15 @@ void AINodeStorage::commit( destination->actor->armyValue); #endif - if(destination->turns <= heroChainTurn) + if(saveToCommited && destination->turns <= heroChainTurn) { commitedTiles.insert(destination->coord); } + + if(destination->turns == source->turns) + { + destination->dayFlags = source->dayFlags; + } } std::vector AINodeStorage::calculateNeighbours( @@ -323,7 +332,7 @@ std::vector AINodeStorage::calculateNeighbours( return neighbours; } -EPathfindingLayer phisycalLayers[2] = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL}; +constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL}; bool AINodeStorage::increaseHeroChainTurnLimit() { @@ -778,7 +787,14 @@ void HeroChainCalculationTask::addHeroChain(const std::vector continue; } - storage.commit(exchangeNode, carrier, carrier->action, chainInfo.turns, chainInfo.moveRemains, chainInfo.getCost()); + storage.commit( + exchangeNode, + carrier, + carrier->action, + chainInfo.turns, + chainInfo.moveRemains, + chainInfo.getCost(), + DO_NOT_SAVE_TO_COMMITED_TILES); if(carrier->specialAction || carrier->chainOther) { @@ -827,6 +843,7 @@ ExchangeCandidate HeroChainCalculationTask::calculateExchange( candidate.turns = carrierParentNode->turns; candidate.setCost(carrierParentNode->getCost() + otherParentNode->getCost() / 1000.0); candidate.moveRemains = carrierParentNode->moveRemains; + candidate.danger = carrierParentNode->danger; if(carrierParentNode->turns < otherParentNode->turns) { @@ -1070,7 +1087,8 @@ struct TowmPortalFinder EPathNodeAction::TELEPORT_NORMAL, bestNode->turns, bestNode->moveRemains - movementNeeded, - movementCost); + movementCost, + DO_NOT_SAVE_TO_COMMITED_TILES); node->theNodeBefore = bestNode; node->addSpecialAction(std::make_shared(targetTown)); @@ -1247,8 +1265,8 @@ bool AINodeStorage::hasBetterChain( && nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength && node.getCost() <= candidateNode->getCost()) { - if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength - && node.getCost() == candidateNode->getCost() + if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateActor->heroFightingStrength) + && vstd::isAlmostEqual(node.getCost(), candidateNode->getCost()) && &node < candidateNode) { continue; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 068304955..08117a5ac 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -24,9 +24,6 @@ namespace NKAI { - const int SCOUT_TURN_DISTANCE_LIMIT = 5; - const int MAIN_TURN_DISTANCE_LIMIT = 10; - namespace AIPathfinding { #ifdef ENVIRONMENT64 @@ -41,11 +38,19 @@ namespace AIPathfinding const int CHAIN_MAX_DEPTH = 4; } +enum DayFlags : ui8 +{ + NONE = 0, + FLY_CAST = 1, + WATER_WALK_CAST = 2 +}; + struct AIPathNode : public CGPathNode { uint64_t danger; uint64_t armyLoss; - int32_t manaCost; + int16_t manaCost; + DayFlags dayFlags; const AIPathNode * chainOther; std::shared_ptr specialAction; const ChainActor * actor; @@ -180,7 +185,7 @@ public: bool selectFirstActor(); bool selectNextActor(); - virtual std::vector getInitialNodes() override; + std::vector getInitialNodes() override; virtual std::vector calculateNeighbours( const PathNodeInfo & source, @@ -192,7 +197,7 @@ public: const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; - virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; + void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; void commit( AIPathNode * destination, @@ -200,7 +205,8 @@ public: EPathNodeAction action, int turn, int movementLeft, - float cost) const; + float cost, + bool saveToCommited = true) const; inline const AIPathNode * getAINode(const CGPathNode * node) const { diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h index 3d229a812..2f242e767 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.h @@ -34,7 +34,7 @@ namespace AIPathfinding ~AIPathfinderConfig(); - virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; + CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; }; } diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp index ff4934b36..4da806986 100644 --- a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp @@ -22,18 +22,18 @@ namespace NKAI namespace AIPathfinding { - AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero) - :spellToCast(spellToCast), hero(hero) + AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd) + :spellToCast(spellToCast), hero(hero), flagsToAdd(flagsToAdd) { manaCost = hero->getSpellCost(spellToCast.toSpell()); } WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero) - :AdventureCastAction(SpellID::WATER_WALK, hero) + :AdventureCastAction(SpellID::WATER_WALK, hero, DayFlags::WATER_WALK_CAST) { } AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero) - : AdventureCastAction(SpellID::FLY, hero) + : AdventureCastAction(SpellID::FLY, hero, DayFlags::FLY_CAST) { } @@ -41,11 +41,12 @@ namespace AIPathfinding const CGHeroInstance * hero, CDestinationNodeInfo & destination, const PathNodeInfo & source, - AIPathNode * dstMode, + AIPathNode * dstNode, const AIPathNode * srcNode) const { - dstMode->manaCost = srcNode->manaCost + manaCost; - dstMode->theNodeBefore = source.node; + dstNode->manaCost = srcNode->manaCost + manaCost; + dstNode->theNodeBefore = source.node; + dstNode->dayFlags = static_cast(dstNode->dayFlags | flagsToAdd); } void AdventureCastAction::execute(const CGHeroInstance * hero) const diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h index 0667e400a..7defcebf0 100644 --- a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h @@ -24,11 +24,12 @@ namespace AIPathfinding SpellID spellToCast; const CGHeroInstance * hero; int manaCost; + DayFlags flagsToAdd; public: - AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero); + AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE); - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual void applyOnDestination( const CGHeroInstance * hero, @@ -37,9 +38,9 @@ namespace AIPathfinding AIPathNode * dstMode, const AIPathNode * srcNode) const override; - virtual bool canAct(const AIPathNode * source) const override; + bool canAct(const AIPathNode * source) const override; - virtual std::string toString() const override; + std::string toString() const override; }; class WaterWalkingAction : public AdventureCastAction diff --git a/AI/Nullkiller/Pathfinding/Actions/BattleAction.h b/AI/Nullkiller/Pathfinding/Actions/BattleAction.h index 595ba23cf..838ba54c2 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BattleAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/BattleAction.h @@ -28,9 +28,9 @@ namespace AIPathfinding { } - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; - virtual std::string toString() const override; + std::string toString() const override; }; } diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h index 5e6ca50d4..2a4dd67de 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h @@ -25,7 +25,7 @@ namespace AIPathfinding class SummonBoatAction : public VirtualBoatAction { public: - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; virtual void applyOnDestination( const CGHeroInstance * hero, @@ -34,11 +34,11 @@ namespace AIPathfinding AIPathNode * dstMode, const AIPathNode * srcNode) const override; - virtual bool canAct(const AIPathNode * source) const override; + bool canAct(const AIPathNode * source) const override; - virtual const ChainActor * getActor(const ChainActor * sourceActor) const override; + const ChainActor * getActor(const ChainActor * sourceActor) const override; - virtual std::string toString() const override; + std::string toString() const override; private: int32_t getManaCost(const CGHeroInstance * hero) const; @@ -56,17 +56,17 @@ namespace AIPathfinding { } - virtual bool canAct(const AIPathNode * source) const override; + bool canAct(const AIPathNode * source) const override; - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; - virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override; + Goals::TSubgoal decompose(const CGHeroInstance * hero) const override; - virtual const ChainActor * getActor(const ChainActor * sourceActor) const override; + const ChainActor * getActor(const ChainActor * sourceActor) const override; - virtual std::string toString() const override; + std::string toString() const override; - virtual const CGObjectInstance * targetObject() const override; + const CGObjectInstance * targetObject() const override; }; } diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h b/AI/Nullkiller/Pathfinding/Actions/QuestAction.h index 15f16a860..52939e5f7 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.h @@ -28,13 +28,13 @@ namespace AIPathfinding { } - virtual bool canAct(const AIPathNode * node) const override; + bool canAct(const AIPathNode * node) const override; - virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override; + Goals::TSubgoal decompose(const CGHeroInstance * hero) const override; - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; - virtual std::string toString() const override; + std::string toString() const override; }; } diff --git a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h index cfb43cc70..05005156b 100644 --- a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h @@ -29,9 +29,9 @@ namespace AIPathfinding { } - virtual void execute(const CGHeroInstance * hero) const override; + void execute(const CGHeroInstance * hero) const override; - virtual std::string toString() const override; + std::string toString() const override; }; } diff --git a/AI/Nullkiller/Pathfinding/Actors.cpp b/AI/Nullkiller/Pathfinding/Actors.cpp index bf176b3ec..71259fa05 100644 --- a/AI/Nullkiller/Pathfinding/Actors.cpp +++ b/AI/Nullkiller/Pathfinding/Actors.cpp @@ -18,7 +18,7 @@ using namespace NKAI; -CCreatureSet emptyArmy; +const CCreatureSet emptyArmy; bool HeroExchangeArmy::needsLastStack() const { @@ -327,7 +327,7 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other) return result; } - HeroActor * exchanged = new HeroActor(actor, other, newArmy, ai); + auto * exchanged = new HeroActor(actor, other, newArmy, ai); exchanged->armyCost += newArmy->armyCost; result.actor = exchanged; @@ -342,7 +342,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade( const CGObjectInstance * upgrader, TResources resources) const { - HeroExchangeArmy * target = new HeroExchangeArmy(); + auto * target = new HeroExchangeArmy(); auto upgradeInfo = ai->armyManager->calculateCreaturesUpgrade(army, upgrader, resources); if(upgradeInfo.upgradeValue) @@ -373,10 +373,10 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade( for(auto & creatureToBuy : buyArmy) { - auto targetSlot = target->getSlotFor(dynamic_cast(creatureToBuy.cre)); + auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature()); target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count); - target->armyCost += creatureToBuy.cre->getFullRecruitCost() * creatureToBuy.count; + target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count; target->requireBuyArmy = true; } } @@ -393,7 +393,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade( HeroExchangeArmy * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1, const CCreatureSet * army2) const { - HeroExchangeArmy * target = new HeroExchangeArmy(); + auto * target = new HeroExchangeArmy(); auto bestArmy = ai->armyManager->getBestArmy(actor->hero, army1, army2); for(auto & slotInfo : bestArmy) @@ -445,7 +445,7 @@ std::string DwellingActor::toString() const CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth) { - CCreatureSet * dwellingCreatures = new CCreatureSet(); + auto * dwellingCreatures = new CCreatureSet(); for(auto & creatureInfo : dwelling->creatures) { diff --git a/AI/Nullkiller/Pathfinding/Actors.h b/AI/Nullkiller/Pathfinding/Actors.h index eef4bee39..4451bda24 100644 --- a/AI/Nullkiller/Pathfinding/Actors.h +++ b/AI/Nullkiller/Pathfinding/Actors.h @@ -28,10 +28,10 @@ class HeroExchangeArmy : public CArmedInstance public: TResources armyCost; bool requireBuyArmy; - virtual bool needsLastStack() const override; + bool needsLastStack() const override; std::shared_ptr getActorAction() const; - HeroExchangeArmy(): CArmedInstance(true), requireBuyArmy(false) {} + HeroExchangeArmy(): CArmedInstance(nullptr, true), requireBuyArmy(false) {} }; struct ExchangeResult @@ -51,24 +51,24 @@ protected: public: uint64_t chainMask; - bool isMovable; - bool allowUseResources; - bool allowBattle; - bool allowSpellCast; + bool isMovable = false; + bool allowUseResources = false; + bool allowBattle = false; + bool allowSpellCast = false; std::shared_ptr actorAction; const CGHeroInstance * hero; HeroRole heroRole; - const CCreatureSet * creatureSet; - const ChainActor * battleActor; - const ChainActor * castActor; - const ChainActor * resourceActor; - const ChainActor * carrierParent; - const ChainActor * otherParent; - const ChainActor * baseActor; + const CCreatureSet * creatureSet = nullptr; + const ChainActor * battleActor = nullptr; + const ChainActor * castActor = nullptr; + const ChainActor * resourceActor = nullptr; + const ChainActor * carrierParent = nullptr; + const ChainActor * otherParent = nullptr; + const ChainActor * baseActor = nullptr; int3 initialPosition; EPathfindingLayer layer; - uint32_t initialMovement; - uint32_t initialTurn; + uint32_t initialMovement = 0; + uint32_t initialTurn = 0; uint64_t armyValue; float heroFightingStrength; uint8_t actorExchangeCount; @@ -126,7 +126,7 @@ public: HeroActor(const ChainActor * carrier, const ChainActor * other, const HeroExchangeArmy * army, const Nullkiller * ai); protected: - virtual ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override; + ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override; }; class ObjectActor : public ChainActor @@ -136,7 +136,7 @@ private: public: ObjectActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn); - virtual std::string toString() const override; + std::string toString() const override; const CGObjectInstance * getActorObject() const override; }; @@ -154,7 +154,7 @@ private: public: DwellingActor(const CGDwelling * dwelling, uint64_t chainMask, bool waitForGrowth, int dayOfWeek); ~DwellingActor(); - virtual std::string toString() const override; + std::string toString() const override; protected: int getInitialTurn(bool waitForGrowth, int dayOfWeek); @@ -168,7 +168,7 @@ private: public: TownGarrisonActor(const CGTownInstance * town, uint64_t chainMask); - virtual std::string toString() const override; + std::string toString() const override; }; } diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index 241a1d9b6..d71aa9cb0 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -61,6 +61,12 @@ namespace AIPathfinding if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER) { + if(nodeStorage->getAINode(source.node)->dayFlags & DayFlags::WATER_WALK_CAST) + { + destination.blocked = false; + return; + } + auto action = waterWalkingActions.find(nodeStorage->getHero(source.node)); if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) @@ -73,6 +79,12 @@ namespace AIPathfinding if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR) { + if(nodeStorage->getAINode(source.node)->dayFlags & DayFlags::FLY_CAST) + { + destination.blocked = false; + return; + } + auto action = airWalkingActions.find(nodeStorage->getHero(source.node)); if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) @@ -91,12 +103,12 @@ namespace AIPathfinding for(const CGHeroInstance * hero : nodeStorage->getAllHeroes()) { - if(hero->canCastThisSpell(waterWalk.toSpell())) + if(hero->canCastThisSpell(waterWalk.toSpell()) && hero->mana >= hero->getSpellCost(waterWalk.toSpell())) { waterWalkingActions[hero] = std::make_shared(hero); } - if(hero->canCastThisSpell(airWalk.toSpell())) + if(hero->canCastThisSpell(airWalk.toSpell()) && hero->mana >= hero->getSpellCost(airWalk.toSpell())) { airWalkingActions[hero] = std::make_shared(hero); } @@ -119,7 +131,7 @@ namespace AIPathfinding { if(obj->ID != Obj::TOWN) //towns were handled in the previous loop { - if(const IShipyard * shipyard = IShipyard::castFrom(obj)) + if(const auto * shipyard = dynamic_cast(obj)) shipyards.push_back(shipyard); } } @@ -179,11 +191,6 @@ namespace AIPathfinding { bool result = false; - if(!specialAction->canAct(nodeStorage->getAINode(source.node))) - { - return false; - } - nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) { auto castNodeOptional = nodeStorage->getOrCreateNode( diff --git a/AI/Nullkiller/main.cpp b/AI/Nullkiller/main.cpp index fff944c8d..695d000b3 100644 --- a/AI/Nullkiller/main.cpp +++ b/AI/Nullkiller/main.cpp @@ -14,7 +14,7 @@ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif -static const char * g_cszAiName = "Nullkiller"; +static const char * const g_cszAiName = "Nullkiller"; extern "C" DLL_EXPORT int GetGlobalAiVersion() { diff --git a/AI/StupidAI/CMakeLists.txt b/AI/StupidAI/CMakeLists.txt index e5cab7296..013364cdf 100644 --- a/AI/StupidAI/CMakeLists.txt +++ b/AI/StupidAI/CMakeLists.txt @@ -8,19 +8,19 @@ set(stupidAI_HEADERS StupidAI.h ) -if(NOT ENABLE_STATIC_AI_LIBS) +if(NOT ENABLE_STATIC_LIBS) list(APPEND stupidAI_SRCS main.cpp StdInc.cpp) endif() assign_source_group(${stupidAI_SRCS} ${stupidAI_HEADERS}) -if(ENABLE_STATIC_AI_LIBS) +if(ENABLE_STATIC_LIBS) add_library(StupidAI STATIC ${stupidAI_SRCS} ${stupidAI_HEADERS}) else() add_library(StupidAI SHARED ${stupidAI_SRCS} ${stupidAI_HEADERS}) install(TARGETS StupidAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR}) endif() -target_link_libraries(StupidAI PRIVATE ${VCMI_LIB_TARGET}) +target_link_libraries(StupidAI PRIVATE vcmi) target_include_directories(StupidAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) vcmi_set_output_dir(StupidAI "AI") diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 3888e5a6b..d2486507c 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -15,8 +15,7 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleInfo.h" - -static std::shared_ptr cbc; +#include "../../lib/CRandomGenerator.h" CStupidAI::CStupidAI() : side(-1) @@ -41,7 +40,7 @@ void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::share { print("init called, saving ptr to IBattleCallback"); env = ENV; - cbc = cb = CB; + cb = CB; wasWaitingForRealize = CB->waitTillRealize; wasUnlockingGs = CB->unlockGsWhenWaiting; @@ -68,15 +67,16 @@ class EnemyInfo { public: const CStack * s; - int adi, adr; + int adi; + int adr; std::vector attackFrom; //for melee fight EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) {} - void calcDmg(const BattleID & battleID, const CStack * ourStack) + void calcDmg(std::shared_ptr cb, const BattleID & battleID, const CStack * ourStack) { // FIXME: provide distance info for Jousting bonus DamageEstimation retal; - DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal); + DamageEstimation dmg = cb->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal); adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); adr = static_cast((retal.damage.min + retal.damage.max) / 2); } @@ -92,14 +92,14 @@ bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2) return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); } -static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2) +static bool willSecondHexBlockMoreEnemyShooters(std::shared_ptr cb, const BattleID & battleID, const BattleHex &h1, const BattleHex &h2) { int shooters[2] = {0}; //count of shooters on hexes for(int i = 0; i < 2; i++) { for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) - if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour)) + if(const auto * s = cb->getBattle(battleID)->battleGetUnitByPos(neighbour)) if(s->isShooter()) shooters[i]++; } @@ -117,7 +117,9 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) //boost::this_thread::sleep_for(boost::chrono::seconds(2)); print("activeStack called for " + stack->nodeName()); ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack); - std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; + std::vector enemiesShootable; + std::vector enemiesReachable; + std::vector enemiesUnreachable; if(stack->creatureId() == CreatureID::CATAPULT) { @@ -152,7 +154,7 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) { if(CStack::isMeleeAttackPossible(stack, s, hex)) { - std::vector::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); + auto i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); if(i == enemiesReachable.end()) { enemiesReachable.push_back(s); @@ -169,10 +171,10 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) } for ( auto & enemy : enemiesReachable ) - enemy.calcDmg(battleID, stack); + enemy.calcDmg(cb, battleID, stack); for ( auto & enemy : enemiesShootable ) - enemy.calcDmg(battleID, stack); + enemy.calcDmg(cb, battleID, stack); if(enemiesShootable.size()) { @@ -183,7 +185,7 @@ void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) else if(enemiesReachable.size()) { const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); - BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);}); + BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(cb, battleID, a, b);}); cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex)); return; diff --git a/AI/StupidAI/main.cpp b/AI/StupidAI/main.cpp index 93f2ff517..5cf31e497 100644 --- a/AI/StupidAI/main.cpp +++ b/AI/StupidAI/main.cpp @@ -16,7 +16,7 @@ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif -static const char *g_cszAiName = "Stupid AI 0.1"; +static const char * const g_cszAiName = "Stupid AI 0.1"; extern "C" DLL_EXPORT int GetGlobalAiVersion() { diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index 64cf46d28..37a007b9a 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -26,7 +26,6 @@ using crint3 = const int3 &; using crstring = const std::string &; using dwellingContent = std::pair>; -const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; const int ALLOWED_ROAMING_HEROES = 8; @@ -71,7 +70,7 @@ public: bool validAndSet() const; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & this->h; h & hid; @@ -103,7 +102,7 @@ struct ObjectIdRef bool operator<(const ObjectIdRef & rhs) const; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; } diff --git a/AI/VCAI/ArmyManager.cpp b/AI/VCAI/ArmyManager.cpp index d639c125e..72ab24d6b 100644 --- a/AI/VCAI/ArmyManager.cpp +++ b/AI/VCAI/ArmyManager.cpp @@ -63,7 +63,7 @@ std::vector::iterator ArmyManager::getWeakestCreature(std::vectorgetLevel() != right.creature->getLevel()) return left.creature->getLevel() < right.creature->getLevel(); - return left.creature->speed() > right.creature->speed(); + return left.creature->getMovementRange() > right.creature->getMovementRange(); }); return weakest; diff --git a/AI/VCAI/BuildingManager.cpp b/AI/VCAI/BuildingManager.cpp index f5529777f..f791a594f 100644 --- a/AI/VCAI/BuildingManager.cpp +++ b/AI/VCAI/BuildingManager.cpp @@ -139,17 +139,17 @@ void BuildingManager::setAI(VCAI * AI) //Set of buildings for different goals. Does not include any prerequisites. static const std::vector essential = { BuildingID::TAVERN, BuildingID::TOWN_HALL }; static const std::vector basicGoldSource = { BuildingID::TOWN_HALL, BuildingID::CITY_HALL }; +static const std::vector defence = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE }; static const std::vector capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL }; static const std::vector unitsSource = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 }; static const std::vector unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP }; -static const std::vector unitGrowth = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1, -BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR }; +static const std::vector unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR }; static const std::vector _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 }; -static const std::vector extra = { BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, -BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings +static const std::vector extra = { BuildingID::MARKETPLACE, BuildingID::BLACKSMITH, BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, +BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings bool BuildingManager::getBuildingOptions(const CGTownInstance * t) { @@ -172,8 +172,26 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t) if(tryBuildAnyStructure(t, essential)) return true; - //the more gold the better and less problems later //TODO: what about building mage guild / marketplace etc. with city hall disabled in editor? - if(tryBuildNextStructure(t, basicGoldSource)) + if (cb->getDate(Date::DAY_OF_WEEK) < 5) // first 4 days of week - try to focus on dwellings + { + if (tryBuildNextStructure(t, unitsSource, 4)) + return true; + } + + if (cb->getDate(Date::DAY_OF_WEEK) > 4) // last 3 days of week - try to focus on growth by building Fort/Citadel/Castle + { + if (tryBuildNextStructure(t, defence, 3)) + return true; + } + + if (t->hasBuilt(BuildingID::CASTLE)) + { + if (tryBuildAnyStructure(t, unitGrowth)) + return true; + } + + //try to make City Hall + if (tryBuildNextStructure(t, basicGoldSource)) return true; //workaround for mantis #2696 - build capitol with separate algorithm if it is available @@ -183,25 +201,6 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t) return true; } - if(!t->hasBuilt(BuildingID::FORT)) //in vast majority of situations fort is top priority building if we already have city hall, TODO: unite with unitGrowth building chain - if(tryBuildThisStructure(t, BuildingID::FORT)) - return true; - - - - if (cb->getDate(Date::DAY_OF_WEEK) > 6) // last 2 days of week - try to focus on growth - { - if (tryBuildNextStructure(t, unitGrowth, 2)) - return true; - } - - //try building dwellings - if (t->hasBuilt(BuildingID::FORT)) - { - if (tryBuildAnyStructure(t, unitsSource, 8 - cb->getDate(Date::DAY_OF_WEEK))) - return true; - } - //try to upgrade dwelling for (int i = 0; i < unitsUpgrade.size(); i++) { diff --git a/AI/VCAI/CMakeLists.txt b/AI/VCAI/CMakeLists.txt index c2400cc86..29c621947 100644 --- a/AI/VCAI/CMakeLists.txt +++ b/AI/VCAI/CMakeLists.txt @@ -94,12 +94,12 @@ set(VCAI_HEADERS VCAI.h ) -if(NOT ENABLE_STATIC_AI_LIBS) +if(NOT ENABLE_STATIC_LIBS) list(APPEND VCAI_SRCS main.cpp StdInc.cpp) endif() assign_source_group(${VCAI_SRCS} ${VCAI_HEADERS}) -if(ENABLE_STATIC_AI_LIBS) +if(ENABLE_STATIC_LIBS) add_library(VCAI STATIC ${VCAI_SRCS} ${VCAI_HEADERS}) else() add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS}) @@ -107,7 +107,7 @@ else() endif() target_include_directories(VCAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(VCAI PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite) +target_link_libraries(VCAI PUBLIC vcmi fuzzylite::fuzzylite) vcmi_set_output_dir(VCAI "AI") enable_pch(VCAI) diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp index 91ba40139..95c3dc4a9 100644 --- a/AI/VCAI/FuzzyEngines.cpp +++ b/AI/VCAI/FuzzyEngines.cpp @@ -37,7 +37,9 @@ void engineBase::addRule(const std::string & txt) struct armyStructure { - float walkers, shooters, flyers; + float walkers; + float shooters; + float flyers; ui32 maxSpeed; }; @@ -406,7 +408,7 @@ float VisitObjEngine::evaluate(Goals::VisitObj & goal) else { MapObjectsEvaluator::getInstance().addObjectData(obj->ID, obj->subID, 0); - logGlobal->error("AI met object type it doesn't know - ID: " + std::to_string(obj->ID) + ", subID: " + std::to_string(obj->subID) + " - adding to database with value " + std::to_string(objValue)); + logGlobal->error("AI met object type it doesn't know - ID: %d, subID: %d - adding to database with value %d ", obj->ID, obj->subID, objValue); } setSharedFuzzyVariables(goal); diff --git a/AI/VCAI/FuzzyEngines.h b/AI/VCAI/FuzzyEngines.h index 63e8279bc..6febb69e4 100644 --- a/AI/VCAI/FuzzyEngines.h +++ b/AI/VCAI/FuzzyEngines.h @@ -38,8 +38,14 @@ public: TacticalAdvantageEngine(); float getTacticalAdvantage(const CArmedInstance * we, const CArmedInstance * enemy); //returns factor how many times enemy is stronger than us private: - fl::InputVariable * ourWalkers, *ourShooters, *ourFlyers, *enemyWalkers, *enemyShooters, *enemyFlyers; - fl::InputVariable * ourSpeed, *enemySpeed; + fl::InputVariable * ourWalkers; + fl::InputVariable * ourShooters; + fl::InputVariable * ourFlyers; + fl::InputVariable * enemyWalkers; + fl::InputVariable * enemyShooters; + fl::InputVariable * enemyFlyers; + fl::InputVariable * ourSpeed; + fl::InputVariable * enemySpeed; fl::InputVariable * bankPresent; fl::InputVariable * castleWalls; fl::OutputVariable * threat; diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index c125e2315..776c3b98f 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -66,13 +66,13 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) { //this one is not fuzzy anymore, just calculate weighted average - auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance); + auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); ui64 totalStrength = 0; ui8 totalChance = 0; - for(auto config : bankInfo->getPossibleGuards()) + for(auto config : bankInfo->getPossibleGuards(bank->cb)) { totalStrength += config.second.totalStrength * config.first; totalChance += config.first; @@ -324,15 +324,8 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship - // case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); case Obj::PYRAMID: - { - if(obj->subID == 0) - return estimateBankDanger(dynamic_cast(obj)); - else - return 0; - } + return estimateBankDanger(dynamic_cast(obj)); default: return 0; } diff --git a/AI/VCAI/Goals/AbstractGoal.cpp b/AI/VCAI/Goals/AbstractGoal.cpp index 3ed71f08d..301f7bdf3 100644 --- a/AI/VCAI/Goals/AbstractGoal.cpp +++ b/AI/VCAI/Goals/AbstractGoal.cpp @@ -109,51 +109,52 @@ bool AbstractGoal::operator==(const AbstractGoal & g) const return false; } -bool AbstractGoal::operator<(AbstractGoal & g) //for std::unique -{ - //TODO: make sure it gets goals consistent with == operator - if (goalType < g.goalType) - return true; - if (goalType > g.goalType) - return false; - if (hero < g.hero) - return true; - if (hero > g.hero) - return false; - if (tile < g.tile) - return true; - if (g.tile < tile) - return false; - if (objid < g.objid) - return true; - if (objid > g.objid) - return false; - if (town < g.town) - return true; - if (town > g.town) - return false; - if (value < g.value) - return true; - if (value > g.value) - return false; - if (priority < g.priority) - return true; - if (priority > g.priority) - return false; - if (resID < g.resID) - return true; - if (resID > g.resID) - return false; - if (bid < g.bid) - return true; - if (bid > g.bid) - return false; - if (aid < g.aid) - return true; - if (aid > g.aid) - return false; - return false; -} +// FIXME: unused code? +//bool AbstractGoal::operator<(AbstractGoal & g) //for std::unique +//{ +// //TODO: make sure it gets goals consistent with == operator +// if (goalType < g.goalType) +// return true; +// if (goalType > g.goalType) +// return false; +// if (hero < g.hero) +// return true; +// if (hero > g.hero) +// return false; +// if (tile < g.tile) +// return true; +// if (g.tile < tile) +// return false; +// if (objid < g.objid) +// return true; +// if (objid > g.objid) +// return false; +// if (town < g.town) +// return true; +// if (town > g.town) +// return false; +// if (value < g.value) +// return true; +// if (value > g.value) +// return false; +// if (priority < g.priority) +// return true; +// if (priority > g.priority) +// return false; +// if (resID < g.resID) +// return true; +// if (resID > g.resID) +// return false; +// if (bid < g.bid) +// return true; +// if (bid > g.bid) +// return false; +// if (aid < g.aid) +// return true; +// if (aid > g.aid) +// return false; +// return false; +//} //TODO: find out why the following are not generated automatically on MVS? bool TSubgoal::operator==(const TSubgoal & rhs) const diff --git a/AI/VCAI/Goals/AbstractGoal.h b/AI/VCAI/Goals/AbstractGoal.h index ed4fc9430..5dc130682 100644 --- a/AI/VCAI/Goals/AbstractGoal.h +++ b/AI/VCAI/Goals/AbstractGoal.h @@ -165,18 +165,18 @@ namespace Goals virtual float accept(FuzzyHelper * f); virtual bool operator==(const AbstractGoal & g) const; - bool operator<(AbstractGoal & g); //final +// bool operator<(AbstractGoal & g); //final virtual bool fulfillsMe(Goals::TSubgoal goal) //TODO: multimethod instead of type check { return false; //use this method to check if goal is fulfilled by another (not equal) goal, operator == is handled spearately } - bool operator!=(const AbstractGoal & g) const - { - return !(*this == g); - } +// bool operator!=(const AbstractGoal & g) const +// { +// return !(*this == g); +// } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & goalType; h & isElementar; diff --git a/AI/VCAI/Goals/AdventureSpellCast.h b/AI/VCAI/Goals/AdventureSpellCast.h index 28ded9dd1..c8e38004e 100644 --- a/AI/VCAI/Goals/AdventureSpellCast.h +++ b/AI/VCAI/Goals/AdventureSpellCast.h @@ -39,6 +39,6 @@ namespace Goals void accept(VCAI * ai) override; std::string name() const override; std::string completeMessage() const override; - virtual bool operator==(const AdventureSpellCast & other) const override; + bool operator==(const AdventureSpellCast & other) const override; }; } diff --git a/AI/VCAI/Goals/Build.h b/AI/VCAI/Goals/Build.h index c6d972d9f..dc552e146 100644 --- a/AI/VCAI/Goals/Build.h +++ b/AI/VCAI/Goals/Build.h @@ -29,7 +29,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const Build & other) const override + bool operator==(const Build & other) const override { return true; } diff --git a/AI/VCAI/Goals/BuildBoat.h b/AI/VCAI/Goals/BuildBoat.h index 367fa6ea9..8a707d7e9 100644 --- a/AI/VCAI/Goals/BuildBoat.h +++ b/AI/VCAI/Goals/BuildBoat.h @@ -32,6 +32,6 @@ namespace Goals void accept(VCAI * ai) override; std::string name() const override; std::string completeMessage() const override; - virtual bool operator==(const BuildBoat & other) const override; + bool operator==(const BuildBoat & other) const override; }; } diff --git a/AI/VCAI/Goals/BuildThis.h b/AI/VCAI/Goals/BuildThis.h index 9cc86abb8..8abdac811 100644 --- a/AI/VCAI/Goals/BuildThis.h +++ b/AI/VCAI/Goals/BuildThis.h @@ -43,6 +43,6 @@ namespace Goals } TSubgoal whatToDoToAchieve() override; //bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const BuildThis & other) const override; + bool operator==(const BuildThis & other) const override; }; } diff --git a/AI/VCAI/Goals/BuyArmy.h b/AI/VCAI/Goals/BuyArmy.h index 55dc97ec4..fbd8126a9 100644 --- a/AI/VCAI/Goals/BuyArmy.h +++ b/AI/VCAI/Goals/BuyArmy.h @@ -36,6 +36,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const BuyArmy & other) const override; + bool operator==(const BuyArmy & other) const override; }; } diff --git a/AI/VCAI/Goals/CGoal.h b/AI/VCAI/Goals/CGoal.h index 6a8876cbf..f2ff5749c 100644 --- a/AI/VCAI/Goals/CGoal.h +++ b/AI/VCAI/Goals/CGoal.h @@ -69,14 +69,14 @@ namespace Goals return ptr; } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); //h & goalType & isElementar & isAbstract & priority; //h & value & resID & objid & aid & tile & hero & town & bid; } - virtual bool operator==(const AbstractGoal & g) const override + bool operator==(const AbstractGoal & g) const override { if(goalType != g.goalType) return false; diff --git a/AI/VCAI/Goals/ClearWayTo.h b/AI/VCAI/Goals/ClearWayTo.h index 432f1ad79..da812db44 100644 --- a/AI/VCAI/Goals/ClearWayTo.h +++ b/AI/VCAI/Goals/ClearWayTo.h @@ -40,6 +40,6 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const ClearWayTo & other) const override; + bool operator==(const ClearWayTo & other) const override; }; } diff --git a/AI/VCAI/Goals/CollectRes.cpp b/AI/VCAI/Goals/CollectRes.cpp index 27e168fa6..cb780c5ac 100644 --- a/AI/VCAI/Goals/CollectRes.cpp +++ b/AI/VCAI/Goals/CollectRes.cpp @@ -43,10 +43,10 @@ TGoalVec CollectRes::getAllPossibleSubgoals() return resID == GameResID(EGameResID::GOLD); break; case Obj::RESOURCE: - return obj->subID == resID; + return dynamic_cast(obj)->resourceID() == GameResID(resID); break; case Obj::MINE: - return (obj->subID == resID && + return (dynamic_cast(obj)->producedResource == GameResID(resID) && (cb->getPlayerRelations(obj->tempOwner, ai->playerID) == PlayerRelations::ENEMIES)); //don't capture our mines break; case Obj::CAMPFIRE: @@ -124,7 +124,9 @@ TSubgoal CollectRes::whatToDoToTrade() ai->retrieveVisitableObjs(visObjs, true); for(const CGObjectInstance * obj : visObjs) { - if(const IMarket * m = IMarket::castFrom(obj, false); m && m->allowsTrade(EMarketMode::RESOURCE_RESOURCE)) + const auto * m = dynamic_cast(obj); + + if(m && m->allowsTrade(EMarketMode::RESOURCE_RESOURCE)) { if(obj->ID == Obj::TOWN) { @@ -169,7 +171,8 @@ TSubgoal CollectRes::whatToDoToTrade() { if (i.getNum() == resID) continue; - int toGive = -1, toReceive = -1; + int toGive = -1; + int toReceive = -1; m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); assert(toGive > 0 && toReceive > 0); howManyCanWeBuy += toReceive * (ai->ah->freeResources()[i] / toGive); diff --git a/AI/VCAI/Goals/CollectRes.h b/AI/VCAI/Goals/CollectRes.h index 70d76d8cc..8a55cae89 100644 --- a/AI/VCAI/Goals/CollectRes.h +++ b/AI/VCAI/Goals/CollectRes.h @@ -35,6 +35,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; TSubgoal whatToDoToTrade(); bool fulfillsMe(TSubgoal goal) override; //TODO: Trade - virtual bool operator==(const CollectRes & other) const override; + bool operator==(const CollectRes & other) const override; }; } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index cb78ee8db..f92560114 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -103,7 +103,7 @@ std::string CompleteQuest::questToString() const return "inactive quest"; MetaString ms; - q.quest->getRolloverText(ms, false); + q.quest->getRolloverText(q.obj->cb, ms, false); return ms.toString(); } @@ -241,7 +241,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const { TGoalVec solutions; - auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); + auto obj = cb->getObj(q.quest->killTarget); if(!obj) return ai->ah->howToVisitObj(q.obj); diff --git a/AI/VCAI/Goals/CompleteQuest.h b/AI/VCAI/Goals/CompleteQuest.h index 16335d278..f50cbfe8f 100644 --- a/AI/VCAI/Goals/CompleteQuest.h +++ b/AI/VCAI/Goals/CompleteQuest.h @@ -30,7 +30,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; std::string name() const override; std::string completeMessage() const override; - virtual bool operator==(const CompleteQuest & other) const override; + bool operator==(const CompleteQuest & other) const override; private: TGoalVec tryCompleteQuest() const; diff --git a/AI/VCAI/Goals/Conquer.h b/AI/VCAI/Goals/Conquer.h index 295930347..ec274d15b 100644 --- a/AI/VCAI/Goals/Conquer.h +++ b/AI/VCAI/Goals/Conquer.h @@ -27,6 +27,6 @@ namespace Goals } TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const Conquer & other) const override; + bool operator==(const Conquer & other) const override; }; } diff --git a/AI/VCAI/Goals/DigAtTile.h b/AI/VCAI/Goals/DigAtTile.h index 963306539..9e426d241 100644 --- a/AI/VCAI/Goals/DigAtTile.h +++ b/AI/VCAI/Goals/DigAtTile.h @@ -36,6 +36,6 @@ namespace Goals return TGoalVec(); } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const DigAtTile & other) const override; + bool operator==(const DigAtTile & other) const override; }; } diff --git a/AI/VCAI/Goals/Explore.h b/AI/VCAI/Goals/Explore.h index 54e55dcae..a96de7f29 100644 --- a/AI/VCAI/Goals/Explore.h +++ b/AI/VCAI/Goals/Explore.h @@ -46,7 +46,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const Explore & other) const override; + bool operator==(const Explore & other) const override; private: TSubgoal exploreNearestNeighbour(HeroPtr h) const; diff --git a/AI/VCAI/Goals/FindObj.h b/AI/VCAI/Goals/FindObj.h index fad944dec..c1cc74fe7 100644 --- a/AI/VCAI/Goals/FindObj.h +++ b/AI/VCAI/Goals/FindObj.h @@ -42,6 +42,6 @@ namespace Goals } TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const FindObj & other) const override; + bool operator==(const FindObj & other) const override; }; } \ No newline at end of file diff --git a/AI/VCAI/Goals/GatherArmy.h b/AI/VCAI/Goals/GatherArmy.h index 97108df76..213f50dc5 100644 --- a/AI/VCAI/Goals/GatherArmy.h +++ b/AI/VCAI/Goals/GatherArmy.h @@ -33,6 +33,6 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const GatherArmy & other) const override; + bool operator==(const GatherArmy & other) const override; }; } diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index 4af9e7434..ed4e1a6c0 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -88,7 +88,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals() } auto creature = VLC->creatures()->getByIndex(objid); - if(t->subID == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O + if(t->getFaction() == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O { auto tryFindCreature = [&]() -> std::optional> { diff --git a/AI/VCAI/Goals/GatherTroops.h b/AI/VCAI/Goals/GatherTroops.h index ff93ca186..5d5553f93 100644 --- a/AI/VCAI/Goals/GatherTroops.h +++ b/AI/VCAI/Goals/GatherTroops.h @@ -35,7 +35,7 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; - virtual bool operator==(const GatherTroops & other) const override; + bool operator==(const GatherTroops & other) const override; private: int getCreaturesCount(const CArmedInstance * army); diff --git a/AI/VCAI/Goals/GetArtOfType.h b/AI/VCAI/Goals/GetArtOfType.h index ba31c2a56..4a69cdb82 100644 --- a/AI/VCAI/Goals/GetArtOfType.h +++ b/AI/VCAI/Goals/GetArtOfType.h @@ -35,6 +35,6 @@ namespace Goals return TGoalVec(); } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const GetArtOfType & other) const override; + bool operator==(const GetArtOfType & other) const override; }; } diff --git a/AI/VCAI/Goals/Invalid.h b/AI/VCAI/Goals/Invalid.h index cd77ac929..044acfd6c 100644 --- a/AI/VCAI/Goals/Invalid.h +++ b/AI/VCAI/Goals/Invalid.h @@ -33,7 +33,7 @@ namespace Goals return iAmElementar(); } - virtual bool operator==(const Invalid & other) const override + bool operator==(const Invalid & other) const override { return true; } diff --git a/AI/VCAI/Goals/RecruitHero.h b/AI/VCAI/Goals/RecruitHero.h index c381eb2ad..253102a2f 100644 --- a/AI/VCAI/Goals/RecruitHero.h +++ b/AI/VCAI/Goals/RecruitHero.h @@ -33,7 +33,7 @@ namespace Goals TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const RecruitHero & other) const override + bool operator==(const RecruitHero & other) const override { return true; } diff --git a/AI/VCAI/Goals/Trade.h b/AI/VCAI/Goals/Trade.h index c8f64416f..f5bced17f 100644 --- a/AI/VCAI/Goals/Trade.h +++ b/AI/VCAI/Goals/Trade.h @@ -33,6 +33,6 @@ namespace Goals priority = 3; //trading is instant, but picking resources is free } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const Trade & other) const override; + bool operator==(const Trade & other) const override; }; } diff --git a/AI/VCAI/Goals/VisitHero.h b/AI/VCAI/Goals/VisitHero.h index c209a0abf..bfaa6ffd7 100644 --- a/AI/VCAI/Goals/VisitHero.h +++ b/AI/VCAI/Goals/VisitHero.h @@ -37,6 +37,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; std::string completeMessage() const override; - virtual bool operator==(const VisitHero & other) const override; + bool operator==(const VisitHero & other) const override; }; } diff --git a/AI/VCAI/Goals/VisitObj.h b/AI/VCAI/Goals/VisitObj.h index 666a9acf0..b92bb4378 100644 --- a/AI/VCAI/Goals/VisitObj.h +++ b/AI/VCAI/Goals/VisitObj.h @@ -27,6 +27,6 @@ namespace Goals TSubgoal whatToDoToAchieve() override; bool fulfillsMe(TSubgoal goal) override; std::string completeMessage() const override; - virtual bool operator==(const VisitObj & other) const override; + bool operator==(const VisitObj & other) const override; }; } diff --git a/AI/VCAI/Goals/VisitTile.h b/AI/VCAI/Goals/VisitTile.h index 334b44c32..50faebd4e 100644 --- a/AI/VCAI/Goals/VisitTile.h +++ b/AI/VCAI/Goals/VisitTile.h @@ -32,6 +32,6 @@ namespace Goals TGoalVec getAllPossibleSubgoals() override; TSubgoal whatToDoToAchieve() override; std::string completeMessage() const override; - virtual bool operator==(const VisitTile & other) const override; + bool operator==(const VisitTile & other) const override; }; } diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index 2ad9abdf8..d2ab613e3 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -51,18 +51,18 @@ TSubgoal Win::whatToDoToAchieve() switch(goal.condition) { case EventCondition::HAVE_ARTIFACT: - return sptr(GetArtOfType(goal.objectType)); + return sptr(GetArtOfType(goal.objectType.as())); case EventCondition::DESTROY: { - if(goal.object) + if(goal.objectID != ObjectInstanceID::NONE) { - auto obj = cb->getObj(goal.object->id); + auto obj = cb->getObj(goal.objectID); if(obj) if(obj->getOwner() == ai->playerID) //we can't capture our own object return sptr(Conquer()); - return sptr(VisitObj(goal.object->id.getNum())); + return sptr(VisitObj(goal.objectID.getNum())); } else { @@ -78,7 +78,7 @@ TSubgoal Win::whatToDoToAchieve() // goal.object = optional, town in which building should be built // Represents "Improve town" condition from H3 (but unlike H3 it consists from 2 separate conditions) - if(goal.objectType == BuildingID::GRAIL) + if(goal.objectType.as() == BuildingID::GRAIL) { if(auto h = ai->getHeroWithGrail()) { @@ -124,13 +124,13 @@ TSubgoal Win::whatToDoToAchieve() } case EventCondition::CONTROL: { - if(goal.object) + if(goal.objectID != ObjectInstanceID::NONE) { - auto objRelations = cb->getPlayerRelations(ai->playerID, goal.object->tempOwner); + auto obj = cb->getObj(goal.objectID); - if(objRelations == PlayerRelations::ENEMIES) + if(obj && cb->getPlayerRelations(ai->playerID, obj->tempOwner) == PlayerRelations::ENEMIES) { - return sptr(VisitObj(goal.object->id.getNum())); + return sptr(VisitObj(goal.objectID.getNum())); } else { @@ -149,9 +149,9 @@ TSubgoal Win::whatToDoToAchieve() case EventCondition::HAVE_RESOURCES: //TODO mines? piles? marketplace? //save? - return sptr(CollectRes(static_cast(goal.objectType), goal.value)); + return sptr(CollectRes(goal.objectType.as(), goal.value)); case EventCondition::HAVE_CREATURES: - return sptr(GatherTroops(goal.objectType, goal.value)); + return sptr(GatherTroops(goal.objectType.as(), goal.value)); case EventCondition::TRANSPORT: { //TODO. merge with bring Grail to town? So AI will first dig grail, then transport it using this goal and builds it @@ -173,11 +173,6 @@ TSubgoal Win::whatToDoToAchieve() case EventCondition::CONST_VALUE: break; - case EventCondition::HAVE_0: - case EventCondition::HAVE_BUILDING_0: - case EventCondition::DESTROY_0: - //TODO: support new condition format - return sptr(Conquer()); default: assert(0); } diff --git a/AI/VCAI/Goals/Win.h b/AI/VCAI/Goals/Win.h index 07dfcdee5..e4edaac7b 100644 --- a/AI/VCAI/Goals/Win.h +++ b/AI/VCAI/Goals/Win.h @@ -31,7 +31,7 @@ namespace Goals } TSubgoal whatToDoToAchieve() override; - virtual bool operator==(const Win & other) const override + bool operator==(const Win & other) const override { return true; } diff --git a/AI/VCAI/Pathfinding/AINodeStorage.cpp b/AI/VCAI/Pathfinding/AINodeStorage.cpp index 8e7bc4a8b..f0bb91c9c 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.cpp +++ b/AI/VCAI/Pathfinding/AINodeStorage.cpp @@ -321,9 +321,7 @@ bool AINodeStorage::hasBetterChain(const PathNodeInfo & source, CDestinationNode bool AINodeStorage::isTileAccessible(const int3 & pos, const EPathfindingLayer layer) const { - const AIPathNode & node = nodes[layer][pos.z][pos.x][pos.y][0]; - - return node.action != EPathNodeAction::UNKNOWN; + return nodes[layer][pos.z][pos.x][pos.y][0].action != EPathNodeAction::UNKNOWN; } std::vector AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand) const diff --git a/AI/VCAI/Pathfinding/AINodeStorage.h b/AI/VCAI/Pathfinding/AINodeStorage.h index 51e32fdcc..518972ed3 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.h +++ b/AI/VCAI/Pathfinding/AINodeStorage.h @@ -87,7 +87,7 @@ public: void initialize(const PathfinderOptions & options, const CGameState * gs) override; - virtual std::vector getInitialNodes() override; + std::vector getInitialNodes() override; virtual std::vector calculateNeighbours( const PathNodeInfo & source, @@ -99,7 +99,7 @@ public: const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; - virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; + void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; const AIPathNode * getAINode(const CGPathNode * node) const; void updateAINode(CGPathNode * node, std::function updater); diff --git a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp index 68e516e1a..dc067db74 100644 --- a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp @@ -41,6 +41,7 @@ namespace AIPathfinding std::shared_ptr nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero()) { + options.ignoreGuards = false; options.useEmbarkAndDisembark = true; options.useTeleportTwoWay = true; options.useTeleportOneWay = true; diff --git a/AI/VCAI/Pathfinding/AIPathfinderConfig.h b/AI/VCAI/Pathfinding/AIPathfinderConfig.h index 81ec9722c..076579e12 100644 --- a/AI/VCAI/Pathfinding/AIPathfinderConfig.h +++ b/AI/VCAI/Pathfinding/AIPathfinderConfig.h @@ -30,6 +30,6 @@ namespace AIPathfinding ~AIPathfinderConfig(); - virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; + CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; }; } diff --git a/AI/VCAI/Pathfinding/Actions/BattleAction.h b/AI/VCAI/Pathfinding/Actions/BattleAction.h index 630e49a55..dc3adb84e 100644 --- a/AI/VCAI/Pathfinding/Actions/BattleAction.h +++ b/AI/VCAI/Pathfinding/Actions/BattleAction.h @@ -25,6 +25,6 @@ namespace AIPathfinding { } - virtual Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; + Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; }; } \ No newline at end of file diff --git a/AI/VCAI/Pathfinding/Actions/BoatActions.h b/AI/VCAI/Pathfinding/Actions/BoatActions.h index 114478fcb..793a74486 100644 --- a/AI/VCAI/Pathfinding/Actions/BoatActions.h +++ b/AI/VCAI/Pathfinding/Actions/BoatActions.h @@ -40,7 +40,7 @@ namespace AIPathfinding { } - virtual Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; + Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; virtual void applyOnDestination( const CGHeroInstance * hero, @@ -66,6 +66,6 @@ namespace AIPathfinding { } - virtual Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; + Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; }; } diff --git a/AI/VCAI/Pathfinding/Actions/TownPortalAction.h b/AI/VCAI/Pathfinding/Actions/TownPortalAction.h index 8d587e052..eba21d392 100644 --- a/AI/VCAI/Pathfinding/Actions/TownPortalAction.h +++ b/AI/VCAI/Pathfinding/Actions/TownPortalAction.h @@ -27,6 +27,6 @@ namespace AIPathfinding { } - virtual Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; + Goals::TSubgoal whatToDo(const HeroPtr & hero) const override; }; } diff --git a/AI/VCAI/Pathfinding/PathfindingManager.cpp b/AI/VCAI/Pathfinding/PathfindingManager.cpp index 3b56951ed..c983150bc 100644 --- a/AI/VCAI/Pathfinding/PathfindingManager.cpp +++ b/AI/VCAI/Pathfinding/PathfindingManager.cpp @@ -190,7 +190,7 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet if(isBlockedBorderGate(firstTileToGet)) { //FIXME: this way we'll not visit gate and activate quest :? - return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->subID)); + return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->getObjTypeIndex())); } auto topObj = cb->getTopObj(firstTileToGet); diff --git a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp index 9603189fc..4e857df98 100644 --- a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -58,7 +58,7 @@ namespace AIPathfinding { if(obj->ID != Obj::TOWN) //towns were handled in the previous loop { - if(const IShipyard * shipyard = IShipyard::castFrom(obj)) + if(const auto * shipyard = dynamic_cast(obj)) shipyards.push_back(shipyard); } } diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index 8a97e430f..b69c4e9bc 100644 --- a/AI/VCAI/ResourceManager.cpp +++ b/AI/VCAI/ResourceManager.cpp @@ -59,19 +59,7 @@ TResources ResourceManager::estimateIncome() const if (obj->ID == Obj::MINE) { auto mine = dynamic_cast(obj); - switch (mine->producedResource.toEnum()) - { - case EGameResID::WOOD: - case EGameResID::ORE: - ret[obj->subID] += WOOD_ORE_MINE_PRODUCTION; - break; - case EGameResID::GOLD: - ret[EGameResID::GOLD] += GOLD_MINE_PRODUCTION; - break; - default: - ret[obj->subID] += RESOURCE_MINE_PRODUCTION; - break; - } + ret += mine->dailyIncome(); } } diff --git a/AI/VCAI/ResourceManager.h b/AI/VCAI/ResourceManager.h index cdbc79007..af686081f 100644 --- a/AI/VCAI/ResourceManager.h +++ b/AI/VCAI/ResourceManager.h @@ -28,7 +28,7 @@ struct DLL_EXPORT ResourceObjective Goals::TSubgoal goal; //what for (build, gather army etc...) //TODO: register? - template void serializeInternal(Handler & h, const int version) + template void serializeInternal(Handler & h) { h & resources; //h & goal; //FIXME: goal serialization is broken @@ -105,7 +105,7 @@ private: void dumpToLog() const; //TODO: register? - template void serializeInternal(Handler & h, const int version) + template void serializeInternal(Handler & h) { h & saving; h & queue; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 1099fd3cb..7ce4679dd 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -14,6 +14,7 @@ #include "BuildingManager.h" #include "Goals/Goals.h" +#include "../../lib/ArtifactUtils.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" @@ -21,7 +22,6 @@ #include "../../lib/CHeroHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/bonuses/CBonusSystemNode.h" #include "../../lib/bonuses/Limiters.h" #include "../../lib/bonuses/Updaters.h" #include "../../lib/bonuses/Propagators.h" @@ -65,7 +65,7 @@ struct SetGlobalState }; -#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai); +#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai) #define NET_EVENT_HANDLER SET_GLOBAL_STATE(this) #define MAKING_TURN SET_GLOBAL_STATE(this) @@ -103,7 +103,7 @@ void VCAI::heroMoved(const TryMoveHero & details, bool verbose) validateObject(details.id); auto hero = cb->getHero(details.id); - const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; + const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0)); const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose)); @@ -311,7 +311,8 @@ void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, Q requestActionASAP([=]() { - float goalpriority1 = 0, goalpriority2 = 0; + float goalpriority1 = 0; + float goalpriority2 = 0; auto firstGoal = getGoal(firstHero); if(firstGoal->goalType == Goals::GATHER_ARMY) @@ -504,14 +505,14 @@ void VCAI::requestRealized(PackageApplied * pa) NET_EVENT_HANDLER; if(status.haveTurn()) { - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { if(pa->result) status.madeTurn(); } } - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { status.receivedAnswerConfirmation(pa->requestID, pa->result); } @@ -563,7 +564,7 @@ void VCAI::objectPropertyChanged(const SetObjectProperty * sop) NET_EVENT_HANDLER; if(sop->what == ObjProperty::OWNER) { - if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES) + if(myCb->getPlayerRelations(playerID, sop->identifier.as()) == PlayerRelations::ENEMIES) { //we want to visit objects owned by oppponents auto obj = myCb->getObj(sop->id, false); @@ -745,9 +746,8 @@ void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, cons requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); } -void VCAI::saveGame(BinarySerializer & h, const int version) +void VCAI::saveGame(BinarySerializer & h) { - LOG_TRACE_PARAMS(logAi, "version '%i'", version); NET_EVENT_HANDLER; validateVisitableObjs(); @@ -755,21 +755,20 @@ void VCAI::saveGame(BinarySerializer & h, const int version) //disabled due to issue 2890 registerGoals(h); #endif // 0 - CAdventureAI::saveGame(h, version); - serializeInternal(h, version); + CAdventureAI::saveGame(h); + serializeInternal(h); } -void VCAI::loadGame(BinaryDeserializer & h, const int version) +void VCAI::loadGame(BinaryDeserializer & h) { - LOG_TRACE_PARAMS(logAi, "version '%i'", version); //NET_EVENT_HANDLER; #if 0 //disabled due to issue 2890 registerGoals(h); #endif // 0 - CAdventureAI::loadGame(h, version); - serializeInternal(h, version); + CAdventureAI::loadGame(h); + serializeInternal(h); } void makePossibleUpgrades(const CArmedInstance * obj) @@ -1166,21 +1165,21 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot for(auto p : h->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(h, p.first)); + allArtifacts.push_back(ArtifactLocation(h->id, p.first)); } } for(auto slot : h->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact))); if(otherh) { for(auto p : otherh->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(otherh, p.first)); + allArtifacts.push_back(ArtifactLocation(otherh->id, p.first)); } for(auto slot : otherh->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact))); } //we give stuff to one hero or another, depending on giveStuffToFirstHero @@ -1195,10 +1194,10 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK) continue; // don't attempt to move catapult and spellbook - if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) + if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot)) continue; //don't reequip artifact we already wear - auto s = location.getSlot(); + auto s = cb->getHero(location.artHolder)->getSlot(location.slot); if(!s || s->locked) //we can't move locks continue; auto artifact = s->artifact; @@ -1209,9 +1208,9 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot bool emptySlotFound = false; for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) { - ArtifactLocation destLocation(target, slot); - if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { + ArtifactLocation destLocation(target->id, slot); cb->swapArtifacts(location, destLocation); //just put into empty slot emptySlotFound = true; changeMade = true; @@ -1225,11 +1224,11 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one { - ArtifactLocation destLocation(target, slot); //if that artifact is better than what we have, pick it - if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { - cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); + ArtifactLocation destLocation(target->id, slot); + cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact))); changeMade = true; break; } @@ -1414,8 +1413,8 @@ void VCAI::wander(HeroPtr h) auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool { const CGHeroInstance * hptr = h.get(); - auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs), - r2 = ah->howManyReinforcementsCanGet(hptr, rhs); + auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs); + auto r2 = ah->howManyReinforcementsCanGet(hptr, rhs); if (r1 != r2) return r1 < r2; else @@ -1609,15 +1608,6 @@ void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); battlename.clear(); - if (queryID != QueryID::NONE) - { - status.addQuery(queryID, "Combat result dialog"); - const int confirmAction = 0; - requestActionASAP([=]() - { - answerQuery(queryID, confirmAction); - }); - } CAdventureAI::battleEnd(battleID, br, queryID); } @@ -1759,11 +1749,11 @@ void VCAI::addVisitableObj(const CGObjectInstance * obj) CGTeleport::addToChannel(knownTeleportChannels, teleportObj); } -const CGObjectInstance * VCAI::lookForArt(int aid) const +const CGObjectInstance * VCAI::lookForArt(ArtifactID aid) const { for(const CGObjectInstance * obj : ai->visitableObjs) { - if(obj->ID == Obj::ARTIFACT && obj->subID == aid) + if(obj->ID == Obj::ARTIFACT && dynamic_cast(obj)->getArtifact() == aid) return obj; } @@ -2144,7 +2134,7 @@ void VCAI::tryRealize(Goals::Trade & g) //trade int accquiredResources = 0; if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) { - if(const IMarket * m = IMarket::castFrom(obj, false)) + if(const auto * m = dynamic_cast(obj)) { auto freeRes = ah->freeResources(); //trade only resources which are not reserved for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++) @@ -2153,13 +2143,14 @@ void VCAI::tryRealize(Goals::Trade & g) //trade if(res.getNum() == g.resID) //sell any other resource continue; - int toGive, toGet; + int toGive; + int toGet; m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); toGive = static_cast(toGive * (it->resVal / toGive)); //round down //TODO trade only as much as needed if (toGive) //don't try to sell 0 resources { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive); + cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); accquiredResources = static_cast(toGet * (it->resVal / toGive)); logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); } @@ -2220,8 +2211,8 @@ void VCAI::tryRealize(Goals::BuyArmy & g) *boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs) { //max value of creatures we can buy with our res - int value1 = lhs.cre->getAIValue() * lhs.count, - value2 = rhs.cre->getAIValue() * rhs.count; + int value1 = lhs.cre->getAIValue() * lhs.count; + int value2 = rhs.cre->getAIValue() * rhs.count; return value1 < value2; }); diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 8231003d9..4db3f89f0 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -68,7 +68,7 @@ public: void heroVisit(const CGObjectInstance * obj, bool started); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battle; h & remainingQueries; @@ -152,8 +152,8 @@ public: void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void saveGame(BinarySerializer & h, const int version) override; //saving - void loadGame(BinaryDeserializer & h, const int version) override; //loading + void saveGame(BinarySerializer & h) override; //saving + void loadGame(BinaryDeserializer & h) override; //loading void finish() override; void availableCreaturesChanged(const CGDwelling * town) override; @@ -251,7 +251,7 @@ public: void retrieveVisitableObjs(); virtual std::vector getFlaggedObjects() const; - const CGObjectInstance * lookForArt(int aid) const; + const CGObjectInstance * lookForArt(ArtifactID aid) const; bool isAccessible(const int3 & pos) const; HeroPtr getHeroWithGrail() const; @@ -301,7 +301,7 @@ public: } #endif - template void serializeInternal(Handler & h, const int version) + template void serializeInternal(Handler & h) { h & knownTeleportChannels; h & knownSubterraneanGates; @@ -341,7 +341,7 @@ public: //we have to explicitly ignore invalid goal class type id h & typeId; Goals::AbstractGoal ignored2; - ignored2.serialize(h, version); + ignored2.serialize(h); } } } @@ -371,11 +371,7 @@ public: { } - virtual ~cannotFulfillGoalException() throw () - { - }; - - const char * what() const throw () override + const char * what() const noexcept override { return msg.c_str(); } @@ -394,11 +390,7 @@ public: msg = goal->name(); } - virtual ~goalFulfilledException() throw () - { - }; - - const char * what() const throw () override + const char * what() const noexcept override { return msg.c_str(); } diff --git a/AI/VCAI/main.cpp b/AI/VCAI/main.cpp index 3a88b2716..1e262024e 100644 --- a/AI/VCAI/main.cpp +++ b/AI/VCAI/main.cpp @@ -14,7 +14,7 @@ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif -static const char * g_cszAiName = "VCAI"; +static const char * const g_cszAiName = "VCAI"; extern "C" DLL_EXPORT int GetGlobalAiVersion() { diff --git a/CCallback.cpp b/CCallback.cpp index 753c443f0..3b692ec21 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -16,6 +16,7 @@ #include "client/Client.h" #include "lib/mapping/CMap.h" #include "lib/mapObjects/CGHeroInstance.h" +#include "lib/mapObjects/CGTownInstance.h" #include "lib/CBuildingHandler.h" #include "lib/CGeneralTextHandler.h" #include "lib/CHeroHandler.h" @@ -237,12 +238,12 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) sendRequest(&pack); } -void CCallback::trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) +void CCallback::trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero) { - trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); + trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); } -void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) +void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) { TradeOnMarketplace pack; pack.marketId = dynamic_cast(market)->id; @@ -254,18 +255,18 @@ void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vecto sendRequest(&pack); } -void CCallback::setFormation(const CGHeroInstance * hero, bool tight) +void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode) { - SetFormation pack(hero->id,tight); + SetFormation pack(hero->id, mode); sendRequest(&pack); } -void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) +void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero) { assert(townOrTavern); assert(hero); - HireHero pack(HeroTypeID(hero->subID), townOrTavern->id); + HireHero pack(hero->getHeroType(), townOrTavern->id, nextHero); pack.player = *player; sendRequest(&pack); } @@ -405,7 +406,10 @@ std::optional CBattleCallback::makeSurrenderRetreatDecision(const std::shared_ptr CBattleCallback::getBattle(const BattleID & battleID) { - return activeBattles.at(battleID); + if (activeBattles.count(battleID)) + return activeBattles.at(battleID); + + throw std::runtime_error("Failed to find battle " + std::to_string(battleID.getNum()) + " of player " + player->toString() + ". Number of ongoing battles: " + std::to_string(activeBattles.size())); } std::optional CBattleCallback::getPlayerID() const @@ -415,10 +419,18 @@ std::optional CBattleCallback::getPlayerID() const void CBattleCallback::onBattleStarted(const IBattleInfo * info) { + if (activeBattles.count(info->getBattleID()) > 0) + throw std::runtime_error("Player " + player->toString() + " is already engaged in battle " + std::to_string(info->getBattleID().getNum())); + + logGlobal->debug("Battle %d started for player %s", info->getBattleID(), player->toString()); activeBattles[info->getBattleID()] = std::make_shared(info, *getPlayerID()); } void CBattleCallback::onBattleEnded(const BattleID & battleID) { + if (activeBattles.count(battleID) == 0) + throw std::runtime_error("Player " + player->toString() + " is not engaged in battle " + std::to_string(battleID.getNum())); + + logGlobal->debug("Battle %d ended for player %s", battleID, player->toString()); activeBattles.erase(battleID); } diff --git a/CCallback.h b/CCallback.h index d7af2e38c..5e9f1cf1a 100644 --- a/CCallback.h +++ b/CCallback.h @@ -12,6 +12,7 @@ #include "lib/CGameInfoCallback.h" #include "lib/battle/CPlayerBattleCallback.h" #include "lib/int3.h" // for int3 +#include "lib/networkPacks/TradeItem.h" VCMI_LIB_NAMESPACE_BEGIN @@ -72,14 +73,14 @@ public: virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell //town - virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)=0; + virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE)=0; virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0; virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0; virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made virtual void swapGarrisonHero(const CGTownInstance *town)=0; - virtual void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce - virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; + virtual void trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce + virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; virtual int selectionMade(int selection, QueryID queryID) =0; virtual int sendQueryReply(std::optional reply, QueryID queryID) =0; @@ -94,7 +95,7 @@ public: virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0; virtual void endTurn()=0; virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) - virtual void setFormation(const CGHeroInstance * hero, bool tight)=0; + virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0; virtual void save(const std::string &fname) = 0; virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; @@ -181,10 +182,10 @@ public: void endTurn() override; void swapGarrisonHero(const CGTownInstance *town) override; void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; - void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; - void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; - void setFormation(const CGHeroInstance * hero, bool tight) override; - void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; + void trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; + void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; + void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override; + void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override; void save(const std::string &fname) override; void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; void gamePause(bool pause) override; diff --git a/CI/conan/base/cross-macro.j2 b/CI/conan/base/cross-macro.j2 index a1829b5f6..7f4edf0ee 100644 --- a/CI/conan/base/cross-macro.j2 +++ b/CI/conan/base/cross-macro.j2 @@ -17,4 +17,5 @@ RC={{ target_host }}-windres {% macro generate_conf(target_host) -%} tools.build:compiler_executables = {"c": "{{ target_host }}-gcc", "cpp": "{{ target_host }}-g++"} tools.build:sysroot = /usr/{{ target_host }} +tools.build:defines = ["WINVER=0x0601", "_WIN32_WINNT=0x0601"] {%- endmacro -%} \ No newline at end of file diff --git a/CI/linux-qt6/before_install.sh b/CI/linux-qt6/before_install.sh index 689101138..df82f65ac 100644 --- a/CI/linux-qt6/before_install.sh +++ b/CI/linux-qt6/before_install.sh @@ -7,4 +7,4 @@ sudo apt-get install libboost-all-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \ ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ -libminizip-dev libfuzzylite-dev # Optional dependencies +libminizip-dev libfuzzylite-dev libsqlite3-dev # Optional dependencies diff --git a/CI/linux-qt6/validate_json.py b/CI/linux-qt6/validate_json.py index 5fbe0ac76..b57961eac 100755 --- a/CI/linux-qt6/validate_json.py +++ b/CI/linux-qt6/validate_json.py @@ -5,56 +5,24 @@ import sys from pathlib import Path from pprint import pprint -import json5 -import jstyleson -import yaml +# VCMI supports JSON with comments, but not JSON5 +import jstyleson -# 'json', 'json5' or 'yaml' -# json: strict, but doesn't preserve line numbers necessarily, since it strips comments before parsing -# json5: strict and preserves line numbers even for files with line comments -# yaml: less strict, allows e.g. leading zeros -VALIDATION_TYPE = "json5" +validation_failed = False -errors = [] -for path in sorted(Path(".").glob("**/*.json")): +for path in sorted(Path(".").glob("**/*.json"), key=lambda path: str(path).lower()): # because path is an object and not a string path_str = str(path) + if path_str.startswith("."): + continue + try: with open(path_str, "r") as file: - if VALIDATION_TYPE == "json": - jstyleson.load(file) - elif VALIDATION_TYPE == "json5": - json5.load(file) - elif VALIDATION_TYPE == "yaml": - file = file.read().replace("\t", " ") - file = file.replace("//", "#") - yaml.safe_load(file) - print(f"Validation of {path_str} succeeded") + jstyleson.load(file) + print(f"✅ {path_str}") except Exception as exc: - print(f"Validation of {path_str} failed") - pprint(exc) + print(f"❌ {path_str}: {exc}") + validation_failed = True - error_pos = path_str - - # create error position strings for each type of parser - if hasattr(exc, "pos"): - # 'json' - # https://stackoverflow.com/a/72850269/2278742 - error_pos = f"{path_str}:{exc.lineno}:{exc.colno}" - print(error_pos) - elif VALIDATION_TYPE == "json5": - # 'json5' - pos = re.findall(r"\d+", str(exc)) - error_pos = f"{path_str}:{pos[0]}:{pos[-1]}" - elif hasattr(exc, "problem_mark"): - # 'yaml' - mark = exc.problem_mark - error_pos = f"{path_str}:{mark.line+1}:{mark.column+1}" - print(error_pos) - - errors.append({"error_pos": error_pos, "error_msg": exc}) - -if errors: - print("The following JSON files are invalid:") - pprint(errors) +if validation_failed: sys.exit(1) diff --git a/CI/linux/before_install.sh b/CI/linux/before_install.sh index e08075d7d..345634134 100644 --- a/CI/linux/before_install.sh +++ b/CI/linux/before_install.sh @@ -7,4 +7,4 @@ sudo apt-get install libboost-all-dev \ libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ qtbase5-dev \ ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ -libminizip-dev libfuzzylite-dev qttools5-dev # Optional dependencies +libminizip-dev libfuzzylite-dev qttools5-dev libsqlite3-dev # Optional dependencies diff --git a/CI/mingw-32/before_install.sh b/CI/mingw-32/before_install.sh new file mode 100644 index 000000000..2bb40f7d1 --- /dev/null +++ b/CI/mingw-32/before_install.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +sudo apt-get update +sudo apt-get install ninja-build mingw-w64 nsis +sudo update-alternatives --set i686-w64-mingw32-g++ /usr/bin/i686-w64-mingw32-g++-posix + +# Workaround for getting new MinGW headers on Ubuntu 22.04. +# Remove it once MinGW headers version in repository will be 10.0 at least +curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-common_10.0.0-3_all.deb \ + && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb; +curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-i686-dev_10.0.0-3_all.deb \ + && sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb; + +mkdir ~/.conan ; cd ~/.conan +curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.1/vcmi-deps-windows-conan-w32.tgz" \ + | tar -xzf - diff --git a/CI/mingw-ubuntu/before_install.sh b/CI/mingw/before_install.sh similarity index 93% rename from CI/mingw-ubuntu/before_install.sh rename to CI/mingw/before_install.sh index bc1acca52..37babba74 100755 --- a/CI/mingw-ubuntu/before_install.sh +++ b/CI/mingw/before_install.sh @@ -12,5 +12,5 @@ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64- && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb; mkdir ~/.conan ; cd ~/.conan -curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.0/vcmi-deps-windows-conan-w64.tgz" \ +curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.1/vcmi-deps-windows-conan-w64.tgz" \ | tar -xzf - diff --git a/CI/msvc/before_install.sh b/CI/msvc/before_install.sh index 851cbd4a9..5388e84f8 100644 --- a/CI/msvc/before_install.sh +++ b/CI/msvc/before_install.sh @@ -1,5 +1,5 @@ curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \ - "https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" + "https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.7/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" 7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" #rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a56e91fd..ceadb6fd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,86 +41,106 @@ if(NOT CMAKE_BUILD_TYPE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release RelWithDebInfo) endif() -set(singleProcess OFF) -set(staticAI OFF) -if(ANDROID) - set(staticAI ON) - set(singleProcess ON) -endif() +# Platform-independent options option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF) option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF) -if(NOT ANDROID) - option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) - option(ENABLE_EDITOR "Enable compilation of map editor" ON) -endif() option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON) option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON) +option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON) -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") - set(ENABLE_SINGLE_APP_BUILD ON) -else() - option(ENABLE_TEST "Enable compilation of unit tests" OFF) - option(ENABLE_SINGLE_APP_BUILD "Builds client and server as single executable" ${singleProcess}) -endif() +# Compilation options option(ENABLE_PCH "Enable compilation using precompiled headers" ON) -option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON) option(ENABLE_DEBUG_CONSOLE "Enable debug console for Windows builds" ON) option(ENABLE_STRICT_COMPILATION "Treat all compiler warnings as errors" OFF) option(ENABLE_MULTI_PROCESS_BUILDS "Enable /MP flag for MSVS solution" ON) -option(COPY_CONFIG_ON_BUILD "Copies config folder into output directory at building phase" ON) -option(ENABLE_STATIC_AI_LIBS "Add AI code into VCMI lib directly" ${staticAI}) +option(ENABLE_COLORIZED_COMPILER_OUTPUT "Colorize compilation output (Clang/GNU)." ON) +option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF) -# Used for Snap packages and also useful for debugging -if(NOT APPLE_IOS AND NOT ANDROID) - option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF) +# Platform-specific options + +if(ANDROID) + set(ENABLE_STATIC_LIBS ON) + set(ENABLE_LAUNCHER OFF) +else() + option(ENABLE_STATIC_LIBS "Build library and all components such as AI statically" OFF) + option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) endif() -# On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. -# The XCode and MSVC builds each require some more configuration, which is enabled by the following option: -if(MSVC OR (CMAKE_GENERATOR STREQUAL "Xcode")) - option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF) +if(APPLE_IOS OR ANDROID) + set(ENABLE_MONOLITHIC_INSTALL OFF) + set(ENABLE_SINGLE_APP_BUILD ON) + set(ENABLE_EDITOR OFF) + set(ENABLE_TEST OFF) + set(ENABLE_LOBBY OFF) + set(ENABLE_SERVER OFF) + set(COPY_CONFIG_ON_BUILD OFF) +else() + option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF) # Used for Snap packages and also useful for debugging + option(COPY_CONFIG_ON_BUILD "Copies config folder into output directory at building phase" ON) + option(ENABLE_SERVER "Enable compilation of dedicated server" ON) + option(ENABLE_EDITOR "Enable compilation of map editor" ON) + option(ENABLE_SINGLE_APP_BUILD "Builds client and launcher as single executable" OFF) + option(ENABLE_TEST "Enable compilation of unit tests" OFF) + option(ENABLE_LOBBY "Enable compilation of lobby server" OFF) endif() -if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode")) - find_program(CCACHE ccache REQUIRED) - if(CCACHE) - # https://stackoverflow.com/a/36515503/2278742 - # Set up wrapper scripts - configure_file(xcode/launch-c.in xcode/launch-c) - configure_file(xcode/launch-cxx.in xcode/launch-cxx) - execute_process(COMMAND chmod a+rx - "${CMAKE_BINARY_DIR}/xcode/launch-c" - "${CMAKE_BINARY_DIR}/xcode/launch-cxx") - # Set Xcode project attributes to route compilation through our scripts - set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/xcode/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/xcode/launch-cxx") - set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/xcode/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/xcode/launch-cxx") - endif() -endif() - -# Allow to pass package name from Travis CI -set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name") -set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename") - # ERM depends on LUA implicitly if(ENABLE_ERM AND NOT ENABLE_LUA) set(ENABLE_LUA ON) endif() -# We don't want to deploy assets into build directory for android/iOS build -if((APPLE_IOS OR ANDROID) AND COPY_CONFIG_ON_BUILD) - set(COPY_CONFIG_ON_BUILD OFF) -endif() - ############################################ # Miscellaneous options # ############################################ +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) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-fdiagnostics-color=always) + endif() +endif() + +if(ENABLE_CCACHE) + find_program(CCACHE ccache REQUIRED) +endif() + +# The XCode and MSVC builds each require some more configuration further down. +if(ENABLE_CCACHE) + set(CMAKE_C_COMPILER_LAUNCHER "ccache") + set(CMAKE_CXX_COMPILER_LAUNCHER "ccache") +endif() + +if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode")) + # https://stackoverflow.com/a/36515503/2278742 + # Set up wrapper scripts + configure_file(xcode/launch-c.in xcode/launch-c) + configure_file(xcode/launch-cxx.in xcode/launch-cxx) + execute_process(COMMAND chmod a+rx + "${CMAKE_BINARY_DIR}/xcode/launch-c" + "${CMAKE_BINARY_DIR}/xcode/launch-cxx") + # Set Xcode project attributes to route compilation through our scripts + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/xcode/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/xcode/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/xcode/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/xcode/launch-cxx") +endif() + +# Allow to pass package name from Travis CI +set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name") +set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename") + set(CMAKE_MODULE_PATH ${CMAKE_HOME_DIRECTORY}/cmake_modules ${PROJECT_SOURCE_DIR}/CI) # Contains custom functions and macros, but don't altering any options @@ -229,7 +249,7 @@ if(ENABLE_EDITOR) endif() if(ENABLE_SINGLE_APP_BUILD) - add_definitions(-DSINGLE_PROCESS_APP=1) + add_definitions(-DENABLE_SINGLE_APP_BUILD) endif() if(APPLE_IOS) @@ -255,7 +275,10 @@ endif() if(MINGW OR MSVC) # Windows Vista or newer for FuzzyLite 6 to compile - add_definitions(-D_WIN32_WINNT=0x0600) + # Except for conan which already has this definition in its preset + if(NOT USING_CONAN) + add_definitions(-D_WIN32_WINNT=0x0600) + endif() #delete lib prefix for dlls (libvcmi -> vcmi) set(CMAKE_SHARED_LIBRARY_PREFIX "") @@ -263,19 +286,16 @@ if(MINGW OR MSVC) if(MSVC) if(ENABLE_CCACHE) # https://github.com/ccache/ccache/discussions/1154#discussioncomment-3611387 - find_program(CCACHE ccache REQUIRED) - if (CCACHE) - file(COPY_FILE - ${CCACHE} ${CMAKE_BINARY_DIR}/cl.exe - ONLY_IF_DIFFERENT) + file(COPY_FILE + ${CCACHE} ${CMAKE_BINARY_DIR}/cl.exe + ONLY_IF_DIFFERENT) - set(CMAKE_VS_GLOBALS - "CLToolExe=cl.exe" - "CLToolPath=${CMAKE_BINARY_DIR}" - "TrackFileAccess=false" - "UseMultiToolTask=true" - ) - endif() + set(CMAKE_VS_GLOBALS + "CLToolExe=cl.exe" + "CLToolPath=${CMAKE_BINARY_DIR}" + "TrackFileAccess=false" + "UseMultiToolTask=true" + ) endif() add_definitions(-DBOOST_ALL_NO_LIB) @@ -463,6 +483,10 @@ if(TARGET SDL2_ttf::SDL2_ttf) add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf) endif() +if(ENABLE_LOBBY) + find_package(SQLite3 REQUIRED) +endif() + if(ENABLE_LAUNCHER OR ENABLE_EDITOR) # Widgets finds its own dependencies (QtGui and QtCore). find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network) @@ -584,21 +608,19 @@ if(APPLE_IOS) add_subdirectory(ios) endif() -set(VCMI_LIB_TARGET vcmi) add_subdirectory_with_folder("AI" AI) -include(VCMI_lib) add_subdirectory(lib) -if(ENABLE_SINGLE_APP_BUILD) - add_subdirectory(lib_server) -endif() +add_subdirectory(server) if(ENABLE_ERM) add_subdirectory(scripting/erm) endif() + if(ENABLE_LUA) add_subdirectory(scripting/lua) endif() + if(NOT TARGET minizip::minizip) add_subdirectory_with_folder("3rdparty" lib/minizip) add_library(minizip::minizip ALIAS minizip) @@ -607,11 +629,21 @@ endif() if(ENABLE_LAUNCHER) add_subdirectory(launcher) endif() + if(ENABLE_EDITOR) add_subdirectory(mapeditor) endif() + +if(ENABLE_LOBBY) + add_subdirectory(lobby) +endif() + add_subdirectory(client) -add_subdirectory(server) + +if(ENABLE_SERVER) + add_subdirectory(serverapp) +endif() + if(ENABLE_TEST) enable_testing() add_subdirectory(test) @@ -715,6 +747,11 @@ endif(WIN32) # Packaging section # ####################################### +if(MSVC) + SET(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${BIN_DIR}) + Include(InstallRequiredSystemLibraries) +endif() + set(CPACK_PACKAGE_VERSION_MAJOR ${VCMI_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${VCMI_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${VCMI_VERSION_PATCH}) @@ -762,10 +799,12 @@ if(WIN32) set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\${VCMI_MAIN_EXECUTABLE}.exe\\\" ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_server dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_server.exe\\\" enable=yes profile=public,private' SW_HIDE + ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_client dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_client.exe\\\" enable=yes profile=public,private' SW_HIDE ") set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ExecShell '' 'netsh' 'advfirewall firewall delete rule name=vcmi_server' SW_HIDE + ExecShell '' 'netsh' 'advfirewall firewall delete rule name=vcmi_client' SW_HIDE ") # Strip MinGW CPack target if build configuration without debug info @@ -798,6 +837,10 @@ elseif(APPLE_MACOS AND NOT ENABLE_MONOLITHIC_INSTALL) set(CPACK_MONOLITHIC_INSTALL 1) set(CPACK_GENERATOR "DragNDrop") set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_SOURCE_DIR}/osx/dmg_background.png") + # Workaround for this issue: + # CPack Error: Error executing: /usr/bin/hdiutil detach "/Volumes/VCMI" + # https://github.com/actions/runner-images/issues/7522#issuecomment-1564467252 + set(CPACK_COMMAND_HDIUTIL "/usr/bin/sudo /usr/bin/hdiutil") # CMake code for CPACK_DMG_DS_STORE executed before DS_STORE_SETUP_SCRIPT # So we can keep both enabled and this shouldn't hurt # set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/osx/dmg_DS_Store") diff --git a/CMakePresets.json b/CMakePresets.json index f43ac51ff..d35b24e74 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -51,7 +51,8 @@ "inherits": "linux-release", "hidden": true, "cacheVariables": { - "ENABLE_TEST": "ON", + "ENABLE_LOBBY": "ON", + "ENABLE_TEST": "ON", "ENABLE_LUA": "ON" } }, @@ -65,6 +66,15 @@ "CMAKE_CXX_COMPILER": "/usr/bin/clang++" } }, + { + "name": "linux-clang-release-ccache", + "displayName": "Clang x86_64-pc-linux-gnu with ccache", + "description": "VCMI Linux Clang with ccache", + "inherits": "linux-release", + "cacheVariables": { + "ENABLE_CCACHE": "ON" + } + }, { "name": "linux-gcc-release", "displayName": "GCC x86_64-pc-linux-gnu", @@ -76,6 +86,15 @@ "CMAKE_CXX_COMPILER": "/usr/bin/g++" } }, + { + "name": "linux-gcc-release-ccache", + "displayName": "GCC x86_64-pc-linux-gnu with ccache", + "description": "VCMI Linux GCC with ccache", + "inherits": "linux-release", + "cacheVariables": { + "ENABLE_CCACHE": "ON" + } + }, { "name": "linux-gcc-debug", "displayName": "GCC x86_64-pc-linux-gnu (debug)", @@ -109,6 +128,15 @@ "CMAKE_CXX_COMPILER": "/usr/bin/g++" } }, + { + "name": "windows-mingw-release", + "displayName": "Windows x64 MinGW Release", + "description": "VCMI Windows Ninja using MinGW", + "inherits": "default-release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, { "name": "windows-msvc-release", "displayName": "Windows x64 RelWithDebInfo", @@ -284,6 +312,11 @@ "configurePreset": "linux-clang-release", "inherits": "default-release" }, + { + "name": "linux-clang-release-ccache", + "configurePreset": "linux-clang-release-ccache", + "inherits": "linux-clang-release" + }, { "name": "linux-clang-test", "configurePreset": "linux-clang-test", @@ -299,6 +332,11 @@ "configurePreset": "linux-gcc-release", "inherits": "default-release" }, + { + "name": "linux-gcc-release-ccache", + "configurePreset": "linux-gcc-release-ccache", + "inherits": "linux-gcc-release" + }, { "name": "linux-gcc-debug", "configurePreset": "linux-gcc-debug", @@ -324,11 +362,15 @@ "configurePreset": "macos-arm-conan-ninja-release", "inherits": "default-release" }, + { + "name": "windows-mingw-release", + "configurePreset": "windows-mingw-release", + "inherits": "default-release" + }, { "name": "windows-msvc-release", "configurePreset": "windows-msvc-release", - "inherits": "default-release", - "configuration": "Release" + "inherits": "default-release" }, { "name": "windows-msvc-release-ccache", @@ -411,6 +453,11 @@ "configurePreset": "macos-ninja-release", "inherits": "default-release" }, + { + "name": "windows-mingw-release", + "configurePreset": "windows-mingw-release", + "inherits": "default-release" + }, { "name": "windows-msvc-release", "configurePreset": "windows-msvc-release", diff --git a/ChangeLog.md b/ChangeLog.md index 82f214f5e..46bb9857b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,463 @@ +# 1.4.5 -> 1.5.0 + +### General +* Added option to disable cheats in game + +### Interface +* Town Portal dialog will now show town icons +* Town Portal dialog will now show town info on right click +* Town Portal dialog will center on town on clicking it +* Town Portal dialog now uses same town ordering as in adventure map interface +* Heroes can now be recruited from the tavern by double-clicking on them +* Added status bar to the backpack window +* Quick backpack window is now only available when enabled Interface enhancements +* Fixed assembly of artifacts in the backpack when backpack is full +* Attempt to use enemy turn replay feature will now show "Not implemented" message + +### Campaigns +* Game will now correctly track who defeated the hero or wandering monsters for related quests and victory conditions +* Birth of a Barbarian: Yog will now start the third scenario with Angelic Alliance in his inventory +* Birth of a Barbarian: Heroes with Angelic Alliance components are now considered to be mission-critical and can't be dismissed or lost in combat +* Birth of a Barbarian: Yog can no longer purchase spellbook from the Mage Guild +* Birth of a Barbarian: Yog will no longer gain Spellpower or Knowledge when leveling up +* Birth of a Barbarian: Scenarios with mission to deliver an artifact will no longer end after just defeating enemies +* Gem will now have her class set to "Sorceress" in campaigns +* Fixed missing names for heroes who have their names customized in map after being transferred to the next scenario +* Artifact transfer will now work correctly if the hero holding the transferable artifact is not also transferring +* Fixed crash on opening of some campaigns in the French version from gog.com +* It is now possible to replay the intro movie from the scenario information window +* When playing the intro video, the subtitles are now correctly synchronized with the audio + +### Battles +* Added option to enable unlimited combat replays during game setup +* Added option to instantly end battle using quick combat (shotcut: 'e') +* Added option to replace auto-combat button action with instant end using quick combat +* Battles against AI players can now be done using quick combat +* Disabling battle queue will now correctly reposition hero statistics preview popup +* Fixed positioning of unit stack size label + +### Launcher +* Added Spanish translation to launcher + +### Map Editor +* Added Chinese translation to map editor + +### AI +* Fixed possible crash on updating NKAI pathfinding data +* Fixed counting mana usage cost of Fly spell +* Added estimation of value of Pyramid and Cyclops Stockpile + +### Modding +* Added new game setting that allows inviting heroes to taverns +* Fixed reversed Overlord and Warlock classes mapping + +# 1.4.4 -> 1.4.5 + +### Stability +* Fixed crash on creature spellcasting +* Fixed crash on unit entering magical obstacles such as quicksands +* Fixed freeze on map loading on some systems +* Fixed crash on attempt to start campaign with unsupported map +* Fixed crash on opening creature information window with invalid SPELL_IMMUNITY bonus + +### Random Maps Generator +* Fixed placement of guards sometimes resulting into open connection into third zone +* Fixed rare crash on multithreaded access during placement of artifacts or wandering monsters + +### Map Editor +* Fixed inspector using wrong editor for some values + +### AI +* Fixed bug leading to AI not attacking wandering monsters in some cases +* Fixed crash on using StupidAI for autocombat or for enemy players + +# 1.4.3 -> 1.4.4 + +### General +* Fixed crash on generation of random maps + +# 1.4.2 -> 1.4.3 + +### General +* Fixed the synchronisation of the audio and video of the opening movies. +* Fixed a bug that caused spells from mods to not show up in the Mage's Guild. +* Changed the default SDL driver on Windows from opengl to autodetection +* When a hero visits a town with a garrisoned hero, they will now automatically exchange spells if one of them has the Scholar skill. +* Movement and mana points are now replenished for new heroes in taverns. + +### Multiplayer +* Simturn contact detection will now correctly check for hero moving range +* Simturn contact detection will now ignore wandering monsters +* Right-clicking the Simturns AI option now displays a tooltip +* Interaction attempts with other players during simturns will now have more concise error messages +* Turn timers are now limited to 24 hours in order to prevent bugs caused by an integer overflow. +* Fixed delays when editing turn timer duration +* Ending a turn during simturns will now block the interface correctly. + +### Campaigns +* Player will no longer start the United Front of Song for the Father campaign with two Nimbuses. +* Fixed missing campaign description after loading saved game +* Campaign completion checkmarks will now be displayed after the entire campaign has been completed, rather than just after the first scenario. +* Fixed positioning of prologue and epilogue text during campaign scenario intros + +### Interface +* Added an option to hide adventure map window when town or battle window are open +* Fixed switching between pages on small version of spellbook +* Saves with long filenames are now truncated in the UI to prevent overflow. +* Added option to sort saved games by change date +* Game now shows correct resource when selecting start bonus +* It is now possible to inspect commander skills during battles. +* Fixed incorrect cursor being displayed when hovering over navigable water tiles +* Fixed incorrect cursor display when hovering over water objects accessible from shore + +### Stability +* Fixed a crash when using the 'vcmiobelisk' cheat more than once. +* Fixed crash when reaching level 201. The maximum level is now limited to 197. +* Fixed crash when accessing a spell with an invalid SPELLCASTER bonus +* Fixed crash when trying to play music for an inaccessible tile +* Fixed memory corruption on loading of old mods with illegal 'index' field +* Fixed possible crash on server shutdown on Android +* Fixed possible crash when the affinity of the hero class is set to an invalid value +* Fixed crash on invalid creature in hero army due to outdated or broken mods +* Failure to initialise video subsystem now displays error message instead of silent crash + +### Random Maps Generator +* Fixed possible creation of a duplicate hero in a random map when the player has chosen the starting hero. +* Fixed banning of quest artifacts on random maps +* Fixed banning of heroes in prison on random maps + +### Battles +* Battle turn queue now displays current turn +* Added option to show unit statistics sidebar in battle +* Right-clicking on a unit in the battle turn queue now displays the unit details popup. +* Fixed error messages for SUMMON_GUARDIANS and TRANSMUTATION bonuses +* Fixed Dendroid Bind ability +* Black Dragons no longer hate Giants, only Titans +* Spellcasting units such as Archangels can no longer cast spells on themselves. +* Coronius specialty will now correctly select affected units + +### Launcher +* Welcome screen will automatically detect existing Heroes 3 installation on Windows +* It is now possible to install mods by dragging and dropping onto the launcher. +* It is now possible to install maps and campaigns by dragging and dropping onto the launcher. +* Czech launcher translation added +* Added option to select preferred SDL driver in launcher + +### Map Editor +* Fixed saving of allowed abilities, spells, artifacts or heroes + +### AI +* AI will no longer attempt to move immobilized units, such as those under the effect of Dendroid Bind. +* Fixed shooters not shooting when they have a range penalty +* Fixed Fire Elemental spell casting +* Fixed rare bug where unit would sometimes do nothing in battle + +### Modding +* Added better reporting of "invalid identifiers" errors with suggestions on how to fix them +* Added FEROCITY bonus (HotA Aysiud) +* Added ENEMY_ATTACK_REDUCTION bonus (HotA Nix) +* Added REVENGE bonus (HotA Haspid) +* Extended DEATH_STARE bonus to support Pirates ability (HotA) +* DEATH_STARE now supports spell ID in addInfo field to override used spell +* SPELL_BEFORE_ATTACK bonus now supports spell priorities +* FIRST_STRIKE bonus supports subtypes damageTypeMelee, damageTypeRanged and damageTypeAll +* BLOCKS_RETALIATION now also blocks FIRST_STRIKE bonus +* Added 'canCastOnSelf' field for spells to allow creatures to cast spells on themselves. + +# 1.4.1 -> 1.4.2 + +### General +* Restored support for Windows 7 +* Restored support for 32-bit builds +* Implemented quick backpack window for slot-specific artifact selection, activated via mouse wheel / swipe gesture +* Added option to search for specific spell in the spellbook +* Added option to skip fading animation on adventure map +* Using alt-tab to switch to another application will no longer activate in-game console/chat +* Increased frequency of checks for server startup to improve server connection time +* added nwcfollowthewhiterabbit / vcmiluck cheat: the currently selected hero permanently gains maximum luck. +* added nwcmorpheus / vcmimorale cheat: the currently selected hero permanently gains maximum morale. +* added nwcoracle / vcmiobelisk cheat: the puzzle map is permanently revealed. +* added nwctheone / vcmigod cheat: reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight to currently selected hero + +### Launcher +* Launcher will now properly show mod installation progress +* Launcher will now correctly select preferred language on first start + +### Multiplayer +* Timers for all players will now be visible at once +* Turn options menu will correctly open for guests when host switches to it +* Guests will correctly see which roads are allowed for random maps by host +* Game will now correctly deactivate unit when timer runs out in pvp battle +* Game will show turn, battle and unit timers separately during battles +* Timer in pvp battles will be only active if unit timer is non-zero +* Timer during adventure map turn will be active only if turn timer is non-zero +* Game will now send notifications to players when simultaneous turns end + +### Stability +* Fixed crash on clicking town or hero list on MacOS and iOS +* Fixed crash on closing vcmi on Android +* Fixed crash on disconnection from multiplayer game +* Fixed crash on finishing game on last day of the month +* Fixed crash on loading h3m maps with mods that alter Witch Hut, Shrine or Scholar +* Fixed crash on opening creature morale detalisation in some localizations +* Fixed possible crash on starting a battle when opening sound from previous battle is still playing +* Fixed crash on map loading in case if there is no suitable option for a random dwelling +* Fixed crash on usage of radial wheel to reorder towns or heroes +* Fixed possible crash on random map generation +* Fixed crash on attempting to transfer last creature when stack experience is enabled +* Fixed crash on accessing invalid settings options +* Fixed server crash on receiving invalid message from player +* Added check for presence of Armageddon Blade campaign files to avoid crash on some Heroes 3 versions + +### Random Maps Generator +* Improved performance of random maps generation +* Rebalance of treasure values and density +* Improve junction zones generation by spacing Monoliths +* Reduced amount of terrain decorations to level more in line with H3 +* Generator will now avoid path routing near map border +* Generator will now check full object area for minimum distance requirement +* Fixed routing of roads behind Subterranean Gates, Monoliths and Mines +* Fixed remaining issues with placement of Corpse +* Fixed placement of one-tile prisons from HotA +* Fixed spawning of Armageddon's Blade and Vial of Dragon Blood on random maps + +### Interface +* Right-clicking hero icon during levelup dialog will now show hero status window +* Added indicator of current turn to unit turn order panel in battles +* Reduces upscaling artifacts on large spellbook +* Game will now display correct date of saved games on Android +* Fixed black screen appearing during spellbook page flip animation +* Fixed description of "Start map with hero" bonus in campaigns +* Fixed invisible chat text input in game lobby +* Fixed positioning of chat history in game lobby +* "Infobar Creature Management" option is now enabled by default +* "Large Spellbook" option is now enabled by default + +### Mechanics +* Anti-magic garrison now actually blocks spell casting +* Berserk spell will no longer cancel if affected unit performs counterattack +* Frenzy spell can no longer be casted on units that should be immune to it +* Master Genie will no longer attempt to cast beneficial spell on creatures immune to it +* Vitality and damage skills of a commander will now correctly grow with level + +### Modding +* Added UNTIL_OWN_ATTACK duration type for bonuses +* Configurable objects with visit mode "first" and "random" now respect "canRefuse" flag + +# 1.4.0 -> 1.4.1 + +### General +* Fixed position for interaction with starting heroes +* Fixed smooth map scrolling when running at high framerate +* Fixed calculation of Fire Shield damage when caster has artifacts that increase its damage +* Fixed untranslated message when visiting signs with random text +* Fixed slider scrolling to maximum value when clicking on "scroll right" button +* Fixed events and seer huts not activating in some cases +* Fixed bug leading to Artifact Merchant selling Grails in loaded saved games +* Fixed placement of objects in random maps near the top border of the map +* Creatures under Slayer spell will no longer deal additional damage to creatures not affected by Slayer +* Description of a mod in Launcher will no longer be converted to lower-case +* Game will no longer fail to generate random map when AI-only players option is set to non-zero value +* Added option to mute audio when VCMI window is not active +* Added option to disable smooth map scrolling +* Reverted ban on U-turns in pathfinder + +### Stability +* Fixed crash on using mods made for VCMI 1.3 +* Fixed crash on generating random map with large number of monoliths +* Fixed crash on losing mission-critical hero in battle +* Fixed crash on generating growth detalization in some localizations +* Fixed crash on loading of some user-made maps + +# 1.3.2 -> 1.4.0 + +### General +* Implemented High Score screen +* Implemented tracking of completed campaigns +* "Secret" Heroes 3 campaigns now require completion of prerequisite campaigns first +* Completing a campaign will now return player to campaign selection window instead of main menu +* Game will now play video on winning or losing a game +* Game will now correctly check for mod compatibility when loading saved games +* Game client will no longer load conflicting mods if player have both of them enabled +* If some mods fail to load due to missing dependencies or conflicts, game client will display message on opening main menu +* Game will no longer crash on loading save with different mod versions and will show error message instead +* Saved games are now 2-3 times smaller than before +* Added Vietnamese translation +* Failure to connect to a MP game will now show proper error message +* Added VSync support +* Implemented tutorial +* Implemented support for playback of audio from video files +* Windows Installer will now automatically add required firewall rules +* Game audio will now be disabled if game window is not focused +* Fixed formatting of date and time of a savegame on Android +* Added list of VCMI authors to credits screen +* Quick combat is now disabled by default +* Spectator mode in single player is now disabled + +### Multiplayer +* Implemented simultaneous turns +* Implemented turn timers, including chess timers version +* Game will now hide entire adventure map on hotseat turn transfer +* Added option to pause game timer while on system options window +* Implemented localization support for maps +* Game will now use texts from local player instead of host +* Multiple fixes to validation of player requests by server + +### Android +* Heroes 3 data import now accepts files in any case +* Fixed detection of Heroes 3 data presence when 'data' directory uses lower case + +### Touchscreen +* Added tutorial video clips that explain supported touch gestures +* Double tap will now be correctly interpreted as double click, e.g. to start scenario via double-click +* Implemented snapping to 100% scale for adventure map zooming +* Implemented smooth scrolling for adventure map +* Implemented radial wheel to reorder list of owned towns and heroes +* Implemented radial wheel for hero exchange in towns + +### Launcher +* When a mod is being downloaded, the launcher will now correctly show progress as well as its total size +* Double-clicking mod name will now perform expected action, e.g. install/update/enable or disable +* Launcher will now show mod extraction progress instead of freezing +* "Friendly AI" option will now correctly display current type of friendly AI +* Player can now correctly switch to global chat after disconnect +* "Resolve mods conflicts" button now attempts to fix all mods if nothing is selected +* Implemented support for mention in game lobby +* Implemented support for global and room channels in game lobby +* Added option to reconnect to game lobby + +### Editor +* It is now possible to configure rewards for Seer Hut, Pandora Boxes and Events +* It is now possible to configure quest (limiter) in Seer Hut and Quest Guards +* It is now possible to configure events and rumors in map editor +* Improved army configuration interface +* Added option to customize hero skills +* It is now possible to select object on map for win/loss conditions or for main town +* Random dwellings can now be linked to a random town +* Added map editor zoom +* Added objects lock functionality +* It is now possible to configure hero placeholders in map editor +* Fixed duplicate artifact image on mouse drag +* Lasso tool will no longer skip tiles +* Fixed layout of roads and rivers + +### Stability +* Fix possible crash on generating random map +* Fixed multiple memory leaks in game client +* Fixed crash on casting Hypnotize multiple times +* Fixed crash on attempting to move all artifacts from hero that has no artifacts +* Fixed crash on attempting to load corrupted .def file +* Fixed crash on clicking on empty Altar of Sacrifice slots + +### AI +* BattleAI should now see strong stacks even if blocked by weak stacks. +* BattleAI will now prefers targets slower than own stack even if they are not reachable this turn. +* Improved BattleAI performance when selecting spell to cast +* Improved BattleAI performance when selection unit action +* Improved BattleAI spell selection logic +* Nullkiller AI can now use Fly and Water Walk spells + +### Campaigns +* Implemented voice-over audio support for Heroes 3 campaigns +* Fixes victory condition on 1st scenario of "Long Live the King" campaign +* Fixed loading of defeat/victory icon and message for some campaign scenarios + +### Interface +* Implemented adventure map dimming on opening windows +* Clicking town hall icon on town screen will now open town hall +* Clicking buildings in town hall will now show which resources are missing (if any) +* Fixed incorrect positioning of total experience text on Altar of Sacrifice +* Game will now show correct video file on battle end +* Game will now correctly loop battle end animation video +* Implemented larger version of spellbooks that displays up to 24 spells at once +* Spell scrolls in hero inventory now show icon of contained spell +* Fixed incorrect hero morale tooltip after visiting adventure map objects +* Fixed incorrect information for skills in hero exchange window +* Confirmation button will now be disabled on automatic server connect dialog +* Attempting to recruit creature in town with no free slots in garrisons will now correctly show error message + +### Main Menu +* Implemented window for quick selection of starting hero, town and bonus +* Implemented map preview in scenario selection and game load screen accessible via right click on map +* Show exact map size in map selection +* Added support for folders in scenario selection and save/load screens +* Added support for "Show Random Maps" button in random map setup screen +* Added starting hero preview screen +* Added option to change name of player while in map setup screen +* Implemented loading screen with progress bar +* Game will now stay on loading screen while random map generation is in process +* Team Alignments popup in scenario options will no longer show empty teams +* Fixed missing borders on team alignments configuration window in random maps +* Fixed map difficulty icon on save/load screen +* Main menu animation will no longer appear on top of new game / load game text + +### Adventure Map Interface +* Picking up an artifact on adventure map will now show artifact assembly dialog if such option exists +* Minimap will now preserve correct aspect ratio on rectangular maps +* Fixed slot highlighting when an artifact is being assembled +* Ctrl-click on hero will now select him instead of changing path of active hero +* In selection windows (level up window, treasure chest pickup, etc) it is now possible to select reward via double-click +* Attacking wandering monsters with preconfigured message will now correctly show the message +* Revisit object button will now be blocked if there is no object to revisit +* Fixed missing tooltip for "revisit object" button +* Fixed calculation of fow reveal range for all objects +* Attempt to close game will now ask for confirmation +* Right-clicking previously visited Seer Huts or Quest Guards will show icon with preview of quest goal +* Right-clicking owned dwellings will show amount of creatures available to for recruitment +* Right-clicking previously visited creature banks will show exact guards composition with their portraits +* Right-clicking artifacts on map will show artifact description +* Right-clicking objects that give bonus to hero will show object description + +### Mechanics +* Heroes in tavern will correctly lose effects from spells or visited objects on new day +* Fixed multiple bugs in offering of Wisdom and Spell Schools on levelup. Mechanic should now work identically to Heroes 3 +* Retreated heroes will no longer restore their entire mana pool on new day +* Fixed Grail in Crypt on some custom maps +* Added support for repeatable quests in Seer Huts +* Using "Sacrifice All" on Altar will now correctly place all creatures but one on altar +* Fixed probabilities of luck and morale +* Blinded stack no longer can get morale +* Creature that attacks while standing in moat will now correctly receive moat damage +* Player resources are now limited to 1 000 000 000 to prevent overflow +* It is no longer possible to escape from town without fort +* Pathfinder will no longer make U-turns when moving onto visitable objects while flying +* Pathfinder will no longer make paths that go over teleporters without actually using them +* Game will now correctly update guard status of tiles that are guarded by multiple wandering monsters +* Moving onto Garrisons and Border Guards entrance tiles that are guarded by wandering monsters will now correctly trigger battle +* It is no longer possible to build second boat in shipyard when shipyard should be blocked by boat with hero +* Gundula is now Offense specialist and not Sorcery, as in H3 + +### Random Maps Generator +* Increased tolerance for placement of Subterranean Gates +* Game will now select random object template out of available options instead of picking first one +* It is no longer possible to create map with a single team +* Game will no longer route roads through non-removable treasure objects, such as Corpse +* Fixed placement of treasure piles with non-removable objects, such as Corpse +* Fixed interface no displaying correct random map settings in some cases +* Fixed misleading error "no info for player X found" +* Fixed bug leading to AI players defeated on day one. + +### Modding +* All bonuses now require string as a subtype. See documentation for exact list of possible strings for each bonus. +* Changes to existing objects parameters in mods will now be applied to ongoing saves +* Fixed handling of engine version compatibility check +* Added support for giving arbitrary bonuses to AI players +* Most mods of type "Translation" are now hidden in Launcher +* Added new mod type: "Compatibility". Mods of this type are hidden in Launcher and are always active if they are compatible. +* Added new mod type: "Maps" +* Added new TERRAIN_NATIVE bonus that makes any terrain native to affected units +* SPELL_DURATION now allows subtypes. If set to spell, bonus will only affect specified spell +* Both game client and launcher will now correctly handle dependencies that are not in lower case +* Implemented support for refusable Witch Hut and Scholar +* Added "variables" to configurable objects that are shared between all rewards +* Added visit mode "limiter" for configurable objects. Hero will be considered as "visited this object" if he fulfills provided condition +* Added option to customize text displayed for visited objects, e.g. show "Already learned" instead of "Visited" +* Added option to define custom description of configurable object, accessible via right-click +* Added option to show object content icons on right-click +* Object now allows checking whether hero can learn spell +* Object limiter now allows checking whether hero can learn skill +* Object reward may now reveal terrain around visiting hero (e.g. Redwood Observatory) + # 1.3.1 -> 1.3.2 ### GENERAL diff --git a/Global.h b/Global.h index e3987bbbe..3ef1a64e4 100644 --- a/Global.h +++ b/Global.h @@ -41,6 +41,10 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); # define VCMI_UNIX # define VCMI_XDG # define VCMI_FREEBSD +#elif defined(__OpenBSD__) +# define VCMI_UNIX +# define VCMI_XDG +# define VCMI_OPENBSD #elif defined(__HAIKU__) # define VCMI_UNIX # define VCMI_XDG @@ -100,6 +104,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #define _USE_MATH_DEFINES +#ifndef NDEBUG +// Enable additional debug checks from glibc / libstdc++ when building with enabled assertions +// Since these defines must be declared BEFORE including glibc header we can not check for __GLIBCXX__ macro to detect that glibc is in use +# define _GLIBCXX_ASSERTIONS +#endif + #include #include #include @@ -675,6 +685,23 @@ namespace vstd return a + (b - a) * f; } + template + bool isAlmostZero(const Floating & value) + { + constexpr Floating epsilon(0.00001); + return std::abs(value) <= epsilon; + } + + template + bool isAlmostEqual(const Floating1 & left, const Floating2 & right) + { + using Floating = decltype(left + right); + constexpr Floating epsilon(0.00001); + const Floating relativeEpsilon = std::max(std::abs(left), std::abs(right)) * epsilon; + const Floating value = std::abs(left - right); + return value <= relativeEpsilon; + } + ///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr static constexpr int abs(int i) { if(i < 0) return -i; diff --git a/Mods/vcmi/Sprites/heroWindow/artifactSlotEmpty.png b/Mods/vcmi/Data/heroWindow/artifactSlotEmpty.png similarity index 100% rename from Mods/vcmi/Sprites/heroWindow/artifactSlotEmpty.png rename to Mods/vcmi/Data/heroWindow/artifactSlotEmpty.png diff --git a/Mods/vcmi/Sprites/buttons/backpackButtonIcon.png b/Mods/vcmi/Data/heroWindow/backpackButtonIcon.png similarity index 100% rename from Mods/vcmi/Sprites/buttons/backpackButtonIcon.png rename to Mods/vcmi/Data/heroWindow/backpackButtonIcon.png diff --git a/Mods/vcmi/Data/heroWindow/commanderButtonIcon.png b/Mods/vcmi/Data/heroWindow/commanderButtonIcon.png new file mode 100644 index 000000000..aa32487fd Binary files /dev/null and b/Mods/vcmi/Data/heroWindow/commanderButtonIcon.png differ diff --git a/Mods/vcmi/Data/lobby/selectionTabSortDate.png b/Mods/vcmi/Data/lobby/selectionTabSortDate.png new file mode 100644 index 000000000..4e58b2838 Binary files /dev/null and b/Mods/vcmi/Data/lobby/selectionTabSortDate.png differ diff --git a/Mods/vcmi/Data/settingsWindow/checkBoxEmpty.png b/Mods/vcmi/Data/settingsWindow/checkBoxEmpty.png deleted file mode 100644 index f2eae977f..000000000 Binary files a/Mods/vcmi/Data/settingsWindow/checkBoxEmpty.png and /dev/null differ diff --git a/Mods/vcmi/Data/settingsWindow/lineHorizontal.png b/Mods/vcmi/Data/settingsWindow/lineHorizontal.png deleted file mode 100644 index 5eb70f1ff..000000000 Binary files a/Mods/vcmi/Data/settingsWindow/lineHorizontal.png and /dev/null differ diff --git a/Mods/vcmi/Data/settingsWindow/lineVertical.png b/Mods/vcmi/Data/settingsWindow/lineVertical.png deleted file mode 100644 index f5c337938..000000000 Binary files a/Mods/vcmi/Data/settingsWindow/lineVertical.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/buttons/backpack.json b/Mods/vcmi/Sprites/buttons/backpack.json deleted file mode 100644 index a74a8c3a1..000000000 --- a/Mods/vcmi/Sprites/buttons/backpack.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "basepath" : "buttons/", - "images" : - [ - { "frame" : 0, "file" : "backpackNormal.png"}, - { "frame" : 1, "file" : "backpackPressed.png"} - ] -} diff --git a/Mods/vcmi/Sprites/buttons/backpackNormal.png b/Mods/vcmi/Sprites/buttons/backpackNormal.png deleted file mode 100644 index 6ea22798c..000000000 Binary files a/Mods/vcmi/Sprites/buttons/backpackNormal.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/buttons/backpackPressed.png b/Mods/vcmi/Sprites/buttons/backpackPressed.png deleted file mode 100644 index 2de1c6be2..000000000 Binary files a/Mods/vcmi/Sprites/buttons/backpackPressed.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/buttons/commander.json b/Mods/vcmi/Sprites/buttons/commander.json deleted file mode 100644 index a5dacd51c..000000000 --- a/Mods/vcmi/Sprites/buttons/commander.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "basepath" : "buttons/", - "images" : - [ - { "frame" : 0, "file" : "commanderNormal.png"}, - { "frame" : 1, "file" : "commanderPressed.png"} - ] -} diff --git a/Mods/vcmi/Sprites/buttons/commanderNormal.png b/Mods/vcmi/Sprites/buttons/commanderNormal.png deleted file mode 100644 index 9d91a3a3f..000000000 Binary files a/Mods/vcmi/Sprites/buttons/commanderNormal.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/buttons/commanderPressed.png b/Mods/vcmi/Sprites/buttons/commanderPressed.png deleted file mode 100644 index b0b75f54e..000000000 Binary files a/Mods/vcmi/Sprites/buttons/commanderPressed.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/lobby/checkbox.json b/Mods/vcmi/Sprites/lobby/checkbox.json new file mode 100644 index 000000000..bfb1e6587 --- /dev/null +++ b/Mods/vcmi/Sprites/lobby/checkbox.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "checkboxBlueOff.png"}, + { "frame" : 1, "file" : "checkboxBlueOn.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png new file mode 100644 index 000000000..8e611ac95 Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png differ diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png new file mode 100644 index 000000000..580941806 Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png differ diff --git a/Mods/vcmi/Sprites/lobby/checkboxOff.png b/Mods/vcmi/Sprites/lobby/checkboxOff.png new file mode 100644 index 000000000..d650018dc Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxOff.png differ diff --git a/Mods/vcmi/Sprites/lobby/checkboxOn.png b/Mods/vcmi/Sprites/lobby/checkboxOn.png new file mode 100644 index 000000000..cd6e46308 Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxOn.png differ diff --git a/Mods/vcmi/Sprites/lobby/dropdown.json b/Mods/vcmi/Sprites/lobby/dropdown.json new file mode 100644 index 000000000..3a6d41203 --- /dev/null +++ b/Mods/vcmi/Sprites/lobby/dropdown.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "dropdownNormal.png"}, + { "frame" : 1, "file" : "dropdownPressed.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/dropdownNormal.png b/Mods/vcmi/Sprites/lobby/dropdownNormal.png new file mode 100644 index 000000000..71dc20b3d Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/dropdownNormal.png differ diff --git a/Mods/vcmi/Sprites/lobby/dropdownPressed.png b/Mods/vcmi/Sprites/lobby/dropdownPressed.png new file mode 100644 index 000000000..48ee1358f Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/dropdownPressed.png differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button190.json b/Mods/vcmi/Sprites/settingsWindow/button190.json deleted file mode 100644 index 0af55c417..000000000 --- a/Mods/vcmi/Sprites/settingsWindow/button190.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "basepath" : "settingsWindow/", - "images" : - [ - { "frame" : 0, "file" : "button190Normal.png"}, - { "frame" : 1, "file" : "button190PressedSelected.png"}, - { "frame" : 2, "file" : "button190Pressed.png"}, - { "frame" : 3, "file" : "button190NormalSelected.png"} - ] -} diff --git a/Mods/vcmi/Sprites/settingsWindow/button190Normal.png b/Mods/vcmi/Sprites/settingsWindow/button190Normal.png deleted file mode 100644 index f120bf294..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button190Normal.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button190NormalSelected.png b/Mods/vcmi/Sprites/settingsWindow/button190NormalSelected.png deleted file mode 100644 index f84b59e8e..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button190NormalSelected.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button190Pressed.png b/Mods/vcmi/Sprites/settingsWindow/button190Pressed.png deleted file mode 100644 index 04821e4ce..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button190Pressed.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button190PressedSelected.png b/Mods/vcmi/Sprites/settingsWindow/button190PressedSelected.png deleted file mode 100644 index 96d453bdb..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button190PressedSelected.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button32.json b/Mods/vcmi/Sprites/settingsWindow/button32.json deleted file mode 100644 index fb532f0f8..000000000 --- a/Mods/vcmi/Sprites/settingsWindow/button32.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "basepath" : "settingsWindow/", - "images" : - [ - { "frame" : 0, "file" : "button32Normal.png"}, - { "frame" : 1, "file" : "button32PressedSelected.png"}, - { "frame" : 2, "file" : "button32Pressed.png"}, - { "frame" : 3, "file" : "button32NormalSelected.png"} - ] -} diff --git a/Mods/vcmi/Sprites/settingsWindow/button32Normal.png b/Mods/vcmi/Sprites/settingsWindow/button32Normal.png deleted file mode 100644 index a4f21f956..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button32Normal.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button32NormalSelected.png b/Mods/vcmi/Sprites/settingsWindow/button32NormalSelected.png deleted file mode 100644 index 79aac1e2b..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button32NormalSelected.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button32Pressed.png b/Mods/vcmi/Sprites/settingsWindow/button32Pressed.png deleted file mode 100644 index dcfeb03e1..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button32Pressed.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button32PressedSelected.png b/Mods/vcmi/Sprites/settingsWindow/button32PressedSelected.png deleted file mode 100644 index 5ed89e784..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button32PressedSelected.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button46.json b/Mods/vcmi/Sprites/settingsWindow/button46.json deleted file mode 100644 index 1d6ade89f..000000000 --- a/Mods/vcmi/Sprites/settingsWindow/button46.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "basepath" : "settingsWindow/", - "images" : - [ - { "frame" : 0, "file" : "button46Normal.png"}, - { "frame" : 1, "file" : "button46PressedSelected.png"}, - { "frame" : 2, "file" : "button46Pressed.png"}, - { "frame" : 3, "file" : "button46NormalSelected.png"} - ] -} diff --git a/Mods/vcmi/Sprites/settingsWindow/button46Normal.png b/Mods/vcmi/Sprites/settingsWindow/button46Normal.png deleted file mode 100644 index e90e6b820..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button46Normal.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button46NormalSelected.png b/Mods/vcmi/Sprites/settingsWindow/button46NormalSelected.png deleted file mode 100644 index 0b81dc96b..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button46NormalSelected.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button46Pressed.png b/Mods/vcmi/Sprites/settingsWindow/button46Pressed.png deleted file mode 100644 index edc181305..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button46Pressed.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button46PressedSelected.png b/Mods/vcmi/Sprites/settingsWindow/button46PressedSelected.png deleted file mode 100644 index f6457d06d..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button46PressedSelected.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button80.json b/Mods/vcmi/Sprites/settingsWindow/button80.json deleted file mode 100644 index 50cb97a40..000000000 --- a/Mods/vcmi/Sprites/settingsWindow/button80.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "basepath" : "settingsWindow/", - "images" : - [ - { "frame" : 0, "file" : "button80Normal.png"}, - { "frame" : 1, "file" : "button80PressedSelected.png"}, - { "frame" : 2, "file" : "button80Pressed.png"}, - { "frame" : 3, "file" : "button80NormalSelected.png"} - ] -} diff --git a/Mods/vcmi/Sprites/settingsWindow/button80Normal.png b/Mods/vcmi/Sprites/settingsWindow/button80Normal.png deleted file mode 100644 index 6ca45a176..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button80Normal.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button80NormalSelected.png b/Mods/vcmi/Sprites/settingsWindow/button80NormalSelected.png deleted file mode 100644 index c34047b52..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button80NormalSelected.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button80Pressed.png b/Mods/vcmi/Sprites/settingsWindow/button80Pressed.png deleted file mode 100644 index 13758a6f4..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button80Pressed.png and /dev/null differ diff --git a/Mods/vcmi/Sprites/settingsWindow/button80PressedSelected.png b/Mods/vcmi/Sprites/settingsWindow/button80PressedSelected.png deleted file mode 100644 index 47cbb0447..000000000 Binary files a/Mods/vcmi/Sprites/settingsWindow/button80PressedSelected.png and /dev/null differ diff --git a/Mods/vcmi/Video/tutorial/AbortSpell.webm b/Mods/vcmi/Video/tutorial/AbortSpell.webm new file mode 100644 index 000000000..6aa7e8b9b Binary files /dev/null and b/Mods/vcmi/Video/tutorial/AbortSpell.webm differ diff --git a/Mods/vcmi/Video/tutorial/BattleDirection.webm b/Mods/vcmi/Video/tutorial/BattleDirection.webm new file mode 100644 index 000000000..67b3407fa Binary files /dev/null and b/Mods/vcmi/Video/tutorial/BattleDirection.webm differ diff --git a/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm b/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm new file mode 100644 index 000000000..ecbf8fbf4 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm differ diff --git a/Mods/vcmi/Video/tutorial/MapPanning.webm b/Mods/vcmi/Video/tutorial/MapPanning.webm new file mode 100644 index 000000000..90ad7da27 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/MapPanning.webm differ diff --git a/Mods/vcmi/Video/tutorial/MapZooming.webm b/Mods/vcmi/Video/tutorial/MapZooming.webm new file mode 100644 index 000000000..8f3a0e3d3 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/MapZooming.webm differ diff --git a/Mods/vcmi/Video/tutorial/RadialWheel.webm b/Mods/vcmi/Video/tutorial/RadialWheel.webm new file mode 100644 index 000000000..f91e87f39 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/RadialWheel.webm differ diff --git a/Mods/vcmi/Video/tutorial/RightClick.webm b/Mods/vcmi/Video/tutorial/RightClick.webm new file mode 100644 index 000000000..bd60061da Binary files /dev/null and b/Mods/vcmi/Video/tutorial/RightClick.webm differ diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index 70c7b3275..d84af049f 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -20,6 +20,7 @@ "vcmi.adventureMap.playerAttacked" : "玩家遭受攻击: %s", "vcmi.adventureMap.moveCostDetails" : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "抱歉,重放对手行动功能目前暂未实现!", "vcmi.capitalColors.0" : "红色", "vcmi.capitalColors.1" : "蓝色", @@ -36,31 +37,56 @@ "vcmi.heroOverview.spells" : "魔法", "vcmi.radialWheel.mergeSameUnit" : "合并相同生物", - "vcmi.radialWheel.showUnitInformation" : "显示生物信息", + "vcmi.radialWheel.fillSingleUnit" : "单个生物填充空格", "vcmi.radialWheel.splitSingleUnit" : "分割单个生物", "vcmi.radialWheel.splitUnitEqually" : "平均分配生物", "vcmi.radialWheel.moveUnit" : "将生物移动到部队", "vcmi.radialWheel.splitUnit" : "分割生物到其他空位", + "vcmi.radialWheel.heroGetArmy" : "移动生物", + "vcmi.radialWheel.heroSwapArmy" : "交换生物", + "vcmi.radialWheel.heroExchange" : "开启英雄交换", + "vcmi.radialWheel.heroGetArtifacts" : "移动宝物", + "vcmi.radialWheel.heroSwapArtifacts" : "交换宝物", + "vcmi.radialWheel.heroDismiss" : "解雇英雄", + + "vcmi.radialWheel.moveTop" : "移到顶端", + "vcmi.radialWheel.moveUp" : "上移", + "vcmi.radialWheel.moveDown" : "下移", + "vcmi.radialWheel.moveBottom" : "移到底端", + + "vcmi.spellBook.search" : "搜索中...", + "vcmi.mainMenu.serverConnecting" : "连接中...", "vcmi.mainMenu.serverAddressEnter" : "使用地址:", + "vcmi.mainMenu.serverConnectionFailed" : "连接失败", "vcmi.mainMenu.serverClosing" : "关闭中...", "vcmi.mainMenu.hostTCP" : "创建TCP/IP游戏", "vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏", "vcmi.mainMenu.playerName" : "Player", - "vcmi.lobby.filename" : "文件名", + "vcmi.lobby.filepath" : "文件路径", "vcmi.lobby.creationDate" : "创建时间", + "vcmi.lobby.scenarioName" : "场景名称", + "vcmi.lobby.mapPreview" : "地图预览", + "vcmi.lobby.noPreview" : "无地上部分", + "vcmi.lobby.noUnderground" : "无地下部分", + "vcmi.lobby.sortDate" : "以修改时间排序地图", + "vcmi.client.errors.invalidMap" : "{非法地图或战役}\n\n启动游戏失败,选择的地图或者战役,无效或被污染。原因:\n%s", + "vcmi.client.errors.missingCampaigns" : "{找不到数据文件}\n\n没有找到战役数据文件!你可能使用了不完整或损坏的英雄无敌3数据文件,请重新安装数据文件。", "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", "vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}", "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", + "vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n", + "vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n", + "vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!", "vcmi.settingsMainWindow.generalTab.hover" : "常规", - "vcmi.settingsMainWindow.generalTab.help" : "切换到“常规”选项卡 - 设置游戏客户端呈现", + "vcmi.settingsMainWindow.generalTab.help" : "切换到“常规”选项卡 - 配置客户端常规内容", "vcmi.settingsMainWindow.battleTab.hover" : "战斗", - "vcmi.settingsMainWindow.battleTab.help" : "切换到“战斗”选项卡 - 这些设置允许配置战斗界面和相关内容", + "vcmi.settingsMainWindow.battleTab.help" : "切换到“战斗”选项卡 - 配置游戏战斗界面内容", "vcmi.settingsMainWindow.adventureTab.hover" : "冒险地图", "vcmi.settingsMainWindow.adventureTab.help" : "切换到“冒险地图”选项卡 - 冒险地图即玩家能操作英雄移动的界面", @@ -70,49 +96,59 @@ "vcmi.systemOptions.townsGroup" : "城镇画面", "vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)", - "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", - "vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (边框)", - "vcmi.systemOptions.fullscreenExclusive.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", - "vcmi.systemOptions.resolutionButton.hover" : "分辨率", - "vcmi.systemOptions.resolutionButton.help" : "{分辨率选择}\n\n改变游戏内的分辨率,更改后需要重启游戏使其生效。", - "vcmi.systemOptions.resolutionMenu.hover" : "分辨率选择", + "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。该模式下,游戏会始终和桌面分辨率保持一致,无视设置的分辨率。 ", + "vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (独占)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{全屏}\n\n选中时,VCMI将以独占全屏模式运行。该模式下,游戏会将显示器分辨率改变为设置值。", + "vcmi.systemOptions.resolutionButton.hover" : "分辨率: %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{分辨率选择}\n\n改变游戏内的分辨率。", + "vcmi.systemOptions.resolutionMenu.hover" : "选择分辨率", "vcmi.systemOptions.resolutionMenu.help" : "修改游戏运行时的分辨率。", - "vcmi.systemOptions.scalingButton.hover" : "界面大小: %p%", - "vcmi.systemOptions.scalingButton.help" : "{界面大小}\n\n改变界面的大小", - "vcmi.systemOptions.scalingMenu.hover" : "选择界面大小", - "vcmi.systemOptions.scalingMenu.help" : "改变游戏界面大小。", - "vcmi.systemOptions.longTouchButton.hover" : "触控间距: %d 毫秒", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{触控间距}\n\n使用触摸屏时,触摸屏幕指定持续时间(以毫秒为单位)后将出现弹出窗口。", - "vcmi.systemOptions.longTouchMenu.hover" : "选择触控间距", - "vcmi.systemOptions.longTouchMenu.help" : "改变触控间距。", + "vcmi.systemOptions.scalingButton.hover" : "界面缩放: %p%", + "vcmi.systemOptions.scalingButton.help" : "{界面缩放}\n\n改变用户界面的缩放比例。", + "vcmi.systemOptions.scalingMenu.hover" : "选择界面缩放", + "vcmi.systemOptions.scalingMenu.help" : "改变游戏内界面缩放。", + "vcmi.systemOptions.longTouchButton.hover" : "长触延迟: %d 毫秒", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{长触延迟}\n\n使用触摸屏时,长触屏幕一定时间后出现弹出窗口(单位:毫秒)。", + "vcmi.systemOptions.longTouchMenu.hover" : "选择长触延迟", + "vcmi.systemOptions.longTouchMenu.help" : "改变长触延迟。", "vcmi.systemOptions.longTouchMenu.entry" : "%d 毫秒", "vcmi.systemOptions.framerateButton.hover" : "显示FPS", - "vcmi.systemOptions.framerateButton.help" : "{显示FPS}\n\n打开/关闭在游戏窗口角落的FPS指示器。", + "vcmi.systemOptions.framerateButton.help" : "{显示FPS}\n\n切换在游戏窗口角落显FPS指示器。", "vcmi.systemOptions.hapticFeedbackButton.hover" : "触觉反馈", "vcmi.systemOptions.hapticFeedbackButton.help" : "{触觉反馈}\n\n切换触摸输入的触觉反馈。", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "界面增强", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{界面增强}\n\n显示所有界面增强内容,如大背包和魔法书等。", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{界面增强}\n\n显示所有界面优化增强,如背包按钮等。关闭该项以贴近经典模式。", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "扩展魔法书", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{扩展魔法书}\n\n启用更大的魔法书界面,每页展示更多魔法,但魔法书翻页特效在该模式下无法呈现。", + "vcmi.systemOptions.audioMuteFocus.hover" : "失去焦点时静音", + "vcmi.systemOptions.audioMuteFocus.help" : "{失去焦点时静音}\n\n当窗口失去焦点时静音,游戏内消息提示和新回合提示除外。", "vcmi.adventureOptions.infoBarPick.hover" : "在信息面板显示消息", - "vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。", + "vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n尽可能将来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。", "vcmi.adventureOptions.numericQuantities.hover" : "生物数量显示", "vcmi.adventureOptions.numericQuantities.help" : "{生物数量显示}\n\n以数字 A-B 格式显示不准确的敌方生物数量。", - "vcmi.adventureOptions.forceMovementInfo.hover" : "在状态栏中显示移动力", - "vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n不需要按ALT就可以显示移动力。", + "vcmi.adventureOptions.forceMovementInfo.hover" : "总是显示移动花费", + "vcmi.adventureOptions.forceMovementInfo.help" : "{总是显示移动花费}\n\n总是在状态栏中显示行动力花费(否则仅在按下ALT时显示)。", "vcmi.adventureOptions.showGrid.hover" : "显示网格", "vcmi.adventureOptions.showGrid.help" : "{显示网格}\n\n显示网格覆盖层,高亮冒险地图物件的边沿。", - "vcmi.adventureOptions.borderScroll.hover" : "滚动边界", - "vcmi.adventureOptions.borderScroll.help" : "{滚动边界}\n\n当光标靠近窗口边缘时滚动冒险地图。 可以通过按住 CTRL 键来禁用。", + "vcmi.adventureOptions.borderScroll.hover" : "边缘滚动", + "vcmi.adventureOptions.borderScroll.help" : "{边缘滚动}\n\n当光标靠近窗口边缘时滚动冒险地图。 可以通过按住 CTRL 键来禁用。", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "信息面板生物管理", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{信息面板生物管理}\n\n允许在信息面板中重新排列生物,而不是在默认组件之间循环。", "vcmi.adventureOptions.leftButtonDrag.hover" : "左键拖动地图", "vcmi.adventureOptions.leftButtonDrag.help" : "{左键拖动地图}\n\n启用后,按住左键移动鼠标将拖动冒险地图视图。", + "vcmi.adventureOptions.smoothDragging.hover" : "平滑地图拖动", + "vcmi.adventureOptions.smoothDragging.help" : "{平滑地图拖动}\n\n启用后,地图拖动会产生柔和的羽化效果。", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "关闭淡入淡出特效", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{关闭淡入淡出特效}\n\n启用后,跳过物体淡出或类似特效(资源收集,登船等)。设置此项能在渲染开销重时能够加快UI的响应,尤其是在PvP对战中。当移动速度被设置为最大时忽略此项设置。", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "将地图卷动速度设置为非常慢", "vcmi.adventureOptions.mapScrollSpeed5.help": "将地图卷动速度设置为非常快", "vcmi.adventureOptions.mapScrollSpeed6.help": "将地图卷动速度设置为即刻。", + "vcmi.adventureOptions.hideBackground.hover" : "隐藏背景", + "vcmi.adventureOptions.hideBackground.help" : "{隐藏背景}\n\n隐藏冒险地图背景,以显示贴图代替。", "vcmi.battleOptions.queueSizeLabel.hover": "回合顺序指示器", "vcmi.battleOptions.queueSizeNoneButton.hover": "关闭", @@ -135,10 +171,13 @@ "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{显示生物射程限制}\n\n当您将鼠标悬停在其上时,显示射手的射程限制。", "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "显示英雄统计数据窗口", "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{显示英雄统计数据窗口}\n\n永久切换并显示主要统计数据和法术点的英雄统计数据窗口。", - "vcmi.battleOptions.enableAutocombatSpells.hover": "魔法", - "vcmi.battleOptions.enableAutocombatSpells.help": "{魔法}\n\n快速战斗时不会使用魔法。", "vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐", "vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。", + "vcmi.battleOptions.endWithAutocombat.hover": "结束战斗", + "vcmi.battleOptions.endWithAutocombat.help": "{结束战斗}\n\n以自动战斗立即结束剩余战斗过程", + + "vcmi.adventureMap.revisitObject.hover" : "重新访问", + "vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。", "vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗", "vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE).", @@ -151,8 +190,22 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d 伤害", "vcmi.battleWindow.damageEstimation.kills" : "%d 将被消灭", "vcmi.battleWindow.damageEstimation.kills.1" : "%d 将被消灭", + "vcmi.battleWindow.killed" : "已消灭", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s 死于精准射击", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s 死于精准射击", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s 死于精准射击", + "vcmi.battleWindow.endWithAutocombat" : "您确定想以自动战斗立即结束吗?", "vcmi.battleResultsWindow.applyResultsLabel" : "接受战斗结果", + + "vcmi.tutorialWindow.title" : "触摸屏介绍", + "vcmi.tutorialWindow.decription.RightClick" : "长按要右键单击的元素。 触摸其他区域以关闭。", + "vcmi.tutorialWindow.decription.MapPanning" : "单指拖拽以移动地图。", + "vcmi.tutorialWindow.decription.MapZooming" : "两指开合更改地图缩放比例。", + "vcmi.tutorialWindow.decription.RadialWheel" : "滑动可打开径向轮以执行各种操作,例如生物/英雄管理和城镇排序。", + "vcmi.tutorialWindow.decription.BattleDirection" : "要从特定方向攻击,请向要进行攻击的方向滑动。", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "将长触状态的手指拉远足够距离,可以取消攻击方向手势。", + "vcmi.tutorialWindow.decription.AbortSpell" : "长触以取消魔法。", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示可招募生物", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示可招募生物}\n\n在城镇摘要(城镇屏幕的左下角)中显示可招募的生物数量,而不是增长。", @@ -171,7 +224,7 @@ "vcmi.townHall.greetingDefence" : "在%s中稍待片刻,富有战斗经验的战士会教你防御技巧(防御力+1)。", "vcmi.townHall.hasNotProduced" : "本周%s并没有产生什么资源。", "vcmi.townHall.hasProduced" : "本周%s产生了%d个%s。", - "vcmi.townHall.greetingCustomBonus" : "当你的英雄访问%s 时,这个神奇的建筑使你的英雄 +%d %s%s。", + "vcmi.townHall.greetingCustomBonus" : "%s 给予英雄 +%d %s%s。", "vcmi.townHall.greetingCustomUntil" : "直到下一场战斗。", "vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。", @@ -184,32 +237,95 @@ "vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面", "vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物", + "vcmi.tavernWindow.inviteHero" : "邀请英雄", + "vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?", "vcmi.creatureWindow.showBonuses.hover" : "属性视图", "vcmi.creatureWindow.showBonuses.help" : "显示指挥官的所有属性增益", "vcmi.creatureWindow.showSkills.hover" : "技能视图", "vcmi.creatureWindow.showSkills.help" : "显示指挥官的所有学习的技能", - "vcmi.creatureWindow.returnArtifact.hover" : "交换宝物", + "vcmi.creatureWindow.returnArtifact.hover" : "返还宝物", "vcmi.creatureWindow.returnArtifact.help" : "点击这个按钮将宝物反还到英雄的背包里", "vcmi.questLog.hideComplete.hover" : "隐藏完成任务", "vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务", - "vcmi.randomMapTab.widgets.defaultTemplate" : "默认", + "vcmi.randomMapTab.widgets.randomTemplate" : "(随机)", "vcmi.randomMapTab.widgets.templateLabel" : "模板", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", "vcmi.randomMapTab.widgets.roadTypesLabel" : "道路类型", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{额外计时器}\n\n当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{转动计时器}\n\n当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{战斗计时器}\n\n战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{堆栈计时器}\n\n当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。", - "vcmi.optionsTab.widgets.labelTimer" : "计时器", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "经典计时器", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "国际象棋计时器", + "vcmi.optionsTab.turnOptions.hover" : "回合选项", + "vcmi.optionsTab.turnOptions.help" : "选择回合计时器并同步回合选项", + "vcmi.optionsTab.selectPreset" : "预设", + "vcmi.optionsTab.chessFieldBase.hover" : "基本计时器", + "vcmi.optionsTab.chessFieldTurn.hover" : "回合计时器", + "vcmi.optionsTab.chessFieldBattle.hover" : "战斗计时器", + "vcmi.optionsTab.chessFieldUnit.hover" : "单位计时器", + "vcmi.optionsTab.chessFieldBase.help" : "当 {回合计时器} 达到 0 时使用。在游戏开始时设置一次。达到 0 时,结束当前回合。任何正在进行的战斗都会以失败告终。", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "在战斗外或{战斗计时器}耗尽时使用。 每回合重置。 剩余部分在回合结束时添加至 {基本计时器}。", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "在战斗外或{战斗计时器}耗尽时使用。 每回合重置。 任何未花费的时间都会丢失。", + "vcmi.optionsTab.chessFieldBattle.help" : "在与 AI 的战斗中使用,或者在 {单位计时器} 耗尽时用于 pvp 战斗。在每次战斗开始时重置。", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "在 PVP 战斗中选择单位动作时使用。 在单位回合结束时将剩余物添加到{战斗计时器}。", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "在 PVP 战斗中选择单位动作时使用。 在每个单位回合开始时重置。 任何未花费的时间都会丢失。", + + "vcmi.optionsTab.accumulate" : "累积", + + "vcmi.optionsTab.simturnsTitle" : "同时进行回合", + "vcmi.optionsTab.simturnsMin.hover" : "最少回合", + "vcmi.optionsTab.simturnsMax.hover" : "最多回合", + "vcmi.optionsTab.simturnsAI.hover" : "(测试中) AI回合同时行动", + "vcmi.optionsTab.simturnsMin.help" : "同时游戏进行的最少指定天数。在此期间玩家之间的联系将被阻止", + "vcmi.optionsTab.simturnsMax.help" : "同时游戏指定的最多天数或直到与其他玩家联系", + "vcmi.optionsTab.simturnsAI.help" : "{AI回合同时行动}\n实验选项。启用同时回合后,允许 AI 玩家与人类玩家同时行动。", + + "vcmi.optionsTab.turnTime.select" : "回合计时器预设", + "vcmi.optionsTab.turnTime.unlimited" : "无限时", + "vcmi.optionsTab.turnTime.classic.1" : "经典计时器: 1 分钟", + "vcmi.optionsTab.turnTime.classic.2" : "经典计时器: 2 分钟", + "vcmi.optionsTab.turnTime.classic.5" : "经典计时器: 5 分钟", + "vcmi.optionsTab.turnTime.classic.10" : "经典计时器: 10 分钟", + "vcmi.optionsTab.turnTime.classic.20" : "经典计时器: 20 分钟", + "vcmi.optionsTab.turnTime.classic.30" : "经典计时器: 30 分钟", + "vcmi.optionsTab.turnTime.chess.20" : "国际象棋计时器: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "国际象棋计时器: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "国际象棋计时器: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "国际象棋计时器: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "国际象棋计时器: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "国际象棋计时器: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "同时进行回合预设", + "vcmi.optionsTab.simturns.none" : "不同时进行回合", + "vcmi.optionsTab.simturns.tillContactMax" : "同时进行: 可进行联系", + "vcmi.optionsTab.simturns.tillContact1" : "同时进行: 1 周, 联系时中断", + "vcmi.optionsTab.simturns.tillContact2" : "同时进行: 2 周, 联系时中断", + "vcmi.optionsTab.simturns.tillContact4" : "同时进行: 1 月, 联系时中断", + "vcmi.optionsTab.simturns.blocked1" : "同时进行: 1 周, 屏蔽联系", + "vcmi.optionsTab.simturns.blocked2" : "同时进行: 2 周, 屏蔽联系", + "vcmi.optionsTab.simturns.blocked4" : "同时进行: 1 月, 屏蔽联系", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d 天", + "vcmi.optionsTab.simturns.days.1" : " %d 天", + "vcmi.optionsTab.simturns.days.2" : " %d 天", + "vcmi.optionsTab.simturns.weeks.0" : " %d 周", + "vcmi.optionsTab.simturns.weeks.1" : " %d 周", + "vcmi.optionsTab.simturns.weeks.2" : " %d 周", + "vcmi.optionsTab.simturns.months.0" : " %d 月", + "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" : "敌人依然存活至今,你失败了!", @@ -219,6 +335,7 @@ "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" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i", @@ -272,10 +389,12 @@ "core.bonus.ENCHANTER.description": "每回合群体施放${subtype.spell}", "core.bonus.ENCHANTED.name": "法术加持", "core.bonus.ENCHANTED.description": "永久处于${subtype.spell}影响", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "忽略攻击 (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "被攻击时,进攻方${val}%的攻击力将被无视。", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "当攻击时,目标生物${val}%的防御力将被无视。", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "发动攻击时,防御方${val}%的防御力将被无视。", "core.bonus.FIRE_IMMUNITY.name": "火系免疫", - "core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法", + "core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法。", "core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)", "core.bonus.FIRE_SHIELD.description": "反弹部分受到的近战伤害", "core.bonus.FIRST_STRIKE.name": "抢先反击", @@ -284,7 +403,9 @@ "core.bonus.FEAR.description": "使得敌方一只部队恐惧", "core.bonus.FEARLESS.name": "无惧", "core.bonus.FEARLESS.description": "免疫恐惧特质", - "core.bonus.FLYING.name": "飞行兵种", + "core.bonus.FEROCITY.name": "凶猛追击", + "core.bonus.FEROCITY.description": "杀死任意生物后额外攻击${val}次", + "core.bonus.FLYING.name": "飞行能力", "core.bonus.FLYING.description": "以飞行的方式移动(无视障碍)", "core.bonus.FREE_SHOOTING.name": "近身射击", "core.bonus.FREE_SHOOTING.description": "能在近战范围内进行射击", @@ -294,50 +415,52 @@ "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "减少从远程和近战中遭受的物理伤害", "core.bonus.HATE.name": "${subtype.creature}的死敌", "core.bonus.HATE.description": "对${subtype.creature}造成额外${val}%伤害", - "core.bonus.HEALER.name": "治疗", + "core.bonus.HEALER.name": "治疗者", "core.bonus.HEALER.description": "可以治疗友军单位", "core.bonus.HP_REGENERATION.name": "再生", "core.bonus.HP_REGENERATION.description": "每回合恢复${val}点生命值", - "core.bonus.JOUSTING.name": "冲锋", + "core.bonus.JOUSTING.name": "勇士冲锋", "core.bonus.JOUSTING.description": "每移动一格 +${val}%伤害", - "core.bonus.KING.name": "顶级怪物", + "core.bonus.KING.name": "王牌", "core.bonus.KING.description": "受${val}级或更高级屠戮成性影响", "core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫1-${val}级魔法", "core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫1-${val}级的魔法", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "受限射击距离", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "无法以${val}格外的单位为射击目标", + "core.bonus.LIMITED_SHOOTING_RANGE.name": "射程限制", + "core.bonus.LIMITED_SHOOTING_RANGE.description": "无法瞄准${val}格以外的单位", "core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)", "core.bonus.LIFE_DRAIN.description": "吸取${val}%伤害回复自身", - "core.bonus.MANA_CHANNELING.name": "魔法虹吸${val}%", + "core.bonus.MANA_CHANNELING.name": "法力虹吸${val}%", "core.bonus.MANA_CHANNELING.description": "使你的英雄有${val}%几率获得敌人施法的魔法值", - "core.bonus.MANA_DRAIN.name": "吸取魔力", + "core.bonus.MANA_DRAIN.name": "吸取法力", "core.bonus.MANA_DRAIN.description": "每回合吸取${val}魔法值", "core.bonus.MAGIC_MIRROR.name": "魔法神镜 (${val}%)", "core.bonus.MAGIC_MIRROR.description": "${val}%几率将进攻性魔法导向一个敌人单位", "core.bonus.MAGIC_RESISTANCE.name": "魔法抵抗 (${val}%)", "core.bonus.MAGIC_RESISTANCE.description": "${val}%几率抵抗敌人的魔法", - "core.bonus.MIND_IMMUNITY.name": "免疫心智", - "core.bonus.MIND_IMMUNITY.description": "不受心智魔法的影响", - "core.bonus.NO_DISTANCE_PENALTY.name": "无视距离惩罚", - "core.bonus.NO_DISTANCE_PENALTY.description": "任意距离均造成全额伤害", + "core.bonus.MIND_IMMUNITY.name": "免疫心智魔法", + "core.bonus.MIND_IMMUNITY.description": "不受心智相关的魔法影响", + "core.bonus.NO_DISTANCE_PENALTY.name": "无视射程惩罚", + "core.bonus.NO_DISTANCE_PENALTY.description": "任意射程造成全额伤害", "core.bonus.NO_MELEE_PENALTY.name": "无近战惩罚", "core.bonus.NO_MELEE_PENALTY.description": "该生物没有近战伤害惩罚", "core.bonus.NO_MORALE.name": "无士气", "core.bonus.NO_MORALE.description": "生物不受士气影响", - "core.bonus.NO_WALL_PENALTY.name": "无城墙影响", - "core.bonus.NO_WALL_PENALTY.description": "攻城战中不被城墙阻挡造成全额伤害", + "core.bonus.NO_WALL_PENALTY.name": "无城墙惩罚", + "core.bonus.NO_WALL_PENALTY.description": "攻城战中无视城墙阻挡,造成全额伤害", "core.bonus.NON_LIVING.name": "无生命", "core.bonus.NON_LIVING.description": "免疫大多数的效果", "core.bonus.RANDOM_SPELLCASTER.name": "随机施法", "core.bonus.RANDOM_SPELLCASTER.description": "可以施放随机魔法", "core.bonus.RANGED_RETALIATION.name": "远程反击", "core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击", - "core.bonus.RECEPTIVE.name": "接受", + "core.bonus.RECEPTIVE.name": "接纳", "core.bonus.RECEPTIVE.description": "不会免疫有益魔法", "core.bonus.REBIRTH.name": "复生 (${val}%)", "core.bonus.REBIRTH.description": "当整支部队死亡后${val}%会复活", "core.bonus.RETURN_AFTER_STRIKE.name": "攻击后返回", "core.bonus.RETURN_AFTER_STRIKE.description": "近战攻击后回到初始位置", + "core.bonus.REVENGE.name": "复仇", + "core.bonus.REVENGE.description": "根据攻击者在战斗中失去的生命值造成额外伤害", "core.bonus.SHOOTER.name": "远程攻击", "core.bonus.SHOOTER.description": "生物可以射击", "core.bonus.SHOOTS_ALL_ADJACENT.name": "范围远程攻击", @@ -369,7 +492,7 @@ "core.bonus.TRANSMUTATION.name": "变形术", "core.bonus.TRANSMUTATION.description": "${val}%机会将被攻击单位变成其他生物", "core.bonus.UNDEAD.name": "不死生物", - "core.bonus.UNDEAD.description": "该生物属于丧尸", + "core.bonus.UNDEAD.description": "该生物属于不死生物", "core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击", "core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人", "core.bonus.WATER_IMMUNITY.name": "水系免疫", diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index f1c561b97..85af6a689 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -29,16 +29,36 @@ "vcmi.capitalColors.5" : "Fialový", "vcmi.capitalColors.6" : "Tyrkysový", "vcmi.capitalColors.7" : "Růžový", + + "vcmi.heroOverview.startingArmy" : "Počáteční jednotky", + "vcmi.heroOverview.warMachine" : "Bojové stroje", + "vcmi.heroOverview.secondarySkills" : "Druhotné schopnosti", + "vcmi.heroOverview.spells" : "Kouzla", "vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky", "vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou", "vcmi.radialWheel.splitSingleUnit" : "Rozdělit jedinou jednotku", - "vcmi.radialWheel.splitUnitEqually" : "Rovnoměrně rozdělit jednotky", + "vcmi.radialWheel.splitUnitEqually" : "Rozdělit jednotky rovnoměrně", "vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu", "vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice", + "vcmi.radialWheel.heroGetArmy" : "Získat armádu jiného hrdiny", + "vcmi.radialWheel.heroSwapArmy" : "Vyměnit armádu s jiným hrdinou", + "vcmi.radialWheel.heroExchange" : "Otevřít výměnu hrdinů", + "vcmi.radialWheel.heroGetArtifacts" : "Získat artefakty od jiního hrdiny", + "vcmi.radialWheel.heroSwapArtifacts" : "Vyměnit artefakty s jiným hrdinou", + "vcmi.radialWheel.heroDismiss" : "Propustit hrdinu", + + "vcmi.radialWheel.moveTop" : "Move to top", + "vcmi.radialWheel.moveUp" : "Move up", + "vcmi.radialWheel.moveDown" : "Move down", + "vcmi.radialWheel.moveBottom" : "Move to bottom", + + "vcmi.spellBook.search" : "hledat...", + "vcmi.mainMenu.serverConnecting" : "Připojování...", "vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", + "vcmi.mainMenu.serverConnectionFailed" : "Připojování selhalo", "vcmi.mainMenu.serverClosing" : "Zavírání...", "vcmi.mainMenu.hostTCP" : "Pořádat hru TCP/IP", "vcmi.mainMenu.joinTCP" : "Připojit se do hry TCP/IP", @@ -46,17 +66,26 @@ "vcmi.lobby.filepath" : "Název souboru", "vcmi.lobby.creationDate" : "Datum vytvoření", + "vcmi.lobby.scenarioName" : "Název scénáře", + "vcmi.lobby.mapPreview" : "Náhled mapy", + "vcmi.lobby.noPreview" : "bez náhledu", + "vcmi.lobby.noUnderground" : "bez podzemí", + "vcmi.client.errors.missingCampaigns" : "{Chybějící datové soubory}\n\nDatové soubory kampaně nebyly nalezeny! Možná máte nekompletní nebo poškozené datové soubory Heroes 3. Prosíme, přeinstalujte hru.", "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", "vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}", + "vcmi.server.errors.modsToDisable" : "{Následující modifikace musí být zakázány}", "vcmi.server.confirmReconnect" : "Chcete se připojit k poslední relaci?", + "vcmi.server.errors.modNoDependency" : "Nelze načíst modifikaci {'%s'}!\n Závisí na modifikaci {'%s'}, která není aktivní!\n", + "vcmi.server.errors.modConflict" : "Nelze načíst modifikaci {'%s'}!\n Je v kolizi s aktivní modifikací {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!", "vcmi.settingsMainWindow.generalTab.hover" : "Obecné", - "vcmi.settingsMainWindow.generalTab.help" : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry", + "vcmi.settingsMainWindow.generalTab.help" : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry.", "vcmi.settingsMainWindow.battleTab.hover" : "Bitva", - "vcmi.settingsMainWindow.battleTab.help" : "Přepne na kartu nastavení bitvy, která umožňuje konfiguraci chování hry v bitvách", + "vcmi.settingsMainWindow.battleTab.help" : "Přepne na kartu nastavení bitvy, která umožňuje konfiguraci chování hry v bitvách.", "vcmi.settingsMainWindow.adventureTab.hover" : "Mapa světa", - "vcmi.settingsMainWindow.adventureTab.help" : "Přepne na kartu nastavení mapy světa (mapa světa je sekce hry, ve které hráči mohou ovládat pohyb hrdinů)", + "vcmi.settingsMainWindow.adventureTab.help" : "Přepne na kartu nastavení mapy světa (mapa světa je sekce hry, ve které hráči mohou ovládat pohyb hrdinů).", "vcmi.systemOptions.videoGroup" : "Nastavení obrazu", "vcmi.systemOptions.audioGroup" : "Nastavení zvuku", @@ -81,24 +110,32 @@ "vcmi.systemOptions.longTouchMenu.help" : "Změnit dobu dlouhého podržení.", "vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund", "vcmi.systemOptions.framerateButton.hover" : "Zobrazit FPS", - "vcmi.systemOptions.framerateButton.help" : "{Zobrazit FPS}\n\nPřepne viditelnost počitadla snímků za sekundu v rohu obrazovky hry", + "vcmi.systemOptions.framerateButton.help" : "{Zobrazit FPS}\n\nPřepne viditelnost počitadla snímků za sekundu v rohu obrazovky hry.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Vibrace", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Vibrace}\n\nPřepnout stav vibrací při dotykovém ovládání", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Vibrace}\n\nPřepnout stav vibrací při dotykovém ovládání.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Vylepšení rozhraní", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Vylepšení rozhraní}\n\nZapne různá vylepšení rozhraní, jako je tlačítko batohu atd. Zakažte pro zážitek klasické hry.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Velká kniha kouzel", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Velká kniha kouzel}\n\nPovolí větší knihu kouzel, do které se jich více vleze na jednu stranu. Animace změny stránek s tímto nastavením nefunguje.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Ztlumit při neaktivitě", + "vcmi.systemOptions.audioMuteFocus.help" : "{Ztlumit při neaktivitě}\n\nZtlumit zvuk, pokud je okno hry v pozadí. Výjimkou jsou zprávy ve hře a zvuk nového tahu.", "vcmi.adventureOptions.infoBarPick.hover" : "Zobrazit zprávy v panelu informací", "vcmi.adventureOptions.infoBarPick.help" : "{Zobrazit zprávy v panelu informací}\n\nKdyž bude možné, herní zprávy z návštěv míst na mapě budou zobrazeny v panelu informací místo ve zvláštním okně.", "vcmi.adventureOptions.numericQuantities.hover" : "Číselné množství jednotek", "vcmi.adventureOptions.numericQuantities.help" : "{Číselné množství jednotek}\n\nZobrazit přibližné množství nepřátelských jednotek ve formátu A-B.", "vcmi.adventureOptions.forceMovementInfo.hover" : "Vždy zobrazit cenu pohybu", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Vždy zobrazit cenu pohybu}\n\nVždy zobrazit informace o bodech pohybu v panelu informací. (Místo zobrazení pouze při stisknuté klávese ALT)", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Vždy zobrazit cenu pohybu}\n\nVždy zobrazit informace o bodech pohybu v panelu informací. (Místo zobrazení pouze při stisknuté klávese ALT).", "vcmi.adventureOptions.showGrid.hover" : "Zobrazit mřížku", "vcmi.adventureOptions.showGrid.help" : "{Zobrazit mřížku}\n\nZobrazit překrytí mřížkou, zvýrazňuje hranice mezi dlaždicemi mapy světa.", "vcmi.adventureOptions.borderScroll.hover" : "Posouvání okraji", "vcmi.adventureOptions.borderScroll.help" : "{Posouvání okraji}\n\nPosouvat mapu světa, když je kurzor na okraji obrazovky. Může být zakázáno držením klávesy CTRL.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", //TODO - "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Posouvání mapy levým kliknutím", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Posouvání mapy levým kliknutím}\n\nPosouvání mapy tažením myši se stisknutým levým tlačítkem.", + "vcmi.adventureOptions.smoothDragging.hover" : "Plynulé posouvání mapy", + "vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nWhen enabled, map dragging has a modern run out effect.", // TODO "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -111,16 +148,16 @@ "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", "vcmi.battleOptions.queueSizeSmallButton.hover": "MALÁ", "vcmi.battleOptions.queueSizeBigButton.hover": "VELKÁ", - "vcmi.battleOptions.queueSizeNoneButton.help": "Nezobrazovat frontu pořadí tahů", + "vcmi.battleOptions.queueSizeNoneButton.help": "Nezobrazovat frontu pořadí tahů.", "vcmi.battleOptions.queueSizeAutoButton.help": "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Zobrazit malou frontu pořadí tahů", - "vcmi.battleOptions.queueSizeBigButton.help": "Zobrazit velkou frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů)", + "vcmi.battleOptions.queueSizeSmallButton.help": "Zobrazit MALOU frontu pořadí tahů.", + "vcmi.battleOptions.queueSizeBigButton.help": "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).", "vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Nastavit rychlost animací na velmi pomalé", - "vcmi.battleOptions.animationsSpeed5.help": "Nastavit rychlost animací na velmi rychlé", - "vcmi.battleOptions.animationsSpeed6.help": "Nastavit rychlost animací na okamžité", + "vcmi.battleOptions.animationsSpeed1.help": "Nastavit rychlost animací na velmi pomalé.", + "vcmi.battleOptions.animationsSpeed5.help": "Nastavit rychlost animací na velmi rychlé.", + "vcmi.battleOptions.animationsSpeed6.help": "Nastavit rychlost animací na okamžité.", "vcmi.battleOptions.movementHighlightOnHover.hover": "Zvýraznění pohybu při najetí", "vcmi.battleOptions.movementHighlightOnHover.help": "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Zobrazit omezení dostřelu střelců", @@ -130,6 +167,9 @@ "vcmi.battleOptions.skipBattleIntroMusic.hover": "Přeskočit úvodní hudbu", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.", + "vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit místo", + "vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit místo}\n\nPokud se hrdina nachází na nějakém místě mapy, může jej znovu navštívit.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Stiskněte jakoukoliv klávesu pro okamžité zahájení bitvy", "vcmi.battleWindow.damageEstimation.melee" : "Zaútočit na %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Zaútočit na %CREATURE (%DAMAGE, %KILLS).", @@ -144,6 +184,15 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Použít výsledek bitvy", + "vcmi.tutorialWindow.title" : "Úvod ovládání dotykem", + "vcmi.tutorialWindow.decription.RightClick" : "Klepněte a držte prvek, na který byste chtěli použít pravé tlačítko myši. Klepněte na volnou oblast pro zavření.", + "vcmi.tutorialWindow.decription.MapPanning" : "Klepněte a držte jedním prstem pro posouvání mapy.", + "vcmi.tutorialWindow.decription.MapZooming" : "Přibližte dva prsty k sobě pro přiblížení mapy.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Přejetí otevře kruhovou nabídku pro různé akce, třeba správa hrdiny/bojovnínků a příkazy měst.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Pro útok ze speifického úhlu, přejeďte směrem, ze kterého má být útok vykonán.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Gesto útoku pod úhlem může být zrušeno, pokud, pokud je prst dostatečně daleko.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Klepněte a držte pro zrušení kouzla.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Zobrazit dostupné jednotky", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Zobrazit dostupné jednotky}\n\nZobrazit počet jednotek dostupných ke koupení místo jejich týdenního přírůstku v přehledu města. (levý spodní okraj obrazovky města).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Zobrazit týdenní přírůstek jednotek", @@ -152,54 +201,107 @@ "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).", "vcmi.townHall.missingBase" : "Základní budova %s musí být postavena jako první", - "vcmi.townHall.noCreaturesToRecruit" : "Žádné jednotky k vycvičení!", //TODO - "vcmi.townHall.greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.", - "vcmi.townHall.greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).", + "vcmi.townHall.noCreaturesToRecruit" : "Žádné jednotky k vycvičení!", + "vcmi.townHall.greetingManaVortex" : "Při pobytu u místa %s se vaše tělo naplnilo novou energií. Máte dvojnásobné množství maximální magické energie.", + "vcmi.townHall.greetingKnowledge" : "Studujete glyfy na the %s a porozumíte fungování různých magií (+1 Znalosti).", "vcmi.townHall.greetingSpellPower" : "%s vás učí nové cesty zaměření vaší magické síly (+1 Síla kouzel).", "vcmi.townHall.greetingExperience" : "Návštěva %s vás naučila spoustu nových dovedností (+1000 zkušeností).", - "vcmi.townHall.greetingAttack" : "Čas strávený poblíž místa zvaného %s allows you to learn more effective combat skills (+1 Attack Skill).", - "vcmi.townHall.greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).", - "vcmi.townHall.hasNotProduced" : "%s zatím nic nevyrobil.", - "vcmi.townHall.hasProduced" : "%s vyrobil %d %s tento týden.", + "vcmi.townHall.greetingAttack" : "Čas strávený poblíž místa zvaného %s vám dovolil se naučit efektivnější bojové dovednosti (+1 Útočná síla).", + "vcmi.townHall.greetingDefence" : "Trávíte čas na místě zvaném %s, zkušení bojovníci vás u toho naučili nové metody obrany (+1 Obranná síla).", + "vcmi.townHall.hasNotProduced" : "%s - zatím nic nevyrobeno.", + "vcmi.townHall.hasProduced" : "%s - vyrobeno %d %s tento týden.", "vcmi.townHall.greetingCustomBonus" : "%s vám dává +%d %s%s", "vcmi.townHall.greetingCustomUntil" : " do další bitvy.", - "vcmi.townHall.greetingInTownMagicWell" : "%s obnovil na maximum vaši magickou energii.", + "vcmi.townHall.greetingInTownMagicWell" : "%s - obnoveno na maximum vaši magickou energii.", "vcmi.logicalExpressions.anyOf" : "Něco z následujících:", "vcmi.logicalExpressions.allOf" : "Všechny následující:", "vcmi.logicalExpressions.noneOf" : "Žádné z následujících:", "vcmi.heroWindow.openCommander.hover" : "Open commander info window", - "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero", + "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero.", "vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window", - "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management", + "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management.", - "vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?", + "vcmi.commanderWindow.artifactMessage" : "Chcete navrátit tento artefakt hrdinovi?", - "vcmi.creatureWindow.showBonuses.hover" : "Switch to bonuses view", - "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander", - "vcmi.creatureWindow.showSkills.hover" : "Switch to skills view", - "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander", + "vcmi.creatureWindow.showBonuses.hover" : "Přepnout na zobrazení bonusů", + "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander.", + "vcmi.creatureWindow.showSkills.hover" : "Přepnout na zobrazení schoostí", + "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander.", "vcmi.creatureWindow.returnArtifact.hover" : "Vrátit artefakt", - "vcmi.creatureWindow.returnArtifact.help" : "Klikněte na toto tlačítko pro navrácení artefaktů do hrdinova batohu", + "vcmi.creatureWindow.returnArtifact.help" : "Klikněte na toto tlačítko pro navrácení artefaktů do hrdinova batohu.", - "vcmi.questLog.hideComplete.hover" : "Hide complete quests", - "vcmi.questLog.hideComplete.help" : "Hide all completed quests", + "vcmi.questLog.hideComplete.hover" : "Skrýt dokončené úkoly", + "vcmi.questLog.hideComplete.help" : "Skrýt všechny dokončené úkoly.", - "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)", + "vcmi.randomMapTab.widgets.randomTemplate" : "(Náhodná)", "vcmi.randomMapTab.widgets.templateLabel" : "Šablona", - "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...", - "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Nastavit...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Přiřazení týmů", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Druhy cest", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", - "vcmi.optionsTab.widgets.labelTimer" : "Časovač", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Klasický časovač", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Šachový časovač", + "vcmi.optionsTab.turnOptions.hover" : "Možnosti tahu", + "vcmi.optionsTab.turnOptions.help" : "Vyberte odpočítávadlo tahů a nastavení souběžných tahů", + "vcmi.optionsTab.selectPreset" : "Preset", + "vcmi.optionsTab.chessFieldBase.hover" : "Base timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer", + "vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer", + "vcmi.optionsTab.chessFieldUnit.hover" : "Unit timer", + "vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Any unspent time is lost", + "vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Used when selecting unit action in pvp combat. Leftover added to {Battle Timer} at end of unit turn.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn. Any unspent time is lost", + + "vcmi.optionsTab.accumulate" : "Accumulate", + + "vcmi.optionsTab.simturnsTitle" : "Souběžné tahy", + "vcmi.optionsTab.simturnsMin.hover" : "Alespoň po", + "vcmi.optionsTab.simturnsMax.hover" : "Nejvíce po", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimentální) Souběžné tahy AI", + "vcmi.optionsTab.simturnsMin.help" : "Hrát souběžně po určený počet dní. Setkání mezi hráči je v této době zablokováno", + "vcmi.optionsTab.simturnsMax.help" : "Hrát souběžně po určený počet dní nebo do setkání s jiným hráčem", + "vcmi.optionsTab.simturnsAI.help" : "{Souběžné tahy AI}\nExperimentální volba. Dovoluje AI hráčům hrát souběžně s lidskými hráči, když jsou souběžné tahy povoleny.", + + "vcmi.optionsTab.turnTime.select" : "Vyberte šablonu nastavení časovače", + "vcmi.optionsTab.turnTime.unlimited" : "Neomezený čas tahu", + "vcmi.optionsTab.turnTime.classic.1" : "Klasický časovač: 1 minuta", + "vcmi.optionsTab.turnTime.classic.2" : "Klasický časovač: 2 minuty", + "vcmi.optionsTab.turnTime.classic.5" : "Klasický časovač: 5 minut", + "vcmi.optionsTab.turnTime.classic.10" : "Klasický časovač: 10 minut", + "vcmi.optionsTab.turnTime.classic.20" : "Klasický časovač: 20 minut", + "vcmi.optionsTab.turnTime.classic.30" : "Klasický časovač: 30 minut", + "vcmi.optionsTab.turnTime.chess.20" : "Šachová: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Šachová: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Šachová: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Šachová: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Šachová: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Šachová: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Vyberte šablonu souběžných tahů", + "vcmi.optionsTab.simturns.none" : "Bez souběžných tahů", + "vcmi.optionsTab.simturns.tillContactMax" : "Souběžně: Do setkání", + "vcmi.optionsTab.simturns.tillContact1" : "Souběžně: 1 týden, přerušit při setkání", + "vcmi.optionsTab.simturns.tillContact2" : "Souběžně: 2 týdny, přerušit při setkání", + "vcmi.optionsTab.simturns.tillContact4" : "Souběžně: 1 mšsíc, přerušit při setkání", + "vcmi.optionsTab.simturns.blocked1" : "Souběžně: 1 týden, setkání zablokována", + "vcmi.optionsTab.simturns.blocked2" : "Souběžně: 2 týdny, setkání zablokována", + "vcmi.optionsTab.simturns.blocked4" : "Souběžně: 1 měsíc, setkání zablokována", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d dní", + "vcmi.optionsTab.simturns.days.1" : " %d den", + "vcmi.optionsTab.simturns.days.2" : " %d dny", + "vcmi.optionsTab.simturns.weeks.0" : " %d týdnů", + "vcmi.optionsTab.simturns.weeks.1" : " %d týden", + "vcmi.optionsTab.simturns.weeks.2" : " %d týdny", + "vcmi.optionsTab.simturns.months.0" : " %d měsíců", + "vcmi.optionsTab.simturns.months.1" : " %d měsíc", + "vcmi.optionsTab.simturns.months.2" : " %d měsíce", // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Nepřítel zvládl přežít do této chvíle. Vítězství je jeho!", @@ -208,162 +310,162 @@ "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Gratulace! Porazili jste všechny nepřátele zamořující tuto zemi a můžete si nárokovat vítězství!", "vcmi.map.victoryCondition.collectArtifacts.message" : "Získejte tři artefakty", "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Gratulace! Všichni vaši nepřítelé byli poraženi a máte Andělskou alianci! Vítězství je vaše!", - "vcmi.map.victoryCondition.angelicAlliance.message" : "Porazte všechny nepřátele a vyrobte Andělskou alianci", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Porazte všechny nepřátele a utužte Andělskou alianci", // 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" : "Basic", - "vcmi.stackExperience.rank.1" : "Novice", - "vcmi.stackExperience.rank.2" : "Trained", - "vcmi.stackExperience.rank.3" : "Skilled", - "vcmi.stackExperience.rank.4" : "Proven", - "vcmi.stackExperience.rank.5" : "Veteran", + "vcmi.stackExperience.description" : "» P o d r o b n o s t i z k u š e n o s t í o d d í l u «\n\nDruh bojovníka ................... : %s\nÚroveň hodnosti ................. : %s (%i)\nBody zkušeností ............... : %i\nZkušenostních bodů do další úrovně hodnosti .. : %i\nMaximum zkušeností na bitvu ... : %i%% (%i)\nPočet bojovníků v oddílu .... : %i\nMaximum nových rekrutů\n bez ztráty současné hodnosti .... : %i\nNásobič zkušeností ........... : %.2f\nNásobič vylepšení .............. : %.2f\nZkušnosti po 10. úrovně hodnosti ........ : %i\nMaximální počet nových rekrutů pro zachování\n 10. úrovně hodnosti s maximálními zkušenostmi: %i", + "vcmi.stackExperience.rank.0" : "Začátečník", + "vcmi.stackExperience.rank.1" : "Učeň", + "vcmi.stackExperience.rank.2" : "Trénovaný", + "vcmi.stackExperience.rank.3" : "Zručný", + "vcmi.stackExperience.rank.4" : "Prověřený", + "vcmi.stackExperience.rank.5" : "Veterán", "vcmi.stackExperience.rank.6" : "Adept", "vcmi.stackExperience.rank.7" : "Expert", - "vcmi.stackExperience.rank.8" : "Elite", - "vcmi.stackExperience.rank.9" : "Master", - "vcmi.stackExperience.rank.10" : "Ace", + "vcmi.stackExperience.rank.8" : "Elitní", + "vcmi.stackExperience.rank.9" : "Mistr", + "vcmi.stackExperience.rank.10" : "Eso", "core.bonus.ADDITIONAL_ATTACK.name": "Dvojitý úder", "core.bonus.ADDITIONAL_ATTACK.description": "Útočí dvakrát", - "core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations", - "core.bonus.ADDITIONAL_RETALIATION.description": "May retaliate ${val} extra times", + "core.bonus.ADDITIONAL_RETALIATION.name": "Další odveta", + "core.bonus.ADDITIONAL_RETALIATION.description": "Může zaútočit zpět navíc ${val}x", "core.bonus.AIR_IMMUNITY.name": "Vzdušná odolnost", - "core.bonus.AIR_IMMUNITY.description": "Immune to all spells from the school of Air magic", + "core.bonus.AIR_IMMUNITY.description": "Imunní všem kouzlům školy vzdušné magie", "core.bonus.ATTACKS_ALL_ADJACENT.name": "Útok okolo", "core.bonus.ATTACKS_ALL_ADJACENT.description": "Útočí na všechny sousední jednotky", - "core.bonus.BLOCKS_RETALIATION.name": "No retaliation", - "core.bonus.BLOCKS_RETALIATION.description": "Enemy cannot retaliate", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "No ranged retaliation", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Enemy cannot retaliate by using a ranged attack", + "core.bonus.BLOCKS_RETALIATION.name": "Žádná odplata", + "core.bonus.BLOCKS_RETALIATION.description": "Nepřítel nemůže zaútočit zpět", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Žádná odplata na dálku", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Nepřítel nemůže zaútočit zpět útokem na dálku", "core.bonus.CATAPULT.name": "Katapult", - "core.bonus.CATAPULT.description": "Attacks siege walls", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduce Casting Cost (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduces the spellcasting cost for the hero by ${val}", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Magic Damper (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Increases spellcasting cost of enemy spells by ${val}", - "core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge", + "core.bonus.CATAPULT.description": "Útočí na ochranné hradby", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Snížit cenu kouzel (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Snižuje cenu energie hrdiny o ${val}", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tlumič magie (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zvyšuje cenu energie kouzlení nepřítele o ${val}", + "core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge", // TODO "core.bonus.CHARGE_IMMUNITY.description": "Immune to Cavalier's and Champion's Charge", - "core.bonus.DARKNESS.name": "Darkness cover", - "core.bonus.DARKNESS.description": "Creates a shroud of darkness with a ${val} radius", - "core.bonus.DEATH_STARE.name": "Death Stare (${val}%)", - "core.bonus.DEATH_STARE.description": "Has a ${val}% chance to kill a single creature", - "core.bonus.DEFENSIVE_STANCE.name": "Defense Bonus", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} Defense when defending", - "core.bonus.DESTRUCTION.name": "Destruction", - "core.bonus.DESTRUCTION.description": "Has ${val}% chance to kill extra units after attack", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Death Blow", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Has a ${val}% chance of dealing double base damage when attacking", + "core.bonus.DARKNESS.name": "Závoj temnoty", + "core.bonus.DARKNESS.description": "Vytvoří clonu temnoty v oblasti ${val} polí", + "core.bonus.DEATH_STARE.name": "Smrtící pohled (${val}%)", + "core.bonus.DEATH_STARE.description": "Má ${val}% šanci zabít jednu creature", + "core.bonus.DEFENSIVE_STANCE.name": "Obranný bonus", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} obranné síly při obraně", + "core.bonus.DESTRUCTION.name": "Zničení", + "core.bonus.DESTRUCTION.description": "Má ${val}% šanci zabít další jednotky po útoku", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Smrtící rána", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Má ${val}% šanci na udělení dvojnásobného základního poškození při útoku", "core.bonus.DRAGON_NATURE.name": "Drak", - "core.bonus.DRAGON_NATURE.description": "Creature has a Dragon Nature", + "core.bonus.DRAGON_NATURE.description": "Jednotka má povahu draka", "core.bonus.EARTH_IMMUNITY.name": "Zemní odolnost", - "core.bonus.EARTH_IMMUNITY.description": "Immune to all spells from the school of Earth magic", - "core.bonus.ENCHANTER.name": "Enchanter", - "core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn", - "core.bonus.ENCHANTED.name": "Enchanted", - "core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored", + "core.bonus.EARTH_IMMUNITY.description": "Imunní všem kouzlům školy zemské magie", + "core.bonus.ENCHANTER.name": "Zaklínač", + "core.bonus.ENCHANTER.description": "Může masově seslat ${subtype.spell} každý tah", + "core.bonus.ENCHANTED.name": "Očarovaný", + "core.bonus.ENCHANTED.description": "Trvale ovlivněm kouzlem ${subtype.spell}", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Nevšímá si ${val} % bodů obrany", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Pří útoku nebude brát v potaz ${val}% bodů obrany obránce", "core.bonus.FIRE_IMMUNITY.name": "Ohnivá odolnost", - "core.bonus.FIRE_IMMUNITY.description": "Immune to all spells from the school of Fire magic", + "core.bonus.FIRE_IMMUNITY.description": "Imunní všem kouzlům školy ohnivé magie", "core.bonus.FIRE_SHIELD.name": "Ohnivý štít (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Reflects part of melee damage", - "core.bonus.FIRST_STRIKE.name": "First Strike", - "core.bonus.FIRST_STRIKE.description": "This creature retaliates before being attacked", - "core.bonus.FEAR.name": "Fear", - "core.bonus.FEAR.description": "Causes Fear on an enemy stack", + "core.bonus.FIRE_SHIELD.description": "Odrazí část zranení útoku zblízka", + "core.bonus.FIRST_STRIKE.name": "První úder", + "core.bonus.FIRST_STRIKE.description": "Tato jednotka útočí zpět ještě než je na ni zaútočeno", + "core.bonus.FEAR.name": "Strach", + "core.bonus.FEAR.description": "Způsobí strach nepřátelskému oddílu", "core.bonus.FEARLESS.name": "Nebojácnost", "core.bonus.FEARLESS.description": "Odolnost proti strachu", "core.bonus.FLYING.name": "Letec", "core.bonus.FLYING.description": "Při pohybu létá (přes překážky)", "core.bonus.FREE_SHOOTING.name": "Blízké výstřely", "core.bonus.FREE_SHOOTING.description": "Může použít výstřely při útoku zblízka", - "core.bonus.GARGOYLE.name": "Gargoyle", - "core.bonus.GARGOYLE.description": "Cannot be raised or healed", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Reduce Damage (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduces physical damage from ranged or melee attacks", + "core.bonus.GARGOYLE.name": "Chrlič", + "core.bonus.GARGOYLE.description": "Cannot be raised or healed", // TODO + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Snižuje poškození (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Snižuje poškození od útoků z dálky a blízka", "core.bonus.HATE.name": "Nesnáší ${subtype.creature}", - "core.bonus.HATE.description": "Does ${val}% more damage to ${subtype.creature}", + "core.bonus.HATE.description": "Dává o ${val} % větší zranění jednotce ${subtype.creature}", "core.bonus.HEALER.name": "Léčitel", "core.bonus.HEALER.description": "Léčí spojenecké jednotky", "core.bonus.HP_REGENERATION.name": "Regenerace", "core.bonus.HP_REGENERATION.description": "Každé kolo léčí ${val} životů", - "core.bonus.JOUSTING.name": "Champion charge", - "core.bonus.JOUSTING.description": "+${val}% damage for each hex travelled", + "core.bonus.JOUSTING.name": "Nabití šampiona", + "core.bonus.JOUSTING.description": "+${val}% poškození za každé projité pole", "core.bonus.KING.name": "Král", - "core.bonus.KING.description": "Vulnerable to SLAYER level ${val} or higher", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Spell immunity 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immune to spells of levels 1-${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Limited shooting range", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Unable to target units farther than ${val} hexes", + "core.bonus.KING.description": "Zranitelný zabijákovi úrovně ${val} a vyšší", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odolnost kouzel 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odolnost vůči kouzlům úrovní 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Omezený dosah střelby", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nevystřelí na jednotky dále než ${val} polí", "core.bonus.LIFE_DRAIN.name": "Vysátí životů (${val}%)", "core.bonus.LIFE_DRAIN.description": "Vysaje ${val}% uděleného poškození", - "core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%", - "core.bonus.MANA_CHANNELING.description": "Gives your hero ${val}% of the mana spent by the enemy", + "core.bonus.MANA_CHANNELING.name": "${val}% kouzelný kanál", + "core.bonus.MANA_CHANNELING.description": "Dá vašemu hrdinovi ${val} % many využité nepřítelem", "core.bonus.MANA_DRAIN.name": "Vysátí many", "core.bonus.MANA_DRAIN.description": "Každé kolo vysaje ${val} many", "core.bonus.MAGIC_MIRROR.name": "Kouzelné zrcadlo (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "Has a ${val}% chance to redirect an offensive spell to an enemy unit", - "core.bonus.MAGIC_RESISTANCE.name": "Magic Resistance (${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "Has a ${val}% chance to resist an enemy spell", - "core.bonus.MIND_IMMUNITY.name": "Mind Spell Immunity", - "core.bonus.MIND_IMMUNITY.description": "Immune to Mind-type spells", - "core.bonus.NO_DISTANCE_PENALTY.name": "No distance penalty", - "core.bonus.NO_DISTANCE_PENALTY.description": "Does full damage at any distance", - "core.bonus.NO_MELEE_PENALTY.name": "No melee penalty", - "core.bonus.NO_MELEE_PENALTY.description": "Creature has no Melee Penalty", - "core.bonus.NO_MORALE.name": "Neutral Morale", - "core.bonus.NO_MORALE.description": "Creature is immune to morale effects", - "core.bonus.NO_WALL_PENALTY.name": "No wall penalty", - "core.bonus.NO_WALL_PENALTY.description": "Full damage during siege", - "core.bonus.NON_LIVING.name": "Non living", - "core.bonus.NON_LIVING.description": "Immunity to many effects", - "core.bonus.RANDOM_SPELLCASTER.name": "Random spellcaster", - "core.bonus.RANDOM_SPELLCASTER.description": "Can cast random spell", - "core.bonus.RANGED_RETALIATION.name": "Ranged retaliation", - "core.bonus.RANGED_RETALIATION.description": "Can perform ranged counterattack", - "core.bonus.RECEPTIVE.name": "Receptive", - "core.bonus.RECEPTIVE.description": "No Immunity to Friendly Spells", - "core.bonus.REBIRTH.name": "Rebirth (${val}%)", - "core.bonus.REBIRTH.description": "${val}% of stack will rise after death", - "core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return", - "core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack", - "core.bonus.SHOOTER.name": "Ranged", - "core.bonus.SHOOTER.description": "Creature can shoot", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "This creature's ranged attacks strike all targets in a small area", - "core.bonus.SOUL_STEAL.name": "Soul Steal", - "core.bonus.SOUL_STEAL.description": "Gains ${val} new creatures for each enemy killed", - "core.bonus.SPELLCASTER.name": "Spellcaster", - "core.bonus.SPELLCASTER.description": "Can cast ${subtype.spell}", - "core.bonus.SPELL_AFTER_ATTACK.name": "Cast After Attack", - "core.bonus.SPELL_AFTER_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} after it attacks", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Cast Before Attack", - "core.bonus.SPELL_BEFORE_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} before it attacks", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Spell Resistance", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Damage from spells reduced by ${val}%.", + "core.bonus.MAGIC_MIRROR.description": "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku", + "core.bonus.MAGIC_RESISTANCE.name": "Magická odolnost (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "Má ${val}% šanci ustát nepřátelské kouzlo", + "core.bonus.MIND_IMMUNITY.name": "Imunita kouzel mysli", + "core.bonus.MIND_IMMUNITY.description": "Imunní vůči kouzlům cílícím na mysl", + "core.bonus.NO_DISTANCE_PENALTY.name": "Bez penalizace vzdálenosti", + "core.bonus.NO_DISTANCE_PENALTY.description": "Plné poškození na jakoukoliv vzdálenost", + "core.bonus.NO_MELEE_PENALTY.name": "Bez penalizace útoku zblízka", + "core.bonus.NO_MELEE_PENALTY.description": "Jednotka není penalizována za útok zblízka", + "core.bonus.NO_MORALE.name": "Neutrální morálka", + "core.bonus.NO_MORALE.description": "Jednotka je imunní vůči efektu morálky", + "core.bonus.NO_WALL_PENALTY.name": "Bez penalizace hradbami", + "core.bonus.NO_WALL_PENALTY.description": "Plné poškození při obléhání", + "core.bonus.NON_LIVING.name": "Neživoucí", + "core.bonus.NON_LIVING.description": "Imunní vůči mnohým efektům", + "core.bonus.RANDOM_SPELLCASTER.name": "Náhodný kouzelník", + "core.bonus.RANDOM_SPELLCASTER.description": "Může seslat náhodné kouzlo", + "core.bonus.RANGED_RETALIATION.name": "Vzdálená msta", + "core.bonus.RANGED_RETALIATION.description": "Může provést protiútok na dálku", + "core.bonus.RECEPTIVE.name": "Přijímavý", + "core.bonus.RECEPTIVE.description": "Není imunní vůči přátelským kouzlům", + "core.bonus.REBIRTH.name": "Znovuzrození (${val}%)", + "core.bonus.REBIRTH.description": "${val}% oddílu se po smrti znovu narodí", + "core.bonus.RETURN_AFTER_STRIKE.name": "Útok a návrat", + "core.bonus.RETURN_AFTER_STRIKE.description": "Navrátí se po útoku na blízko", + "core.bonus.SHOOTER.name": "Střelec", + "core.bonus.SHOOTER.description": "Jednotka může střílet", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Střílí okolo", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Vzdálené útoky této jednotky zasáhnou všechny cíle v malé oblasti", + "core.bonus.SOUL_STEAL.name": "Zloděj duší", + "core.bonus.SOUL_STEAL.description": "Získá ${val} nových jednotek za každého zabitého nepřítele", + "core.bonus.SPELLCASTER.name": "Kouzelník", + "core.bonus.SPELLCASTER.description": "Může seslat ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name": "Kouzlení po útoku", + "core.bonus.SPELL_AFTER_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} po zaútočení", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Kouzlení před útokem", + "core.bonus.SPELL_BEFORE_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} před zaútočením", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Magická odolnost", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Zranění od kouzel sníženo o ${val}%.", "core.bonus.SPELL_IMMUNITY.name": "Odolnost vůči kouzlům", "core.bonus.SPELL_IMMUNITY.description": "Odolnost proti ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "Spell-like attack", - "core.bonus.SPELL_LIKE_ATTACK.description": "Attacks with ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura of Resistance", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Nearby stacks get ${val}% magic resistance", - "core.bonus.SUMMON_GUARDIANS.name": "Summon guardians", - "core.bonus.SUMMON_GUARDIANS.description": "At the start of battle summons ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Synergizable", + "core.bonus.SPELL_LIKE_ATTACK.name": "Útok kouzlem", + "core.bonus.SPELL_LIKE_ATTACK.description": "Útočí kouzlem ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura odolnosti", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Oddíly poblíž získají ${val}% magickou odolnost", + "core.bonus.SUMMON_GUARDIANS.name": "Povolat strážce", + "core.bonus.SUMMON_GUARDIANS.description": "Na začátku bitvy povolá ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Synergizable", // TODO "core.bonus.SYNERGY_TARGET.description": "This creature is vulnerable to synergy effect", - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Breath", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Breath Attack (2-hex range)", - "core.bonus.THREE_HEADED_ATTACK.name": "Three-headed attack", - "core.bonus.THREE_HEADED_ATTACK.description": "Attacks three adjacent units", - "core.bonus.TRANSMUTATION.name": "Transmutation", - "core.bonus.TRANSMUTATION.description": "${val}% chance to transform attacked unit to a different type", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Dech", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Dechový útok (dosah do dvou polí)", + "core.bonus.THREE_HEADED_ATTACK.name": "Tříhlavý útok", + "core.bonus.THREE_HEADED_ATTACK.description": "Útočí na tři sousední jednotky", + "core.bonus.TRANSMUTATION.name": "Transmutace", + "core.bonus.TRANSMUTATION.description": "${val}% šance na přeměnu útočené jednotky na jiný druh", "core.bonus.UNDEAD.name": "Nemrtvý", "core.bonus.UNDEAD.description": "Jednotka je nemrtvá", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Unlimited retaliations", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Can retaliate against an unlimited number of attacks", - "core.bonus.WATER_IMMUNITY.name": "Water immunity", - "core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic", - "core.bonus.WIDE_BREATH.name": "Wide breath", - "core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)" + "core.bonus.UNLIMITED_RETALIATIONS.name": "Neomezené odvety", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Může se mstít za neomezený počet útoků", + "core.bonus.WATER_IMMUNITY.name": "Vodní odolnost", + "core.bonus.WATER_IMMUNITY.description": "Imunní všem kouzlům školy vodní magie", + "core.bonus.WIDE_BREATH.name": "Široký dech", + "core.bonus.WIDE_BREATH.description": "Útočí širokým dechem (více polí)" } diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 079e3de1a..699290e5c 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -13,13 +13,14 @@ "vcmi.adventureMap.monsterThreat.levels.10" : "Deadly", "vcmi.adventureMap.monsterThreat.levels.11" : "Impossible", - "vcmi.adventureMap.confirmRestartGame" : "Are you sure you want to restart the game?", - "vcmi.adventureMap.noTownWithMarket" : "There are no available marketplaces!", - "vcmi.adventureMap.noTownWithTavern" : "There are no available towns with taverns!", - "vcmi.adventureMap.spellUnknownProblem" : "There is an unknown problem with this spell! No more information is available.", - "vcmi.adventureMap.playerAttacked" : "Player has been attacked: %s", - "vcmi.adventureMap.moveCostDetails" : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns" : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING", + "vcmi.adventureMap.confirmRestartGame" : "Are you sure you want to restart the game?", + "vcmi.adventureMap.noTownWithMarket" : "There are no available marketplaces!", + "vcmi.adventureMap.noTownWithTavern" : "There are no available towns with taverns!", + "vcmi.adventureMap.spellUnknownProblem" : "There is an unknown problem with this spell! No more information is available.", + "vcmi.adventureMap.playerAttacked" : "Player has been attacked: %s", + "vcmi.adventureMap.moveCostDetails" : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!", "vcmi.capitalColors.0" : "Red", "vcmi.capitalColors.1" : "Blue", @@ -54,13 +55,14 @@ "vcmi.radialWheel.moveDown" : "Move down", "vcmi.radialWheel.moveBottom" : "Move to bottom", + "vcmi.spellBook.search" : "search...", + "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", "vcmi.mainMenu.serverConnectionFailed" : "Failed to connect", "vcmi.mainMenu.serverClosing" : "Closing...", "vcmi.mainMenu.hostTCP" : "Host TCP/IP game", "vcmi.mainMenu.joinTCP" : "Join TCP/IP game", - "vcmi.mainMenu.playerName" : "Player", "vcmi.lobby.filepath" : "File path", "vcmi.lobby.creationDate" : "Creation date", @@ -68,11 +70,38 @@ "vcmi.lobby.mapPreview" : "Map preview", "vcmi.lobby.noPreview" : "no preview", "vcmi.lobby.noUnderground" : "no underground", + "vcmi.lobby.sortDate" : "Sorts maps by change date", + + "vcmi.lobby.login.title" : "VCMI Lobby", + "vcmi.lobby.login.username" : "Username:", + "vcmi.lobby.login.connecting" : "Connecting...", + "vcmi.lobby.login.error" : "Connection error: %s", + "vcmi.lobby.login.create" : "New Account", + "vcmi.lobby.login.login" : "Login", + "vcmi.lobby.room.create" : "Create Room", + "vcmi.lobby.room.players.limit" : "Players Limit", + "vcmi.lobby.room.public" : "Public", + "vcmi.lobby.room.private" : "Private", + "vcmi.lobby.room.description.public" : "Any player can join public room.", + "vcmi.lobby.room.description.private" : "Only invited players can join private room.", + "vcmi.lobby.room.description.new" : "To start the game, select a scenario or set up a random map.", + "vcmi.lobby.room.description.load" : "To start the game, use one of your saved games.", + "vcmi.lobby.room.description.limit" : "Up to %d players can enter your room, including you.", + "vcmi.lobby.room.new" : "New Game", + "vcmi.lobby.room.load" : "Load Game", + "vcmi.lobby.room.type" : "Room Type", + "vcmi.lobby.room.mode" : "Game Mode", + + "vcmi.client.errors.invalidMap" : "{Invalid map or campaign}\n\nFailed to start game! Selected map or campaign might be invalid or corrupted. Reason:\n%s", + "vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.", + "vcmi.server.errors.disconnected" : "{Network Error}\n\nConnection to game server has been lost!", "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", "vcmi.server.errors.modsToEnable" : "{Following mods are required}", "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", - "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", + "vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n", + "vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", @@ -108,7 +137,11 @@ "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Enhancements", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements. Such as a larger spell book, backpack, etc. Disable to have a more classic experience.", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements. Such as a backpack button etc. Disable to have a more classic experience.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Large Spell Book", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Large Spell Book}\n\nEnables larger spell book that fits more spells per page. Spell book page change animation does not work with this setting enabled.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Mute on inactivity", + "vcmi.systemOptions.audioMuteFocus.help" : "{Mute on inactivity}\n\nMute audio on inactive window focus. Exceptions are ingame messages and new turn sound.", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", @@ -124,12 +157,18 @@ "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.", "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map", "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", + "vcmi.adventureOptions.smoothDragging.hover" : "Smooth Map Dragging", + "vcmi.adventureOptions.smoothDragging.help" : "{Smooth Map Dragging}\n\nWhen enabled, map dragging has a modern run out effect.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skip fading effects", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Skip fading effects}\n\nWhen enabled, Skips object fadeout and similar effects (resource collection, ship embark etc). Makes UI more reactive in some cases at the expense of aesthetics. Especially useful in PvP games. For maximum movement speed skipping is active regardless of this setting.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow.", "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast.", "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.", + "vcmi.adventureOptions.hideBackground.hover" : "Hide Background", + "vcmi.adventureOptions.hideBackground.help" : "{Hide Background}\n\nHide the adventuremap in the background and show a texture instead.", "vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue", "vcmi.battleOptions.queueSizeNoneButton.hover": "OFF", @@ -153,7 +192,12 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows", "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", + "vcmi.battleOptions.endWithAutocombat.hover": "Ends battle", + "vcmi.battleOptions.endWithAutocombat.help": "{Ends battle}\n\nAuto-Combat plays battle to end instant", + + "vcmi.adventureMap.revisitObject.hover" : "Revisit Object", + "vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately", "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", @@ -166,9 +210,23 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d damage", "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", + "vcmi.battleWindow.killed" : "Killed", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s were killed by accurate shots!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s was killed with an accurate shot!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s were killed by accurate shots!", + "vcmi.battleWindow.endWithAutocombat" : "Are you sure you wish to end the battle with auto combat?", "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", + "vcmi.tutorialWindow.title" : "Touchscreen Introduction", + "vcmi.tutorialWindow.decription.RightClick" : "Touch and hold the element on which you want to right-click. Touch the free area to close.", + "vcmi.tutorialWindow.decription.MapPanning" : "Touch and drag with one finger to move the map.", + "vcmi.tutorialWindow.decription.MapZooming" : "Pinch with two fingers to change the map zoom.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Swiping opens radial wheel for various actions, such as creature/hero management and town ordering.", + "vcmi.tutorialWindow.decription.BattleDirection" : "To attack from a particular direction, swipe in the direction from which the attack is to be made.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "The attack direction gesture can be cancelled if the finger is far enough away.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Touch and hold to cancel a spell.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nShow the number of creatures available to purchase instead of their growth in town summary (bottom-left corner of town screen).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", @@ -199,6 +257,8 @@ "vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window", "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management.", + "vcmi.tavernWindow.inviteHero" : "Invite hero", + "vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?", "vcmi.creatureWindow.showBonuses.hover" : "Switch to bonuses view", @@ -217,14 +277,75 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Road Types", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", - "vcmi.optionsTab.widgets.labelTimer" : "Timer", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Classic timer", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Chess timer", + "vcmi.optionsTab.turnOptions.hover" : "Turn Options", + "vcmi.optionsTab.turnOptions.help" : "Select turn timer and simultaneous turns options", + "vcmi.optionsTab.selectPreset" : "Preset", + "vcmi.optionsTab.chessFieldBase.hover" : "Base timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer", + "vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer", + "vcmi.optionsTab.chessFieldUnit.hover" : "Unit timer", + "vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Any unspent time is lost", + "vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Used when selecting unit action in pvp combat. Leftover added to {Battle Timer} at end of unit turn.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn. Any unspent time is lost", + + "vcmi.optionsTab.accumulate" : "Accumulate", + + "vcmi.optionsTab.simturnsTitle" : "Simultaneous turns", + "vcmi.optionsTab.simturnsMin.hover" : "At least for", + "vcmi.optionsTab.simturnsMax.hover" : "At most for", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", + "vcmi.optionsTab.simturnsMin.help" : "Play simultaneously for specified number of days. Contacts between players during this period are blocked", + "vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player", + "vcmi.optionsTab.simturnsAI.help" : "{Simultaneous AI Turns}\nExperimental option. Allows AI players to act at the same time as human player when simultaneous turns are enabled.", + + "vcmi.optionsTab.turnTime.select" : "Select turn timer preset", + "vcmi.optionsTab.turnTime.unlimited" : "Unlimited turn time", + "vcmi.optionsTab.turnTime.classic.1" : "Classic timer: 1 minute", + "vcmi.optionsTab.turnTime.classic.2" : "Classic timer: 2 minutes", + "vcmi.optionsTab.turnTime.classic.5" : "Classic timer: 5 minutes", + "vcmi.optionsTab.turnTime.classic.10" : "Classic timer: 10 minutes", + "vcmi.optionsTab.turnTime.classic.20" : "Classic timer: 20 minutes", + "vcmi.optionsTab.turnTime.classic.30" : "Classic timer: 30 minutes", + "vcmi.optionsTab.turnTime.chess.20" : "Chess: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Chess: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Chess: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Chess: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Chess: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Chess: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Select simultaneous turns preset", + "vcmi.optionsTab.simturns.none" : "No simultaneous turns", + "vcmi.optionsTab.simturns.tillContactMax" : "Simturns: Until contact", + "vcmi.optionsTab.simturns.tillContact1" : "Simturns: 1 week, break on contact", + "vcmi.optionsTab.simturns.tillContact2" : "Simturns: 2 weeks, break on contact", + "vcmi.optionsTab.simturns.tillContact4" : "Simturns: 1 month, break on contact", + "vcmi.optionsTab.simturns.blocked1" : "Simturns: 1 week, contacts blocked", + "vcmi.optionsTab.simturns.blocked2" : "Simturns: 2 weeks, contacts blocked", + "vcmi.optionsTab.simturns.blocked4" : "Simturns: 1 month, contacts blocked", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d days", + "vcmi.optionsTab.simturns.days.1" : " %d day", + "vcmi.optionsTab.simturns.days.2" : " %d days", + "vcmi.optionsTab.simturns.weeks.0" : " %d weeks", + "vcmi.optionsTab.simturns.weeks.1" : " %d week", + "vcmi.optionsTab.simturns.weeks.2" : " %d weeks", + "vcmi.optionsTab.simturns.months.0" : " %d months", + "vcmi.optionsTab.simturns.months.1" : " %d month", + "vcmi.optionsTab.simturns.months.2" : " %d months", + + "vcmi.optionsTab.extraOptions.hover" : "Extra Options", + "vcmi.optionsTab.extraOptions.help" : "Additional settings for the game", + + "vcmi.optionsTab.cheatAllowed.hover" : "Allow cheats", + "vcmi.optionsTab.unlimitedReplay.hover" : "Unlimited battle replay", + "vcmi.optionsTab.cheatAllowed.help" : "{Allow cheats}\nAllows the inputs of cheats during the game.", + "vcmi.optionsTab.unlimitedReplay.help" : "{Unlimited battle replay}\nNo limit of replaying battles.", // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!", @@ -234,6 +355,7 @@ "vcmi.map.victoryCondition.collectArtifacts.message" : "Acquire Three Artifacts", "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Congratulations! All your enemies have been defeated and you have Angelic Alliance! Victory is yours!", "vcmi.map.victoryCondition.angelicAlliance.message" : "Defeat All Enemies and create Angelic Alliance", + "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Alas, you have lost part of the Angelic Alliance. All is lost.", // 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", @@ -248,7 +370,7 @@ "vcmi.stackExperience.rank.8" : "Elite", "vcmi.stackExperience.rank.9" : "Master", "vcmi.stackExperience.rank.10" : "Ace", - + "core.bonus.ADDITIONAL_ATTACK.name": "Double Strike", "core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice", "core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations", @@ -287,6 +409,8 @@ "core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn", "core.bonus.ENCHANTED.name": "Enchanted", "core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignore Attack (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "When being attacked, ${val}% of the attacker's attack is ignored", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored", "core.bonus.FIRE_IMMUNITY.name": "Fire immunity", @@ -299,6 +423,8 @@ "core.bonus.FEAR.description": "Causes Fear on an enemy stack", "core.bonus.FEARLESS.name": "Fearless", "core.bonus.FEARLESS.description": "Immune to Fear ability", + "core.bonus.FEROCITY.name": "Ferocity", + "core.bonus.FEROCITY.description": "Attacks ${val} additional times if killed anybody", "core.bonus.FLYING.name": "Fly", "core.bonus.FLYING.description": "Flies when moving (ignores obstacles)", "core.bonus.FREE_SHOOTING.name": "Shoot Close", @@ -353,6 +479,8 @@ "core.bonus.REBIRTH.description": "${val}% of stack will rise after death", "core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return", "core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack", + "core.bonus.REVENGE.name": "Revenge", + "core.bonus.REVENGE.description": "Deals extra damage based on attacker's lost health in battle", "core.bonus.SHOOTER.name": "Ranged", "core.bonus.SHOOTER.description": "Creature can shoot", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 4c1cbdb79..bf2f4f6ef 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -13,13 +13,14 @@ "vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich", "vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich", - "vcmi.adventureMap.confirmRestartGame" : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?", - "vcmi.adventureMap.noTownWithMarket" : "Kein Marktplatz verfügbar!", - "vcmi.adventureMap.noTownWithTavern" : "Keine Stadt mit Taverne verfügbar!", - "vcmi.adventureMap.spellUnknownProblem" : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.", - "vcmi.adventureMap.playerAttacked" : "Spieler wurde attackiert: %s", - "vcmi.adventureMap.moveCostDetails" : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING", + "vcmi.adventureMap.confirmRestartGame" : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?", + "vcmi.adventureMap.noTownWithMarket" : "Kein Marktplatz verfügbar!", + "vcmi.adventureMap.noTownWithTavern" : "Keine Stadt mit Taverne verfügbar!", + "vcmi.adventureMap.spellUnknownProblem" : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.", + "vcmi.adventureMap.playerAttacked" : "Spieler wurde attackiert: %s", + "vcmi.adventureMap.moveCostDetails" : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Das Wiederholen des gegnerischen Zuges ist aktuell noch nicht implementiert!", "vcmi.capitalColors.0" : "Rot", "vcmi.capitalColors.1" : "Blau", @@ -36,10 +37,25 @@ "vcmi.heroOverview.spells" : "Zaubersprüche", "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", + "vcmi.radialWheel.fillSingleUnit" : "Füllen mit einzelnen Kreaturen", "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", + + "vcmi.radialWheel.heroGetArmy" : "Armee von anderem Helden erhalten", + "vcmi.radialWheel.heroSwapArmy" : "Tausche Armee mit anderem Helden", + "vcmi.radialWheel.heroExchange" : "Öffne Tausch-Menü", + "vcmi.radialWheel.heroGetArtifacts" : "Artefakte von anderen Helden erhalten", + "vcmi.radialWheel.heroSwapArtifacts" : "Tausche Artefakte mit anderen Helden", + "vcmi.radialWheel.heroDismiss" : "Held entlassen", + + "vcmi.radialWheel.moveTop" : "Ganz nach oben bewegen", + "vcmi.radialWheel.moveUp" : "Nach oben bewegen", + "vcmi.radialWheel.moveDown" : "Nach unten bewegen", + "vcmi.radialWheel.moveBottom" : "Ganz nach unten bewegen", + + "vcmi.spellBook.search" : "suchen...", "vcmi.mainMenu.serverConnecting" : "Verbinde...", "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", @@ -55,10 +71,16 @@ "vcmi.lobby.mapPreview" : "Kartenvorschau", "vcmi.lobby.noPreview" : "Keine Vorschau", "vcmi.lobby.noUnderground" : "Kein Untergrund", + "vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum", + "vcmi.client.errors.missingCampaigns" : "{Fehlende Dateien}\n\nEs wurden keine Kampagnendateien gefunden! Möglicherweise verwendest du unvollständige oder beschädigte Heroes 3 Datendateien. Bitte installiere die Spieldaten neu.", "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", + "vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}", "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", + "vcmi.server.errors.modNoDependency" : "Mod {'%s'} konnte nicht geladen werden!\n Sie hängt von Mod {'%s'} ab, die nicht aktiv ist!\n", + "vcmi.server.errors.modConflict" : "Mod {'%s'} konnte nicht geladen werden!\n Konflikte mit aktiver Mod {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!", "vcmi.settingsMainWindow.generalTab.hover" : "Allgemein", "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", @@ -82,7 +104,7 @@ "vcmi.systemOptions.resolutionMenu.help" : "Ändere die Spielauflösung.", "vcmi.systemOptions.scalingButton.hover" : "Interface-Skalierung: %p%", "vcmi.systemOptions.scalingButton.help" : "{Interface-Skalierung}\n\nÄndern der Skalierung des Interfaces im Spiel", - "vcmi.systemOptions.scalingMenu.hover" : "Skalierung des Interfaces auswählen", + "vcmi.systemOptions.scalingMenu.hover" : "Skalierung auswählen", "vcmi.systemOptions.scalingMenu.help" : "Ändern der Skalierung des Interfaces im Spiel.", "vcmi.systemOptions.longTouchButton.hover" : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds" "vcmi.systemOptions.longTouchButton.help" : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)", @@ -94,7 +116,11 @@ "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Verbesserungen", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein größeres Zauberbuch, Rucksack, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein Rucksack-Button, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Großes Zauberbuch", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Großes Zauberbuch}\n\nErmöglicht ein größeres Zauberbuch, in das mehr Zaubersprüche pro Seite passen. Die Animation des Seitenwechsels im Zauberbuch funktioniert nicht, wenn diese Einstellung aktiviert ist.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Stumm bei Inaktivität", + "vcmi.systemOptions.audioMuteFocus.help" : "{Stumm bei Inaktivität}\n\nSchaltet Audio bei inaktiven Fenster-Fokus stumm. Ausnahmen sind Ingame-Nachrichten und der Neuer-Zug-Sound.", "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", @@ -110,12 +136,18 @@ "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln", "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links", "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen", + "vcmi.adventureOptions.smoothDragging.hover" : "Nahtloses Ziehen der Karte", + "vcmi.adventureOptions.smoothDragging.help" : "{Nahtloses Ziehen der Karte}\n\nWenn aktiviert hat das Ziehen der Karte einen sanften Auslaufeffekt.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fading-Effekte überspringen", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Fading-Effekte überspringen}\n\nWenn diese Funktion aktiviert ist, werden das Ausblenden von Objekten und ähnliche Effekte übersprungen (Ressourcensammlung, Anlegen von Schiffen usw.). Macht die Benutzeroberfläche in einigen Fällen auf Kosten der Ästhetik reaktiver. Besonders nützlich in PvP-Spielen. Für maximale Bewegungsgeschwindigkeit ist das Überspringen unabhängig von dieser Einstellung aktiv.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen", "vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen", "vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen", + "vcmi.adventureOptions.hideBackground.hover" : "Hintergrund ausblenden", + "vcmi.adventureOptions.hideBackground.help" : "{Hintergrund ausblenden}\n\nDie Abenteuerkarte im Hintergrund ausblenden und stattdessen eine Textur anzeigen.", "vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen", "vcmi.battleOptions.queueSizeNoneButton.hover": "AUS", @@ -140,6 +172,11 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.", + "vcmi.battleOptions.endWithAutocombat.hover": "Kampf beenden", + "vcmi.battleOptions.endWithAutocombat.help": "{Kampf beenden}\n\nAutokampf spielt den Kampf sofort zu Ende", + + "vcmi.adventureMap.revisitObject.hover" : "Objekt erneut besuchen", + "vcmi.adventureMap.revisitObject.help" : "{Objekt erneut besuchen}\n\nSteht ein Held gerade auf einem Kartenobjekt, kann er den Ort erneut aufsuchen.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", @@ -152,9 +189,23 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden", "vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden", "vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden", + "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!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s wurden durch gezielte Schüsse getötet!", + "vcmi.battleWindow.endWithAutocombat" : "Seid Ihr sicher, dass Ihr den Kampf mit Auto-Kampf beenden wollt?", "vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen", + "vcmi.tutorialWindow.title" : "Touchscreen Einführung", + "vcmi.tutorialWindow.decription.RightClick" : "Berührt und haltet das Element, auf das mit der rechten Maustaste geklickt werden soll. Berührt den freien Bereich, um zu schließen.", + "vcmi.tutorialWindow.decription.MapPanning" : "Berührt und zieht mit einem Finger, um die Karte zu verschieben.", + "vcmi.tutorialWindow.decription.MapZooming" : "Berührt mit zwei Fingern, um den Kartenzoom zu ändern.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Durch Streichen öffnet sich das Radialrad für verschiedene Aktionen, wie z.B. Kreaturen-/Heldenverwaltung und Stadtreihenfolge.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Um aus einer bestimmten Richtung anzugreifen, wischt in die Richtung, aus der der Angriff erfolgen soll.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Die Geste für die Angriffsrichtung kann abgebrochen werden, wenn der Finger weit genug entfernt ist.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Berühren und halten, um einen Zauber abzubrechen.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Verfügbare Kreaturen anzeigen", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Verfügbare Kreaturen anzeigen}\n\n Zeigt in der Stadtübersicht (linke untere Ecke) die zum Kauf verfügbaren Kreaturen anstelle ihres Wachstums an.", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentl. Wachstum der Kreaturen anz.", @@ -182,6 +233,10 @@ "vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster", "vcmi.heroWindow.openCommander.help" : "Zeige Informationen über Kommandanten dieses Helden", + "vcmi.heroWindow.openBackpack.hover" : "Artefakt-Rucksack-Fenster öffnen", + "vcmi.heroWindow.openBackpack.help" : "Öffnet ein Fenster, das die Verwaltung des Artefakt-Rucksacks erleichtert", + + "vcmi.tavernWindow.inviteHero" : "Helden einladen", "vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?", @@ -201,6 +256,76 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", + "vcmi.optionsTab.turnOptions.hover" : "Spielzug-Optionen", + "vcmi.optionsTab.turnOptions.help" : "Optionen zu Spielzug-Timer und simultanen Zügen", + "vcmi.optionsTab.selectPreset" : "Voreinstellung", + + "vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Spielzug-Timer", + "vcmi.optionsTab.chessFieldBattle.hover" : "Kampf-Timer", + "vcmi.optionsTab.chessFieldUnit.hover" : "Einheiten-Timer", + "vcmi.optionsTab.chessFieldBase.help" : "Wird verwendet, wenn {Spielzug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Zug beendet. Jeder laufende Kampf endet mit einem Verlust.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren", + "vcmi.optionsTab.chessFieldBattle.help" : "Wird in Kämpfen mit der KI oder im PvP-Kampf verwendet, wenn der {Einheiten-Timer} abläuft. Wird zu Beginn eines jeden Kampfes zurückgesetzt.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Der Rest wird am Ende des Zuges der Einheit zum {Kampf-Timer} hinzugefügt.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn des Zuges jeder Einheit zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren", + + "vcmi.optionsTab.accumulate" : "Akkumulieren", + + "vcmi.optionsTab.simturnsTitle" : "Simultane Züge", + "vcmi.optionsTab.simturnsMin.hover" : "Zumindest für", + "vcmi.optionsTab.simturnsMax.hover" : "Höchstens für", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimentell) Simultane KI Züge", + "vcmi.optionsTab.simturnsMin.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen. Die Kontakte zwischen den Spielern sind während dieser Zeit blockiert", + "vcmi.optionsTab.simturnsMax.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen oder bis zum Kontakt mit einem anderen Spieler", + "vcmi.optionsTab.simturnsAI.help" : "{Simultane KI Züge}\nExperimentelle Option. Ermöglicht es den KI-Spielern, gleichzeitig mit dem menschlichen Spieler zu agieren, wenn simultane Spielzüge aktiviert sind.", + + "vcmi.optionsTab.turnTime.select" : "Spielzug-Timer-Voreinst. wählen", + "vcmi.optionsTab.turnTime.unlimited" : "Unbegrenzter Spielzug-Timer", + "vcmi.optionsTab.turnTime.classic.1" : "Klassischer Timer: 1 Minute", + "vcmi.optionsTab.turnTime.classic.2" : "Klassischer Timer: 2 Minuten", + "vcmi.optionsTab.turnTime.classic.5" : "Klassischer Timer: 5 Minuten", + "vcmi.optionsTab.turnTime.classic.10" : "Klassischer Timer: 10 Minuten", + "vcmi.optionsTab.turnTime.classic.20" : "Klassischer Timer: 20 Minuten", + "vcmi.optionsTab.turnTime.classic.30" : "Klassischer Timer: 30 Minuten", + "vcmi.optionsTab.turnTime.chess.20" : "Schach: 20:00 10:00 02:00 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Schach: 16:00 08:00 01:30 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Schach: 08:00 04:00 01:00 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Schach: 04:00 02:00 00:30 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Schach: 02:00 01:00 00:15 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Schach: 01:00 01:00 00:00 00:00", + + "vcmi.optionsTab.simturns.select" : "Voreinst. für simultane Züge wählen", + "vcmi.optionsTab.simturns.none" : "Keine simultanen Züge", + "vcmi.optionsTab.simturns.tillContactMax" : "Simzüge: Bis zum Kontakt", + "vcmi.optionsTab.simturns.tillContact1" : "Simzüge: 1 Woche, Stop bei Kontakt", + "vcmi.optionsTab.simturns.tillContact2" : "Simzüge: 2 Wochen, Stop bei Kontakt", + "vcmi.optionsTab.simturns.tillContact4" : "Simzüge: 1 Monat, Stop bei Kontakt", + "vcmi.optionsTab.simturns.blocked1" : "Simzüge: 1 Woche, Kontakte block.", + "vcmi.optionsTab.simturns.blocked2" : "Simzüge: 2 Wochen, Kontakte block.", + "vcmi.optionsTab.simturns.blocked4" : "Simzüge: 1 Monat, Kontakte block.", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : "%d Tage", + "vcmi.optionsTab.simturns.days.1" : "%d Tag", + "vcmi.optionsTab.simturns.days.2" : "%d Tage", + "vcmi.optionsTab.simturns.weeks.0" : "%d Wochen", + "vcmi.optionsTab.simturns.weeks.1" : "%d Woche", + "vcmi.optionsTab.simturns.weeks.2" : "%d Wochen", + "vcmi.optionsTab.simturns.months.0" : "%d Monate", + "vcmi.optionsTab.simturns.months.1" : "%d Monat", + "vcmi.optionsTab.simturns.months.2" : "%d Monate", + + "vcmi.optionsTab.extraOptions.hover" : "Extra Optionen", + "vcmi.optionsTab.extraOptions.help" : "Zusätzliche Einstellungen für das Spiel", + + "vcmi.optionsTab.cheatAllowed.hover" : "Cheats erlauben", + "vcmi.optionsTab.unlimitedReplay.hover" : "Unbegrenzte Kampfwiederholung", + "vcmi.optionsTab.cheatAllowed.help" : "{Cheats erlauben}\nErlaubt die Eingabe von Cheats während des Spiels.", + "vcmi.optionsTab.unlimitedReplay.help" : "{Unbegrenzte Kampfwiederholung}\nKämpfe lassen sich unbegrenzt wiederholen.", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!", @@ -223,7 +348,7 @@ "vcmi.stackExperience.rank.8" : "Elite", "vcmi.stackExperience.rank.9" : "Meister", "vcmi.stackExperience.rank.10" : "Ass", - + "core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag", "core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an", "core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen", @@ -262,6 +387,8 @@ "core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern", "core.bonus.ENCHANTED.name": "Verzaubert", "core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Angriff ignorieren (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Bei Angriff, wird ${val}% des Angreifers ignoriert.", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff", "core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität", @@ -274,6 +401,8 @@ "core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel", "core.bonus.FEARLESS.name": "Furchtlos", "core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht", + "core.bonus.FEROCITY.name": "Wildheit", + "core.bonus.FEROCITY.description": "Greift ${val} zusätzliche Male an, wenn jemand getötet wird", "core.bonus.FLYING.name": "Fliegen", "core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)", "core.bonus.FREE_SHOOTING.name": "Nah schießen", @@ -328,6 +457,8 @@ "core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen", "core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr", "core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück", + "core.bonus.REVENGE.name": "Rache", + "core.bonus.REVENGE.description": "Verursacht zusätzlichen Schaden basierend auf der verlorenen Gesundheit des Angreifers im Kampf", "core.bonus.SHOOTER.name": "Fernkämpfer", "core.bonus.SHOOTER.description": "Kreatur kann schießen", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index 1526e83e3..904f478fc 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -30,14 +30,35 @@ "vcmi.capitalColors.6" : "Jasnoniebieski", "vcmi.capitalColors.7" : "Różowy", + "vcmi.heroOverview.startingArmy" : "Jednostki startowe", + "vcmi.heroOverview.warMachine" : "Machiny wojenne", + "vcmi.heroOverview.secondarySkills" : "Umiejętności drugorzędne", + "vcmi.heroOverview.spells" : "Zaklęcia", + "vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia", + "vcmi.radialWheel.fillSingleUnit" : "Wypełnij pojedynczymi stworzeniami", "vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie", "vcmi.radialWheel.splitUnitEqually" : "Podziel stworzenia równo", "vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii", "vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca", + "vcmi.radialWheel.heroGetArmy" : "Weź armię z innego bohatera", + "vcmi.radialWheel.heroSwapArmy" : "Zamień armię z innym bohaterem", + "vcmi.radialWheel.heroExchange" : "Rozpocznij wymianę między bohaterami", + "vcmi.radialWheel.heroGetArtifacts" : "Weź artefakty z innego bohatera", + "vcmi.radialWheel.heroSwapArtifacts" : "Zamień artefakty z innym bohaterem", + "vcmi.radialWheel.heroDismiss" : "Dymisja bohatera", + + "vcmi.radialWheel.moveTop" : "Przenieś na początek", + "vcmi.radialWheel.moveUp" : "Przenieś w górę", + "vcmi.radialWheel.moveDown" : "Przenieś w dół", + "vcmi.radialWheel.moveBottom" : "Przenieś na spód", + + "vcmi.spellBook.search" : "szukaj...", + "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", + "vcmi.mainMenu.serverConnectionFailed" : "Połączenie nie powiodło się", "vcmi.mainMenu.serverClosing" : "Zamykanie...", "vcmi.mainMenu.hostTCP" : "Hostuj grę TCP/IP", "vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP", @@ -45,10 +66,20 @@ "vcmi.lobby.filepath" : "Nazwa pliku", "vcmi.lobby.creationDate" : "Data utworzenia", + "vcmi.lobby.scenarioName" : "Nazwa scenariusza", + "vcmi.lobby.mapPreview" : "Podgląd mapy", + "vcmi.lobby.noPreview" : "brak podglądu", + "vcmi.lobby.noUnderground" : "brak podziemi", + "vcmi.lobby.sortDate" : "Sortuj mapy według daty modyfikacji", + "vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.", "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", + "vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}", "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", + "vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n", + "vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", "vcmi.settingsMainWindow.generalTab.hover" : "Ogólne", "vcmi.settingsMainWindow.generalTab.help" : "Przełącza do zakładki opcji ogólnych, która zawiera ustawienia związane z ogólnym działaniem gry", @@ -62,7 +93,7 @@ "vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now "vcmi.systemOptions.townsGroup" : "Ekran miasta", - "vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (borderless/okno pełnoekranowe)", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)", "vcmi.systemOptions.fullscreenBorderless.help" : "{Pełny ekran w trybie okna}\n\nPo wybraniu VCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.", "vcmi.systemOptions.fullscreenExclusive.hover" : "Pełny ekran (tradycyjny)", "vcmi.systemOptions.fullscreenExclusive.help" : "{Pełny ekran}\n\nPo wybraniu VCMI będzie działać w trybie ekskluzywnego pełnego ekranu. W tym trybie gra zmieni rozdzielczość monitora do wybranej rozdzielczości.", @@ -77,12 +108,18 @@ "vcmi.systemOptions.longTouchButton.hover" : "Czas długiego dotyku: %d ms", // Translation note: "ms" = "milliseconds" "vcmi.systemOptions.longTouchButton.help" : "{Czas długiego dotyku}\n\nPodczas używania ekranu dotykowego, wyskakujące okienka pojawią się po dotknięciu ekranu przez określony czas, w milisekundach", "vcmi.systemOptions.longTouchMenu.hover" : "Wybierz czas długiego dotyku", - "vcmi.systemOptions.longTouchMenu.help" : "Zmienia czas wymagany do aktywacji długiego dotyku.", + "vcmi.systemOptions.longTouchMenu.help" : "Nowy czas aktywacji długiego dotyku.", "vcmi.systemOptions.longTouchMenu.entry" : "%d milisekund", "vcmi.systemOptions.framerateButton.hover" : "Pokaż FPS", "vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Wibracje urządzenia", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Ulepszenia interfejsu", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Ulepszenia interfejsu}\n\nWłącza różne ulepszenia interfejsu poprawiające wygodę rozgrywki. Takie jak przycisk sakwy bohatera itp. Wyłącz jeśli szukasz bardziej klasycznej wersji gry.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Duża księga zaklęć", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Duża księga zaklęć}\n\nWłącza dużą księgę czarów, która mieści więcej zaklęć na stronę. Animacja zmiany strony nie działa gdy ta opcja jest włączona.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Wycisz nieaktywną grę", + "vcmi.systemOptions.audioMuteFocus.help" : "{Wycisz nieaktywną grę}\n\nWycisza dźwięk gdy okno gry staje się nieaktywne. Wyjątkiem są dźwięki wiadomości i nowej tury.", "vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym", "vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.", @@ -98,12 +135,16 @@ "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.", "vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem", "vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nGdy włączone, umożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.", + "vcmi.adventureOptions.smoothDragging.hover" : "'Pływające' przeciąganie mapy", + "vcmi.adventureOptions.smoothDragging.help" : "{'Pływające' przeciąganie mapy}\n\nGdy włączone, przeciąganie mapy następuje ze stopniowo zanikającym przyspieszeniem.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "Ustaw szybkość przesuwania mapy na bardzo wolną.", "vcmi.adventureOptions.mapScrollSpeed5.help": "Ustaw szybkość przesuwania mapy na bardzo szybką.", "vcmi.adventureOptions.mapScrollSpeed6.help": "Ustaw szybkość przesuwania mapy na błyskawiczną.", + "vcmi.adventureOptions.hideBackground.hover" : "Ukryj tło", + "vcmi.adventureOptions.hideBackground.help" : "{Ukryj tło}\n\nUkryj mapę przygody w tle i pokaż zastępczo teksturę.", "vcmi.battleOptions.queueSizeLabel.hover": "Pokaż kolejkę ruchu jednostek", "vcmi.battleOptions.queueSizeNoneButton.hover": "BRAK", @@ -129,6 +170,9 @@ "vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.", + "vcmi.adventureMap.revisitObject.hover" : "Odwiedź obiekt ponownie", + "vcmi.adventureMap.revisitObject.help" : "{Odwiedź obiekt ponownie}\n\nJeżeli bohater aktualnie stoi na polu odwiedzającym obiekt za pomocą tego przycisku może go odwiedzić ponownie.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo", "vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).", @@ -140,13 +184,26 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "obrażenia: %d", "vcmi.battleWindow.damageEstimation.kills" : "zginie: %d", "vcmi.battleWindow.damageEstimation.kills.1" : "zginie: %d", + "vcmi.battleWindow.killed" : "Zabici", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s zostało zabitych poprzez celne strzały!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s został zabity poprzez celny strzał!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s zostali zabici poprzez celne strzały!", "vcmi.battleResultsWindow.applyResultsLabel" : "Zatwierdź wynik bitwy", + "vcmi.tutorialWindow.title" : "Samouczek ekranu dotykowego", + "vcmi.tutorialWindow.decription.RightClick" : "Dotknij i przytrzymaj palec na elemencie na którym chcesz wykonać akcję prawego przycisku myszy. Dotknij wolny obszar by zamknąć.", + "vcmi.tutorialWindow.decription.MapPanning" : "Dotknij i przeciągnij jednym palcem by przesunąć mapę.", + "vcmi.tutorialWindow.decription.MapZooming" : "Za pomocą gestu szczypania / rozwierania dwóch palców możesz zmieniać powiększenie mapy.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Przeciąganie palcem otwiera kołowe menu dla różnych akcji takich jak zarządzanie stworzeniami/bohaterami i sortowanie miast.", + "vcmi.tutorialWindow.decription.BattleDirection" : "By zaatakować z określonego kierunku przeciągnij palcem w stronę kierunku z którego chcesz zaatakować.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Atak z wyborem kierunku może zostać anulowany jeśli palec znajdzie się wystarczająco daleko.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Naciśnij i przytrzymaj by anulować rzucenie zaklęcia.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Pokaż dostępne stworzenia", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Pokazuje dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Wyświetla dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Pokaż tygodniowy przyrost stworzeń", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Pokaż tygodniowy przyrost stworzeń}\n\n Shows creatures' weekly growth instead of avaialable amount in town summary (lewy dolny r óg).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Pokaż tygodniowy przyrost stworzeń}\n\n Wyświetla tygodniowy przyrost jednostek zamiast dostępnej ilości do zakupu (lewy dolny róg).", "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompaktowa informacja o stworzeniu", "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktowa informacja o stworzeniu}\n\n Zmniejszona informacja o stworzeniu w podsumowaniu miasta.", @@ -191,6 +248,68 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Typy dróg", + "vcmi.optionsTab.turnOptions.hover" : "Ustawienia tur", + "vcmi.optionsTab.turnOptions.help" : "Ustaw limity czasu oraz tury symultaniczne", + "vcmi.optionsTab.selectPreset" : "Szablonowe ustawienie", + + "vcmi.optionsTab.chessFieldBase.hover" : "Zegar startowy", + "vcmi.optionsTab.chessFieldTurn.hover" : "Zegar tury", + "vcmi.optionsTab.chessFieldBattle.hover" : "Zegar bitewny", + "vcmi.optionsTab.chessFieldUnit.hover" : "Zegar jednostki", + "vcmi.optionsTab.chessFieldBase.help" : "Odlicza czas gdy {zegar tury} osiągnie 0. Czas nie odnawia się. Koniec czasu oznacza zakończenie tury, a trwająca bitwa zostanie przegrana.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Używany poza bitwą lub gdy {Zegar bitewny} się wyczerpie. Odnawia się co turę. Nadwyżkę dodaje się do {Zegara startowego} pod koniec tury.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Używany poza bitwą lub gdy {Zegar bitewny} się wyczerpie. Odnawia się co turę. Niewykorzystany czas zostaje utracony.", + "vcmi.optionsTab.chessFieldBattle.help" : "Używany w bitwach z graczem AI lub gdy {Zegar jednostki} się wyczerpie w bitwie pomiędzy ludźmi. Odnawia się przy starcie każdej bitwy.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Nadwyżka czasu dodaje się do {Timera bitwy}", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Resetuje się przy rozpoczęciu akcji jednostką.", + + "vcmi.optionsTab.accumulate" : "Akumuluj", + + "vcmi.optionsTab.simturnsTitle" : "Tury symultaniczne", + "vcmi.optionsTab.simturnsMin.hover" : "Minimum", + "vcmi.optionsTab.simturnsMax.hover" : "Maksimum", + "vcmi.optionsTab.simturnsAI.hover" : "(Eksperymentalne) Równoczesne tury graczy AI", + "vcmi.optionsTab.simturnsMin.help" : "Graj równocześnie przez określoną liczbę dni. Kontakt pomiędzy graczami do tego czasu jest zablokowany.", + "vcmi.optionsTab.simturnsMax.help" : "Graj równocześnie przez określoną liczbę dni lub do momentu napotkania innego gracza.", + "vcmi.optionsTab.simturnsAI.help" : "{Równoczesne tury graczy AI}\nOpcja eksperymentalna. Pozwala graczom sterowanym przez komputer wykonywać akcje w tym samym czasie co gracz ludzki gdy jednoczesne tury są włączone.", + + "vcmi.optionsTab.turnTime.select" : "Gotowe schematy zegarów", + "vcmi.optionsTab.turnTime.unlimited" : "Nieograniczony czas tury", + "vcmi.optionsTab.turnTime.classic.1" : "Klasyczny: 1 minuta", + "vcmi.optionsTab.turnTime.classic.2" : "Klasyczny: 2 minuty", + "vcmi.optionsTab.turnTime.classic.5" : "Klasyczny: 5 minut", + "vcmi.optionsTab.turnTime.classic.10" : "Klasyczny: 10 minut", + "vcmi.optionsTab.turnTime.classic.20" : "Klasyczny: 20 minut", + "vcmi.optionsTab.turnTime.classic.30" : "Klasyczny: 30 minut", + "vcmi.optionsTab.turnTime.chess.20" : "Szach: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Szach: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Szach: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Szach: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Szach: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Szach: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Gotowe schematy tur sym.", + "vcmi.optionsTab.simturns.none" : "Brak tur symultanicznych", + "vcmi.optionsTab.simturns.tillContactMax" : "Tury sym.: Do kontaktu", + "vcmi.optionsTab.simturns.tillContact1" : "Tury sym.: 1 tydz., kontakt: wł.", + "vcmi.optionsTab.simturns.tillContact2" : "Tury sym.: 2 tyg., kontakt: wł.", + "vcmi.optionsTab.simturns.tillContact4" : "Tury sym.: 1 mies., kontakt: wł.", + "vcmi.optionsTab.simturns.blocked1" : "Tury sym.: 1 tydz., kontakt: wył.", + "vcmi.optionsTab.simturns.blocked2" : "Tury sym.: 2 tyg., kontakt: wył.", + "vcmi.optionsTab.simturns.blocked4" : "Tury sym.: 1 mies., kontakt: wył.", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d dni", + "vcmi.optionsTab.simturns.days.1" : " %d dzień", + "vcmi.optionsTab.simturns.days.2" : " %d dni", + "vcmi.optionsTab.simturns.weeks.0" : " %d tygodni", + "vcmi.optionsTab.simturns.weeks.1" : " %d tydzień", + "vcmi.optionsTab.simturns.weeks.2" : " %d tygodnie", + "vcmi.optionsTab.simturns.months.0" : " %d miesięcy", + "vcmi.optionsTab.simturns.months.1" : " %d miesiąc", + "vcmi.optionsTab.simturns.months.2" : " %d miesiące", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Wróg dał radę przetrwać do dzisiejszego dnia. Zwycięstwo należy do niego!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulacje! Dałeś radę przetrwać. Zwycięstwo jest twoje!", @@ -203,12 +322,12 @@ //WoG strings translations missing here - should be taken from polish WoG translation "core.bonus.ADDITIONAL_ATTACK.name": "Podwójne Uderzenie", - "core.bonus.ADDITIONAL_ATTACK.description": "Atakuje podwójnie", - "core.bonus.ADDITIONAL_RETALIATION.name": "Dodatkowe kontrataki", - "core.bonus.ADDITIONAL_RETALIATION.description": "Może kontratakować ${val} dodatkowych razy", - "core.bonus.AIR_IMMUNITY.name": "Odporność na powietrze", + "core.bonus.ADDITIONAL_ATTACK.description": "Atakuje dwa razy", + "core.bonus.ADDITIONAL_RETALIATION.name": "Dodatkowy odwet", + "core.bonus.ADDITIONAL_RETALIATION.description": "${val} dodatkowy kontratak", + "core.bonus.AIR_IMMUNITY.name": "Odporność: Powietrze", "core.bonus.AIR_IMMUNITY.description": "Odporny na wszystkie czary szkoły powietrza", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Atakuje wszystko dookoła", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Obrotowy atak", "core.bonus.ATTACKS_ALL_ADJACENT.description": "Atakuje wszystkich sąsiadujących wrogów", "core.bonus.BLOCKS_RETALIATION.name": "Bez kontrataku", "core.bonus.BLOCKS_RETALIATION.description": "Wróg nie może kontratakować", @@ -216,11 +335,11 @@ "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Wróg nie może kontratakować poprzez strzelanie", "core.bonus.CATAPULT.name": "Katapulta", "core.bonus.CATAPULT.description": "Atakuje mury obronne", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Zmniejsz koszt czarów (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Zmniejsza koszt czaru bohatera", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tłumienie magii (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zwiększa koszt wrogich czarów", - "core.bonus.CHARGE_IMMUNITY.name": "Odporność na szarżę", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Profesja magii", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Zmniejsza o ${val} koszt czarów bohatera", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tłumienie magii", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zwiększa o ${val} koszt wrogich czarów", + "core.bonus.CHARGE_IMMUNITY.name": "Odporność: Szarża", "core.bonus.CHARGE_IMMUNITY.description": "Odporny na szarżę czempionów", "core.bonus.DARKNESS.name": "Całun ciemności", "core.bonus.DARKNESS.description": "Generuje ${val} wartości promienia mgły wojny", @@ -234,15 +353,17 @@ "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% szans na podwójne obrażenia", "core.bonus.DRAGON_NATURE.name": "Smok", "core.bonus.DRAGON_NATURE.description": "Stworzenie posiada smoczą naturę", - "core.bonus.EARTH_IMMUNITY.name": "Odporność na ziemię", + "core.bonus.EARTH_IMMUNITY.name": "Odporność: Ziemia", "core.bonus.EARTH_IMMUNITY.description": "Odporny na wszystkie czary szkoły ziemi", "core.bonus.ENCHANTER.name": "Czarodziej", - "core.bonus.ENCHANTER.description": "Może rzucać masowy czar ${subtype.spell} każdej tury", + "core.bonus.ENCHANTER.description": "Rzuca czar ${subtype.spell}", "core.bonus.ENCHANTED.name": "Zaczarowany", - "core.bonus.ENCHANTED.description": "Pod wpływem trwałego ${subtype.spell}", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoruje Obronę (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoruje część obrony podczas ataku", - "core.bonus.FIRE_IMMUNITY.name": "Odporność na ogień", + "core.bonus.ENCHANTED.description": "Pod trwałym wpływem czaru ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignoruje Atak (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Przy zostaniu zaatakowanym ignoruje ${val}% ataku wroga", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Osłabienie Obrony (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Osłabia obronę wroga podczas ataku", + "core.bonus.FIRE_IMMUNITY.name": "Odporność: Ogień", "core.bonus.FIRE_IMMUNITY.description": "Odporny na wszystkie czary szkoły ognia", "core.bonus.FIRE_SHIELD.name": "Ognista tarcza (${val}%)", "core.bonus.FIRE_SHIELD.description": "Odbija część obrażeń z walki wręcz", @@ -252,26 +373,28 @@ "core.bonus.FEAR.description": "Wzbudza strach na wrogim stworzeniu", "core.bonus.FEARLESS.name": "Nieustraszony", "core.bonus.FEARLESS.description": "Odporny na strach", + "core.bonus.FEROCITY.name": "Dzikość", + "core.bonus.FEROCITY.description": "Dodatkowe ${val} ataków jeżeli zabito kogokolwiek", "core.bonus.FLYING.name": "Lot", "core.bonus.FLYING.description": "Może latać (ignoruje przeszkody)", "core.bonus.FREE_SHOOTING.name": "Bliski Strzał", "core.bonus.FREE_SHOOTING.description": "Może strzelać w zasięgu walki wręcz", "core.bonus.GARGOYLE.name": "Gargulec", - "core.bonus.GARGOYLE.description": "Nie może być wskrzeszony lub uleczony", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Zmniejsz obrażenia (${val}%)", + "core.bonus.GARGOYLE.description": "Nie może się wskrzesić i uleczyć", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Redukcja obrażeń (${val}%)", "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Zmiejsza obrażenia fizyczne z dystansu lub walki wręcz", - "core.bonus.HATE.name": "Nienawidzi ${subtype.creature}", - "core.bonus.HATE.description": "Zadaje ${val}% więcej obrażeń", + "core.bonus.HATE.name": "${subtype.creature}", + "core.bonus.HATE.description": "+${val}% dodatkowych obrażeń", "core.bonus.HEALER.name": "Uzdrowiciel", "core.bonus.HEALER.description": "Leczy sprzymierzone jednostki", "core.bonus.HP_REGENERATION.name": "Regeneracja", - "core.bonus.HP_REGENERATION.description": "Leczy ${val} punktów zdrowia każdej rundy", + "core.bonus.HP_REGENERATION.description": "Leczy ${val} pkt. zdrowia co rundę", "core.bonus.JOUSTING.name": "Szarża Czempiona", "core.bonus.JOUSTING.description": "+${val}% obrażeń na przebytego heksa", - "core.bonus.KING.name": "Król", - "core.bonus.KING.description": "Wrażliwy na czar POGROMCA stopnia zaawansowania ${val} lub wyższego", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność na czary 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poziomu", + "core.bonus.KING.name": "Król - wrażliwy na", + "core.bonus.KING.description": "czar POGROMCA stopnia ${val}+", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność: Czary 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poz.", "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Ograniczony zasięg strzelania", "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nie może strzelać do celów będących dalej niż ${val} heksów", "core.bonus.LIFE_DRAIN.name": "Wysysa życie (${val}%)", @@ -282,17 +405,17 @@ "core.bonus.MANA_DRAIN.description": "Wysysa ${val} many każdej tury", "core.bonus.MAGIC_MIRROR.name": "Magiczne Zwierciadło (${val}%)", "core.bonus.MAGIC_MIRROR.description": "${val}% szans na odbicie ofensywnego czaru do wroga", - "core.bonus.MAGIC_RESISTANCE.name": "Odporność na Magię(${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "${val}% szans na przeciwstawienie się wrogiemu czarowi", - "core.bonus.MIND_IMMUNITY.name": "Odporność na czasy umysłu", + "core.bonus.MAGIC_RESISTANCE.name": "Odporność: Magia(${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "${val}% szans na unik wrogiego czaru", + "core.bonus.MIND_IMMUNITY.name": "Odporność: Czary umysłu", "core.bonus.MIND_IMMUNITY.description": "Odporny na czary typu umysłu", "core.bonus.NO_DISTANCE_PENALTY.name": "Brak ograniczeń za odległość", "core.bonus.NO_DISTANCE_PENALTY.description": "Pełne obrażenia z każdego zasięgu", - "core.bonus.NO_MELEE_PENALTY.name": "Brak ograniczeń za walkę wręcz", + "core.bonus.NO_MELEE_PENALTY.name": "Bez ograniczeń", "core.bonus.NO_MELEE_PENALTY.description": "Stworzenie nie ma kar w walce wręcz", "core.bonus.NO_MORALE.name": "Neutralne Morale", - "core.bonus.NO_MORALE.description": "Stworzenie jest odporne na efekty morale", - "core.bonus.NO_WALL_PENALTY.name": "Brak kar za strzelanie przez przeszkody", + "core.bonus.NO_MORALE.description": "Odporność na efekty morale", + "core.bonus.NO_WALL_PENALTY.name": "Bez przeszkód", "core.bonus.NO_WALL_PENALTY.description": "Pełne obrażenia podczas oblężenia", "core.bonus.NON_LIVING.name": "Nie żyjący", "core.bonus.NON_LIVING.description": "Niewrażliwość na wiele efektów", @@ -306,6 +429,8 @@ "core.bonus.REBIRTH.description": "${val}% stworzeń powstanie po śmierci", "core.bonus.RETURN_AFTER_STRIKE.name": "Atak i Powrót", "core.bonus.RETURN_AFTER_STRIKE.description": "Wraca po ataku wręcz", + "core.bonus.REVENGE.name": "Odwet", + "core.bonus.REVENGE.description": "Zadaje dodatkowe obrażenia zależne od strat własnych oddziału", "core.bonus.SHOOTER.name": "Dystansowy", "core.bonus.SHOOTER.description": "Stworzenie może strzelać", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Ostrzeliwuje wszystko dookoła", @@ -314,20 +439,20 @@ "core.bonus.SOUL_STEAL.description": "Zdobywa ${val} nowych stworzeń za każdego zabitego wroga", "core.bonus.SPELLCASTER.name": "Czarodziej", "core.bonus.SPELLCASTER.description": "Może rzucić ${subtype.spell}", - "core.bonus.SPELL_AFTER_ATTACK.name": "Rzuca czar po ataku", - "core.bonus.SPELL_AFTER_ATTACK.description": "${val}% szans aby rzucić ${subtype.spell} po ataku", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Rzuca czar przed atakiem", - "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% szans aby rzucić ${subtype.spell} przed atakiem", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Odporność na czary", + "core.bonus.SPELL_AFTER_ATTACK.name": "${val}% szans na czar", + "core.bonus.SPELL_AFTER_ATTACK.description": "${subtype.spell} po ataku", + "core.bonus.SPELL_BEFORE_ATTACK.name": "${val}% szans na czar", + "core.bonus.SPELL_BEFORE_ATTACK.description": "${subtype.spell} przed atakiem", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Obrona przed magią", "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Obrażenia od czarów są zmniejszone o ${val}%.", - "core.bonus.SPELL_IMMUNITY.name": "Odporność na czar", - "core.bonus.SPELL_IMMUNITY.description": "Odporny na ${subtype.spell}", + "core.bonus.SPELL_IMMUNITY.name": "Odporność: Zaklęcie", + "core.bonus.SPELL_IMMUNITY.description": "${subtype.spell}", "core.bonus.SPELL_LIKE_ATTACK.name": "Atak czaropodobny", "core.bonus.SPELL_LIKE_ATTACK.description": "Atakuje z użyciem ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura Odporności", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Pobliskie stworzenia otrzymują ${val}% magicznej odporności", - "core.bonus.SUMMON_GUARDIANS.name": "Wezwij strażników", - "core.bonus.SUMMON_GUARDIANS.description": "Na początku walki wzywa ${subtype.creature} (${val}%)", + "core.bonus.SPELL_RESISTANCE_AURA.name": "O ${val}% słabszy", + "core.bonus.SPELL_RESISTANCE_AURA.description": "efekt czarów dla pobl. stwor.", + "core.bonus.SUMMON_GUARDIANS.name": "Wzywa na początku walki", + "core.bonus.SUMMON_GUARDIANS.description": "${subtype.creature} (${val}%)", "core.bonus.SYNERGY_TARGET.name": "Synergiczny", "core.bonus.SYNERGY_TARGET.description": "To stworzenie jest podatne na efekt synergii", "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Zionięcie", @@ -340,7 +465,7 @@ "core.bonus.UNDEAD.description": "Stworzenie jest nieumarłe", "core.bonus.UNLIMITED_RETALIATIONS.name": "Nieskończone kontrataki", "core.bonus.UNLIMITED_RETALIATIONS.description": "Kontratakuje nieskończoną ilość razy", - "core.bonus.WATER_IMMUNITY.name": "Odporność na wodę", + "core.bonus.WATER_IMMUNITY.name": "Odporność: Woda", "core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody", "core.bonus.WIDE_BREATH.name": "Szerokie zionięcie", "core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)" diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON index 0cccafbb0..971a9a9e2 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON @@ -1,9 +1,9 @@ { "Blockbuster M" : - //(ban fly/DD, 2 player, 15-Jun-03, midnight design) { "minSize" : "m", "maxSize" : "m", "players" : "2", + "description" : "Ban fly/DD, 2 player, 15-Jun-03, Midnight design", "zones" : { "1" : @@ -119,6 +119,7 @@ { "minSize" : "l", "maxSize" : "l", "players" : "2", + "description" : "Ban fly/DD, 2 player, 15-Jun-03, Midnight design", "zones" : { "1" : @@ -246,6 +247,7 @@ { "minSize" : "xl", "maxSize" : "xl", "players" : "2", + "description" : "Ban fly/DD, 2 player, 15-Jun-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json b/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json index 539c90dbc..d62bdde01 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json @@ -2,7 +2,7 @@ "Coldshadow's Fantasy": { "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "4-8", "cpu" : "3-6", + "players" : "4-8", "humans" : "3-6", "zones": { "1": diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON index bac5bc7bc..b1aabf8cd 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON @@ -1,9 +1,9 @@ { "Extreme L" : - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) { "minSize" : "l", "maxSize" : "l", "players" : "2", + "description" : "Ban fly/DD/Orb of Inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, Midnight design", "zones" : { "1" : @@ -180,10 +180,10 @@ ] }, "Extreme XL" : - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) { "minSize" : "xl", "maxSize" : "xh", "players" : "2", + "description" : "Ban fly/DD/Orb of Inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON index 62006c85e..eeaff98cd 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON @@ -1,9 +1,9 @@ { "Extreme II L": - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)" { "minSize" : "l", "maxSize" : "l", "players" : "2", + "description": "Ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, Midnight design", "zones" : { "1" : @@ -154,10 +154,10 @@ ] }, "Extreme II XL": - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) { "minSize" : "xl", "maxSize" : "xh", "players" : "2", + "description": "Ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON index cf9e8e970..5f44d593e 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON @@ -3,6 +3,7 @@ { "minSize" : "xl", "maxSize" : "xl", "players" : "2", + "description" : "Ban Fly/DD, 2 player, 31-May-03, Midnight design", "zones" : { "1" : @@ -322,6 +323,7 @@ { "minSize" : "xl+u", "maxSize" : "g+u", "players" : "2", + "description" : "Ban Fly/DD, 2 player, 31-May-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON index 773b68625..72e6369ce 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON @@ -3,6 +3,7 @@ { "minSize" : "m+u", "maxSize" : "l+u", "players" : "2", + "description" : "Ban Fly/DD, 2 player, 3-Aug-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON index 3113a91dd..ef7610aa6 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON @@ -1,9 +1,9 @@ { "Poor Jebus" : - //(made by Bjorn190, modified by Maretti and Angelito) { "minSize" : "m", "maxSize" : "xl+u", "players" : "2-4", + "description" : "Made by Bjorn190, modified by Maretti and Angelito", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON index f41c68cd3..93bf54143 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON @@ -1,9 +1,9 @@ { "Reckless" : - //(2 player, 6-Jan-03, midnight design) { "minSize" : "l", "maxSize" : "xl+u", "players" : "2", + "description" : "2 players, 6-Jan-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON index 063ec0b1f..e4afc15f5 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON @@ -1,9 +1,9 @@ { "Roadrunner" : - //(ban fly/DD, 2 player, 31-May-03, midnight design) { "minSize" : "xl", "maxSize" : "xl", "players" : "2", + "description" : "Ban fly/DD, 2 players, 31-May-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON index 99080a813..97fb6be6d 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON @@ -3,6 +3,7 @@ { "minSize" : "m", "maxSize" : "m", "players" : "2", + "description" : "Ban fly/DD, 2 players, 3-Aug-03, Midnight design", "zones" : { "1" : @@ -119,6 +120,7 @@ { "minSize" : "m+u", "maxSize" : "l", "players" : "2", + "description" : "Ban fly/DD, 2 players, 3-Aug-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON index 0bad27b1e..c24add0e1 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON @@ -1,9 +1,9 @@ { "SuperSlam" : - //(2 player, Large or XL no under) For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules { "minSize" : "l", "maxSize" : "xl", "players" : "2", + "description" : "2 players, Large or XL, no under) For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/triad.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/triad.JSON index e5def0045..5ede0dbc0 100644 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/triad.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/triad.JSON @@ -1,9 +1,9 @@ { "Triad L" : - //(ban fly/DD, 3 players, 9-Jan-03, midnight design) { "minSize" : "l", "maxSize" : "l", "players" : "2-3", + "description" : "Ban fly/DD, 3 players, 9-Jan-03, Midnight design", "zones" : { "1" : @@ -182,6 +182,7 @@ { "minSize" : "xl", "maxSize" : "xl", "players" : "3", + "description" : "Ban fly/DD, 3 players, 9-Jan-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/vortex.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/vortex.JSON index 37a1940ae..0be7894dd 100644 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/vortex.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/vortex.JSON @@ -1,9 +1,9 @@ { "Vortex" : - //(ban fly/DD, 3-4 player, 31-May-03, midnight design) { "minSize" : "xl", "maxSize" : "xl", "players" : "2-4", + "description" : "Ban fly/DD, 3-4 players, 31-May-03, Midnight design", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON index 33af217a1..015cf2168 100644 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON @@ -5117,7 +5117,7 @@ "Midnight Mix 34" : { "minSize" : "l", "maxSize" : "xl+u", - "players" : "6", "cpu" : "2", + "players" : "6", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON index d88b64e4a..189124ae2 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON @@ -2,7 +2,7 @@ "Meeting in Muzgob" : { "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-6", "cpu" : "2", + "players" : "2-6", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON index 337e38523..7593292e7 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON @@ -2,7 +2,7 @@ "Newcomers" : { "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-3", "cpu" : "1", + "players" : "2-3", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON index 053516b20..c93d0fb96 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON @@ -2,7 +2,7 @@ "South of Hell" : { "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", "cpu" : "1", + "players" : "2", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON index e3d640f71..d3f603b66 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON @@ -2,7 +2,7 @@ "Worlds at War" : { "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2", "cpu" : "3", + "players" : "2", "humans" : "3", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON index 399bc52ed..a71dc2ae4 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON @@ -2,7 +2,7 @@ "Gauntlet" : { "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "1", "cpu" : "5", + "players" : "1", "humans" : "5", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON index 764d31a7a..885c1676a 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON @@ -2,7 +2,7 @@ "2SM0k" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "6", + "players" : "2", "humans" : "1-2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON index 6dc559750..18dc90ed9 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON @@ -2,7 +2,8 @@ "2SM2i(2)" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", + "players" : "2", + "humans": "1-2", "zones" : { "1" : @@ -32,7 +33,7 @@ }, "3" : { - "type" : "playerStart", + "type" : "treasure", "size" : 11, "monsters" : "normal", "minesLikeZone" : 1, @@ -54,7 +55,7 @@ }, "5" : { - "type" : "playerStart", + "type" : "treasure", "size" : 11, "monsters" : "normal", "neutralTowns" : { "towns" : 1 }, diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON index e3f7174b3..7411144d4 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON @@ -2,7 +2,7 @@ "3SB0b" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", + "players" : "2", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON index 08909bd9b..fea84faa8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON @@ -2,7 +2,7 @@ "3SB0c" : { "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", + "players" : "2", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON index 905ebb6bd..da29fda5b 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON @@ -2,7 +2,7 @@ "5SB0a" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", + "players" : "2-3", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON index 96197f7c0..6d7d63f5f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON @@ -2,7 +2,7 @@ "5SB0b" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", + "players" : "2-3", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON index 854f2a371..10d05291e 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON @@ -2,7 +2,7 @@ "7SB0b" : { "minSize" : "s", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", + "players" : "2-6", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON index 3729a33eb..266d0ba57 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON @@ -2,7 +2,7 @@ "7SB0c" : { "minSize" : "s+u", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", + "players" : "2-7", "humans" : "2-6", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 24b7d7931..636d36d7c 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -201,13 +201,14 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Виды дорог", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Время игрока}\n\nОбратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Время на ход}\n\nОбратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Время на битву}\n\nОбратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Время на отряд}\n\nОбратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", - "vcmi.optionsTab.widgets.labelTimer" : "Таймер", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Классические часы", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Шахматные часы", + "vcmi.optionsTab.chessFieldBase.hover" : "Время игрока", + "vcmi.optionsTab.chessFieldTurn.hover" : "Время на ход", + "vcmi.optionsTab.chessFieldBattle.hover" : "Время на битву", + "vcmi.optionsTab.chessFieldUnit.hover" : "Время на отряд", + "vcmi.optionsTab.chessFieldBase.help" : "Обратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.", + "vcmi.optionsTab.chessFieldTurn.help" : "Обратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.", + "vcmi.optionsTab.chessFieldBattle.help" : "Обратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.", + "vcmi.optionsTab.chessFieldUnit.help" : "Обратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", "mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов", "mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев", diff --git a/Mods/vcmi/config/vcmi/spanish.json b/Mods/vcmi/config/vcmi/spanish.json index e18986ebc..12291a2cc 100644 --- a/Mods/vcmi/config/vcmi/spanish.json +++ b/Mods/vcmi/config/vcmi/spanish.json @@ -20,6 +20,7 @@ "vcmi.adventureMap.playerAttacked" : "El jugador ha sido atacado: %s", "vcmi.adventureMap.moveCostDetails" : "Puntos de movimiento - Coste: %TURNS turnos + %POINTS puntos, Puntos restantes: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Puntos de movimiento - Coste: %POINTS puntos, Puntos restantes: %REMAINING", + "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Disculpe, la repetición del turno del oponente aún no está implementada.", "vcmi.capitalColors.0" : "Rojo", "vcmi.capitalColors.1" : "Azul", @@ -30,9 +31,57 @@ "vcmi.capitalColors.6" : "Turquesa", "vcmi.capitalColors.7" : "Rosa", - "vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero", - "vcmi.server.errors.modsToEnable" : "{Mods necesarios para cargar el juego}", - "vcmi.server.confirmReconnect" : "¿Conectar a la última sesión?", + "vcmi.heroOverview.startingArmy" : "Unidades iniciales", + "vcmi.heroOverview.warMachine" : "Máquinas de Guerra", + "vcmi.heroOverview.secondarySkills" : "Habilidades secundarias", + "vcmi.heroOverview.spells" : "Hechizos", + + "vcmi.radialWheel.mergeSameUnit" : "Mezclar criaturas idénticas", + "vcmi.radialWheel.fillSingleUnit" : "Rellenar con criaturas individuales", + "vcmi.radialWheel.splitSingleUnit" : "Separar una sola criatura", + "vcmi.radialWheel.splitUnitEqually" : "Dividir criaturas por igual", + "vcmi.radialWheel.moveUnit" : "Trasladar criaturas a otro ejército", + "vcmi.radialWheel.splitUnit" : "Dividir criatura a otra ranura", + + "vcmi.radialWheel.heroGetArmy" : "Obtener ejército de otro héroe", + "vcmi.radialWheel.heroSwapArmy" : "Intercambiar ejército con otro héroe", + "vcmi.radialWheel.heroExchange" : "intercambio entre héroes", + "vcmi.radialWheel.heroGetArtifacts" : "Obtener artefactos de otro héroe", + "vcmi.radialWheel.heroSwapArtifacts" : "Intercambiar artefactos con otro héroe", + "vcmi.radialWheel.heroDismiss" : "Despedir al héroe", + + "vcmi.radialWheel.moveTop" : "Mover arriba", + "vcmi.radialWheel.moveUp" : "Mover hacia arriba", + "vcmi.radialWheel.moveDown" : "Mover hacia abajo", + "vcmi.radialWheel.moveBottom" : "Mover abajo", + + "vcmi.spellBook.search" : "buscar...", + + "vcmi.mainMenu.serverConnecting" : "Conectando...", + "vcmi.mainMenu.serverAddressEnter" : "Ingrese la dirección:", + "vcmi.mainMenu.serverConnectionFailed" : "Falló la conexión", + "vcmi.mainMenu.serverClosing" : "Cerrando...", + "vcmi.mainMenu.hostTCP" : "Crear juego TCP/IP", + "vcmi.mainMenu.joinTCP" : "Unirse a juego TCP/IP", + "vcmi.mainMenu.playerName" : "Jugador", + + "vcmi.lobby.filepath" : "Ruta del archivo", + "vcmi.lobby.creationDate" : "Fecha de creación", + "vcmi.lobby.scenarioName" : "Nombre del escenario", + "vcmi.lobby.mapPreview" : "Vista previa del mapa", + "vcmi.lobby.noPreview" : "sin vista previa", + "vcmi.lobby.noUnderground" : "sin subterráneo", + "vcmi.lobby.sortDate" : "Ordena los mapas por la fecha de modificación", + + "vcmi.client.errors.invalidMap" : "{Mapa o Campaña invalido}\n\n¡No se pudo iniciar el juego! El mapa o la campaña seleccionados pueden no ser válidos o estar dañados. Motivo:\n%s", + "vcmi.client.errors.missingCampaigns" : "{Archivos de datos faltantes}\n\n¡No se encontraron los archivos de datos de las campañas! Quizás estés utilizando archivos de datos incompletos o dañados de Heroes 3. Por favor, reinstala los datos del juego.", + "vcmi.server.errors.existingProcess" : "Otro servidor VCMI está en ejecución. Por favor, termínalo antes de comenzar un nuevo juego.", + "vcmi.server.errors.modsToEnable" : "{Se requieren los siguientes mods}", + "vcmi.server.errors.modsToDisable" : "{Deben desactivarse los siguientes mods}", + "vcmi.server.confirmReconnect" : "¿Quieres reconectar a la última sesión?", + "vcmi.server.errors.modNoDependency" : "Error al cargar el mod {'%s'}.\n Depende del mod {'%s'}, que no está activo.\n", + "vcmi.server.errors.modConflict" : "Error al cargar el mod {'%s'}.\n Conflicto con el mod activo {'%s'}.\n", + "vcmi.server.errors.unknownEntity" : "Error al cargar la partida guardada. ¡Se encontró una entidad desconocida '%s' en la partida guardada! Es posible que la partida no sea compatible con la versión actualmente instalada de los mods.", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Cambiar a la pestaña de opciones generales, que contiene ajustes relacionados con el comportamiento general del juego", @@ -46,12 +95,33 @@ "vcmi.systemOptions.otherGroup" : "Otras configuraciones", // actualmente no utilizada "vcmi.systemOptions.townsGroup" : "Pantalla de la ciudad", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Pantalla completa (sin bordes)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Pantalla completa sin bordes}\n\nSi está seleccionado, VCMI se ejecutará en modo de pantalla completa sin bordes. En este modo, el juego siempre usará la misma resolución que el escritorio, ignorando la resolución seleccionada.", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Pantalla completa (exclusiva)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Pantalla completa}\n\nSi está seleccionado, VCMI se ejecutará en modo de pantalla completa exclusiva. En este modo, el juego cambiará la resolución del monitor a la resolución seleccionada.", "vcmi.systemOptions.resolutionButton.hover" : "Resolución: %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Seleccionar resolución}\n\n Cambia la resolución de la pantalla del juego. Se requiere reiniciar el juego para aplicar la nueva resolución.", - "vcmi.systemOptions.resolutionMenu.hover" : "Seleccionar resolución", - "vcmi.systemOptions.resolutionMenu.help" : "Cambia la resolución de la pantalla del juego.", + "vcmi.systemOptions.resolutionButton.help" : "{Seleccionar Resolución}\n\nCambia la resolución de pantalla del juego.", + "vcmi.systemOptions.resolutionMenu.hover" : "Seleccionar Resolución", + "vcmi.systemOptions.resolutionMenu.help" : "Cambia la resolución de pantalla del juego.", + "vcmi.systemOptions.scalingButton.hover" : "Escalado de interfaz: %p%", + "vcmi.systemOptions.scalingButton.help" : "{Escalado de interfaz}\n\nCambia el escalado de la interfaz del juego.", + "vcmi.systemOptions.scalingMenu.hover" : "Seleccionar Escalado de Interfaz", + "vcmi.systemOptions.scalingMenu.help" : "Cambia el escalado de la interfaz del juego.", + "vcmi.systemOptions.longTouchButton.hover" : "Intervalo de toque largo: %d ms", // Nota de traducción: "ms" = "milisegundos" + "vcmi.systemOptions.longTouchButton.help" : "{Intervalo de toque largo}\n\nCuando se utiliza una pantalla táctil, las ventanas emergentes aparecerán después de tocar la pantalla durante la duración especificada, en milisegundos.", + "vcmi.systemOptions.longTouchMenu.hover" : "Seleccionar Intervalo de Toque Largo", + "vcmi.systemOptions.longTouchMenu.help" : "Cambia la duración del intervalo de toque largo.", + "vcmi.systemOptions.longTouchMenu.entry" : "%d milisegundos", "vcmi.systemOptions.framerateButton.hover" : "Mostrar FPS", - "vcmi.systemOptions.framerateButton.help" : "{Mostrar FPS}\n\n Muestra el contador de Frames Por Segundo en la esquina de la ventana del juego.", + "vcmi.systemOptions.framerateButton.help" : "{Mostrar FPS}\n\nAlternar la visibilidad del contador de Frames Per Second en la esquina de la ventana del juego.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Retroalimentación háptica", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Retroalimentación háptica}\n\nAlternar la retroalimentación háptica en las entradas táctiles.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Mejoras de la interfaz", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Mejoras de la interfaz}\n\nAlternar diversas mejoras de interfaz de calidad de vida. Como un botón de mochila, etc. Desactiva para tener una experiencia más clásica.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Libro de hechizos grande", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Libro de hechizos grande}\n\nPermite un libro de hechizos más grande que permite más hechizos por página. La animación de cambio de página del libro de hechizos no funciona con esta configuración habilitada.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Silenciar con inactividad", + "vcmi.systemOptions.audioMuteFocus.help" : "{Silenciar con inactividad}\n\nSilenciar el audio cuando la ventana no está activa. Las excepciones son los mensajes en el juego y el sonido de turno nuevo.", "vcmi.adventureOptions.infoBarPick.hover" : "Mostrar mensajes en el panel de información", "vcmi.adventureOptions.infoBarPick.help" : "{Mostrar mensajes en el panel de información}\n\nSiempre que sea posible, los mensajes del juego sobre los objetos del mapa que se visiten se mostrarán en el panel de información, en lugar de aparecer en una ventana separada.", @@ -61,12 +131,24 @@ "vcmi.adventureOptions.forceMovementInfo.help": "{Mostrar siempre el coste de movimiento}\n\n Reemplaza la información predeterminada de la barra de estado con datos de puntos de movimiento sin necesidad de mantener presionado el botón ALT.", "vcmi.adventureOptions.showGrid.hover": "Mostrar cuadrícula", "vcmi.adventureOptions.showGrid.help": "{Mostrar cuadrícula}\n\n Muestra una superposición de cuadrícula que muestra las fronteras entre las casillas del mapa de aventuras.", + "vcmi.adventureOptions.borderScroll.hover" : "Desplazamiento de borde", + "vcmi.adventureOptions.borderScroll.help" : "{Desplazamiento de borde}\n\nDesplaza el mapa de aventuras cuando el cursor está adyacente al borde de la ventana. Puede desactivarse manteniendo presionada la tecla CTRL.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Gestión de criaturas en el panel de información", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Gestión de criaturas en el panel de información}\n\nPermite reorganizar criaturas en el panel de información en lugar de cambiar entre los componentes predeterminados.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Arrastrar mapa con clic izquierdo", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Arrastrar mapa con clic izquierdo}\n\nCuando está habilitado, mover el ratón con el botón izquierdo presionado arrastrará la vista del mapa de aventuras.", + "vcmi.adventureOptions.smoothDragging.hover" : "Arrastre suave del mapa", + "vcmi.adventureOptions.smoothDragging.help" : "{Arrastre suave del mapa}\n\nCuando está habilitado, el arrastre del mapa tiene un efecto de deslizamiento moderno.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Omitir efectos de desvanecimiento", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Omitir efectos de desvanecimiento}\n\nCuando está habilitado, omite el desvanecimiento de objetos y otros efectos similares (recolección de recursos, embarque en barco, etc.). Hace que la interfaz sea más reactiva en algunos casos a expensas de la estética. Especialmente útil en juegos JcJ. Para obtener la máxima velocidad de movimiento, la omisión está activa independientemente de esta configuración.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "Establece la velocidad de desplazamiento del mapa como muy lenta", "vcmi.adventureOptions.mapScrollSpeed5.help": "Establece la velocidad de desplazamiento del mapa como muy rápida", "vcmi.adventureOptions.mapScrollSpeed6.help": "Establece la velocidad de desplazamiento del mapa como instantánea.", + "vcmi.adventureOptions.hideBackground.hover" : "Ocultar fondo", + "vcmi.adventureOptions.hideBackground.help" : "{Ocultar fondo}\n\nOculta el mapa de aventuras en el fondo y muestra una textura en su lugar..", "vcmi.battleOptions.queueSizeLabel.hover": "Mostrar orden de turno de criaturas", "vcmi.battleOptions.queueSizeNoneButton.hover": "APAGADO", @@ -85,68 +167,176 @@ "vcmi.battleOptions.animationsSpeed6.help": "Establece la velocidad de animación como instantánea.", "vcmi.battleOptions.movementHighlightOnHover.hover": "Resaltado de movimiento al pasar el ratón", "vcmi.battleOptions.movementHighlightOnHover.help": "{Resaltado de movimiento al pasar el ratón}\n\nResalta el rango de movimiento de la unidad cuando el cursor esta sobre esta.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Mostrar límites de alcance para tiradores", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostrar límites de alcance para tiradores al pasar el ratón}\n\nMuestra los límites de alcance de los tiradores al pasar el ratón sobre ellos.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Mostrar ventanas de estadísticas de héroes", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostrar ventanas de estadísticas de héroes}\n\nAlternar permanentemente las ventanas de estadísticas de héroes que muestran estadísticas primarias y puntos de hechizo.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Omitir música de introducción", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Omitir música de introducción}\n\n Omitir la breve música que se reproduce al comienzo de cada batalla antes de que comience la acción. También se puede omitir presionando la tecla ESC.", - "vcmi.battleWindow.pressKeyToSkipIntro" : "Presiona una tecla para comenzar la batalla inmediatamente.", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Omitir música de introducción}\n\nPermitir acciones durante la música de introducción que se reproduce al comienzo de cada batalla.", + "vcmi.battleOptions.endWithAutocombat.hover": "Finaliza la batalla", + "vcmi.battleOptions.endWithAutocombat.help": "{Finaliza la batalla}\n\nAutomatiza la batalla y la finaliza al instante", - "vcmi.battleWindow.damageEstimation.melee" : "Ataque %CREATURE (%DAMAGE).", - "vcmi.battleWindow.damageEstimation.meleeKills" : "Ataque %CREATURE (%DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.ranged" : "Disparo %CREATURE (%SHOTS, %DAMAGE).", - "vcmi.battleWindow.damageEstimation.rangedKills" : "Disparo %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.adventureMap.revisitObject.hover" : "Revisitar objeto", + "vcmi.adventureMap.revisitObject.help" : "{Revisitar objeto}\n\nSi un héroe se encuentra actualmente en un objeto del mapa, puede volver a visitar la ubicación.", + + "vcmi.battleWindow.pressKeyToSkipIntro" : "Presiona cualquier tecla para empezar la batalla inmediatamente", + "vcmi.battleWindow.damageEstimation.melee" : "Atacar %CREATURE (%DAÑO).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Atacar %CREATURE (%DAÑO, %BAJAS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Disparar a %CREATURE (%DISPAROS, %DAÑO).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Disparar a %CREATURE (%DISPAROS, %DAÑO, %BAJAS).", "vcmi.battleWindow.damageEstimation.shots" : "%d disparos restantes", - "vcmi.battleWindow.damageEstimation.shots.1" : "%d disparos restantes", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d disparo restante", "vcmi.battleWindow.damageEstimation.damage" : "%d daño", "vcmi.battleWindow.damageEstimation.damage.1" : "%d daño", "vcmi.battleWindow.damageEstimation.kills" : "%d perecerán", - "vcmi.battleWindow.damageEstimation.kills.1" : "%d perecerán", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d perecerá", + "vcmi.battleWindow.killed" : "Eliminados", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s han sido eliminados por disparos certeros", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s ha sido eliminado por un disparo certero", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s han sido eliminados por disparos certeros", + "vcmi.battleWindow.endWithAutocombat" : "¿Quieres finalizar la batalla con combate automatizado?", "vcmi.battleResultsWindow.applyResultsLabel" : "Aplicar resultado de la batalla", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Mostrar criaturas disponibles", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Mostrar criaturas disponibles}\n\n Muestra las criaturas disponibles para comprar en lugar de su crecimiento en el resumen de la ciudad (esquina inferior izquierda).", + "vcmi.tutorialWindow.title" : "Introducción a la pantalla táctil", + "vcmi.tutorialWindow.decription.RightClick" : "Toca y mantén presionado el elemento en el que quieres hacer clic derecho. Toca el área libre para cerrar.", + "vcmi.tutorialWindow.decription.MapPanning" : "Toca y arrastra con un dedo para mover el mapa.", + "vcmi.tutorialWindow.decription.MapZooming" : "Hace zoom con dos dedos para cambiar el zoom del mapa.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Deslizar abre la rueda radial para diversas acciones, como la gestión de criaturas/héroes y el pedido de ciudad.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Para atacar desde una dirección específica, desliza en la dirección desde la cual se va a realizar el ataque.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "El gesto de dirección de ataque se puede cancelar si el dedo está lo suficientemente alejado.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Toca y mantén presionado para cancelar un hechizo.", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Mostrar criaturas disponibles", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Mostrar criaturas disponibles}\n\nMuestra la cantidad de criaturas disponibles para comprar en lugar de su crecimiento en el resumen de la ciudad (esquina inferior izquierda de la pantalla de la ciudad).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Mostrar crecimiento semanal de criaturas", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Mostrar crecimiento semanal de criaturas}\n\n Muestra el crecimiento semanal de las criaturas en lugar de la cantidad disponible en el resumen de la ciudad (esquina inferior izquierda).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Mostrar crecimiento semanal de criaturas}\n\nMuestra el crecimiento semanal de las criaturas en lugar de la cantidad disponible en el resumen de la ciudad (esquina inferior izquierda de la pantalla de la ciudad).", "vcmi.otherOptions.compactTownCreatureInfo.hover": "Información compacta de criaturas de la ciudad", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Información compacta de criaturas de la ciudad}\n\n Información más pequeña de las criaturas de la ciudad en el resumen de la ciudad.", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Información compacta de criaturas de la ciudad}\n\nMuestra información más pequeña para las criaturas de la ciudad en el resumen de la ciudad (esquina inferior izquierda de la pantalla de la ciudad).", - "vcmi.townHall.missingBase": "El edificio base %s debe ser construido primero", - "vcmi.townHall.noCreaturesToRecruit": "¡No hay criaturas para reclutar!", - "vcmi.townHall.greetingManaVortex": "A medida que te acercas a %s, tu cuerpo se llena de nueva energía. Has duplicado tus puntos de magia normales.", - "vcmi.townHall.greetingKnowledge": "Estudias los glifos en %s y obtienes una visión de cómo funcionan varias magias (+1 Conocimiento).", - "vcmi.townHall.greetingSpellPower": "%s te enseña nuevas formas de enfocar tus poderes mágicos (+1 Poder).", - "vcmi.townHall.greetingExperience": "Una visita a %s te enseña muchas habilidades nuevas (+1000 experiencia).", - "vcmi.townHall.greetingAttack": "El tiempo pasado en %s te permite aprender habilidades de combate más efectivas (+1 habilidad de Ataque).", - "vcmi.townHall.greetingDefence": "Pasando tiempo en %s, los guerreros experimentados allí te enseñan habilidades defensivas adicionales (+1 Defensa).", - "vcmi.townHall.hasNotProduced": "%s no ha producido nada todavía.", - "vcmi.townHall.hasProduced": "%s produjo %d %s esta semana.", - "vcmi.townHall.greetingCustomBonus": "%s te da +%d %s%s", - "vcmi.townHall.greetingCustomUntil": " hasta la próxima batalla.", - "vcmi.townHall.greetingInTownMagicWell": "%s ha restaurado tus puntos de magia al máximo.", + "vcmi.townHall.missingBase" : "Primero se debe construir el edificio base %s", + "vcmi.townHall.noCreaturesToRecruit" : "¡No hay criaturas para reclutar!", + "vcmi.townHall.greetingManaVortex" : "Al acercarte a %s, tu cuerpo se llena de nueva energía. Has duplicado tus puntos de hechizo normales.", + "vcmi.townHall.greetingKnowledge" : "Estudias los glifos en %s y obtienes una visión de los entresijos de varias magias (+1 conocimiento).", + "vcmi.townHall.greetingSpellPower" : "El %s te enseña nuevas formas de enfocar tus poderes mágicos (+1 Poder).", + "vcmi.townHall.greetingExperience" : "Una visita a %s te enseña muchas habilidades nuevas (+1000 Experiencia).", + "vcmi.townHall.greetingAttack" : "El tiempo dedicado en %s te permite aprender habilidades de combate más efectivas (+1 habilidad de ataque).", + "vcmi.townHall.greetingDefence" : "Pasando tiempo en %s, los guerreros experimentados allí te enseñan habilidades defensivas adicionales (+1 Defensa).", + "vcmi.townHall.hasNotProduced" : "%s aún no ha producido nada.", + "vcmi.townHall.hasProduced" : "%s ha producido %d %s esta semana.", + "vcmi.townHall.greetingCustomBonus" : "%s te da +%d %s%s", + "vcmi.townHall.greetingCustomUntil" : " hasta la próxima batalla.", + "vcmi.townHall.greetingInTownMagicWell" : "%s ha restaurado tus puntos de hechizo al máximo.", - "vcmi.logicalExpressions.anyOf" : "Cualquiera de los siguientes:", - "vcmi.logicalExpressions.allOf" : "Todos los siguientes:", - "vcmi.logicalExpressions.noneOf" : "Ninguno de los siguientes:", + "vcmi.logicalExpressions.anyOf" : "Cualquiera de lo siguiente:", + "vcmi.logicalExpressions.allOf" : "Todo lo siguiente:", + "vcmi.logicalExpressions.noneOf" : "Ninguno de lo siguiente:", - "vcmi.heroWindow.openCommander.hover" : "Abrir ventana de comandante", - "vcmi.heroWindow.openCommander.help" : "Muestra información sobre el comandante de este héroe", + "vcmi.heroWindow.openCommander.hover" : "Abrir ventana de información del comandante", + "vcmi.heroWindow.openCommander.help" : "Muestra detalles sobre el comandante de este héroe.", + "vcmi.heroWindow.openBackpack.hover" : "Abrir ventana de mochila de artefactos", + "vcmi.heroWindow.openBackpack.help" : "Abre la ventana que facilita la gestión de la mochila de artefactos.", + + "vcmi.tavernWindow.inviteHero" : "Invitar heroe", "vcmi.commanderWindow.artifactMessage" : "¿Quieres devolver este artefacto al héroe?", "vcmi.creatureWindow.showBonuses.hover" : "Cambiar a vista de bonificaciones", - "vcmi.creatureWindow.showBonuses.help" : "Muestra todas las bonificaciones activas del comandante", + "vcmi.creatureWindow.showBonuses.help" : "Mostrar todas las bonificaciones activas del comandante.", "vcmi.creatureWindow.showSkills.hover" : "Cambiar a vista de habilidades", - "vcmi.creatureWindow.showSkills.help" : "Muestra todas las habilidades aprendidas del comandante", + "vcmi.creatureWindow.showSkills.help" : "Mostrar todas las habilidades aprendidas del comandante.", "vcmi.creatureWindow.returnArtifact.hover" : "Devolver artefacto", - "vcmi.creatureWindow.returnArtifact.help" : "Usa este botón para devolver un artefacto del almacen al inventario del héroe", + "vcmi.creatureWindow.returnArtifact.help" : "Haz clic en este botón para devolver el artefacto a la mochila del héroe.", - "vcmi.questLog.hideComplete.hover" : "Ocultar misiones completas", - "vcmi.questLog.hideComplete.help" : "Ocultar todas las misiones que ya han sido completadas", + "vcmi.questLog.hideComplete.hover" : "Ocultar misiones completadas", + "vcmi.questLog.hideComplete.help" : "Ocultar todas las misiones completadas.", + "vcmi.randomMapTab.widgets.randomTemplate" : "(Aleatorio)", "vcmi.randomMapTab.widgets.templateLabel" : "Plantilla", - "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configurar...", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuración...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alineaciones de equipos", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Tipos de caminos", + "vcmi.optionsTab.turnOptions.hover" : "Opciones de turno", + "vcmi.optionsTab.turnOptions.help" : "Seleccionar temporizador de turno y opciones de turnos simultáneos", + "vcmi.optionsTab.selectPreset" : "Preconfigurado", + + "vcmi.optionsTab.chessFieldBase.hover" : "Cronómetro base", + "vcmi.optionsTab.chessFieldTurn.hover" : "Cronómetro de turno", + "vcmi.optionsTab.chessFieldBattle.hover" : "Cronómetro de batalla", + "vcmi.optionsTab.chessFieldUnit.hover" : "Cronómetro de unidad", + "vcmi.optionsTab.chessFieldBase.help" : "Se utiliza cuando el {Cronómetro de Turno} alcanza 0. Se establece una vez al inicio del juego. Al alcanzar cero, termina el turno actual. Cualquier combate en curso terminará con una derrota.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Se utiliza fuera de combate o cuando el {Cronómetro de Batalla} se agota. Se restablece cada turno. El tiempo restante se suma al {Temporizador Base} al final del turno.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Se utiliza fuera de combate o cuando el {Cronómetro de Batalla} se agota. Se restablece cada turno. Se pierde cualquier tiempo no utilizado.", + "vcmi.optionsTab.chessFieldBattle.help" : "Se utiliza en batallas con IA o en combates JcJ cuando el {Cronómetro de Unidad} se agota. Se restablece al inicio de cada combate.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Se utiliza al seleccionar la acción de una unidad en combate JcJ. El tiempo restante se suma al {Temporizador de Batalla} al final del turno de la unidad.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Se utiliza al seleccionar la acción de una unidad en combate JcJ. Se restablece al inicio del turno de cada unidad. Se pierde cualquier tiempo no utilizado.", + + "vcmi.optionsTab.accumulate" : "Acumular", + + "vcmi.optionsTab.simturnsTitle" : "Turnos simultáneos", + "vcmi.optionsTab.simturnsMin.hover" : "Al menos por", + "vcmi.optionsTab.simturnsMax.hover" : "Como máximo por", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Turnos simultáneos de la IA", + "vcmi.optionsTab.simturnsMin.help" : "Jugar simultáneamente durante el número especificado de días. Los contactos entre jugadores durante este período están bloqueados.", + "vcmi.optionsTab.simturnsMax.help" : "Jugar simultáneamente durante el número especificado de días o hasta el contacto con otro jugador.", + "vcmi.optionsTab.simturnsAI.help" : "{Turnos Simultáneos de IA}\nOpción experimental. Permite que los jugadores controlados por la IA actúen al mismo tiempo que el jugador humano cuando los turnos simultáneos están habilitados.", + + "vcmi.optionsTab.turnTime.select" : "Configuración del Cronómetro de turno", + "vcmi.optionsTab.turnTime.unlimited" : "Tiempo de turno ilimitado", + "vcmi.optionsTab.turnTime.classic.1" : "Cronómetro clásico: 1 minuto", + "vcmi.optionsTab.turnTime.classic.2" : "Cronómetro clásico: 2 minutos", + "vcmi.optionsTab.turnTime.classic.5" : "Cronómetro clásico: 5 minutos", + "vcmi.optionsTab.turnTime.classic.10" : "Cronómetro clásico: 10 minutos", + "vcmi.optionsTab.turnTime.classic.20" : "Cronómetro clásico: 20 minutos", + "vcmi.optionsTab.turnTime.classic.30" : "Cronómetro clásico: 30 minutos", + "vcmi.optionsTab.turnTime.chess.20" : "Ajedrez: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Ajedrez: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Ajedrez: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Ajedrez: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Ajedrez: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Ajedrez: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Configuración de turnos simultáneos", + "vcmi.optionsTab.simturns.none" : "Sin turnos simultáneos", + "vcmi.optionsTab.simturns.tillContactMax" : "Turnos simultáneos: Hasta contactar", + "vcmi.optionsTab.simturns.tillContact1" : "Turnos simultáneos: 1 semana, interrupción al contactar", + "vcmi.optionsTab.simturns.tillContact2" : "Turnos simultáneos: 2 semanas, interrupción al contactar", + "vcmi.optionsTab.simturns.tillContact4" : "Turnos simultáneos: 1 mes, interrupción al contactar", + "vcmi.optionsTab.simturns.blocked1" : "Turnos simultáneos: 1 semana, contactos bloqueados", + "vcmi.optionsTab.simturns.blocked2" : "Turnos simultáneos: 2 semanas, contactos bloqueados", + "vcmi.optionsTab.simturns.blocked4" : "Turnos simultáneos: 1 mes, contactos bloqueados", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d días", + "vcmi.optionsTab.simturns.days.1" : " %d día", + "vcmi.optionsTab.simturns.days.2" : " %d días", + "vcmi.optionsTab.simturns.weeks.0" : " %d semanas", + "vcmi.optionsTab.simturns.weeks.1" : " %d semana", + "vcmi.optionsTab.simturns.weeks.2" : " %d semanas", + "vcmi.optionsTab.simturns.months.0" : " %d meses", + "vcmi.optionsTab.simturns.months.1" : " %d mes", + "vcmi.optionsTab.simturns.months.2" : " %d meses", + + "vcmi.optionsTab.extraOptions.hover" : "Opciones extra", + "vcmi.optionsTab.extraOptions.help" : "Opciones adicionales para el juego", + + "vcmi.optionsTab.cheatAllowed.hover" : "Permitir trampas", + "vcmi.optionsTab.unlimitedReplay.hover" : "Repetición de batalla ilimitada", + "vcmi.optionsTab.cheatAllowed.help" : "{Permitir trampas}\nPermite la introducción de trucos durante el juego.", + "vcmi.optionsTab.unlimitedReplay.help" : "{Repetición de batalla ilimitada}\nSin límite de repetición de batallas.", + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "El enemigo ha logrado sobrevivir hasta hoy. ¡La victoria es suya!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "¡Felicidades! Has logrado sobrevivir. ¡La victoria es tuya!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "El enemigo ha derrotado a todas las criaturas que asolaban esta tierra y reclama la victoria.", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "¡Felicidades! Has derrotado a todas las criaturas que asolaban esta tierra y puedes reclamar la victoria.", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Adquirir tres artefactos", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "¡Felicidades! Todos tus enemigos han sido derrotados y tienes la Alianza Angelical. ¡La victoria es tuya!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Derrota a todos los enemigos y crea la Alianza Angelical", + "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Por desgracia, has perdido parte de la Alianza Angélica. Todo se ha perdido.", + // few strings from WoG used by vcmi "vcmi.stackExperience.description" : "» D e t a l l e s d e E x p e r i e n c i a d e l G r u p o «\n\nTipo de Criatura ................ : %s\nRango de Experiencia ............ : %s (%i)\nPuntos de Experiencia ............ : %i\nPuntos de Experiencia para el\nSiguiente Rango ............... : %i\nExperiencia Máxima por Batalla .. : %i%% (%i)\nNúmero de Criaturas en el grupo .. : %i\nMáximo de Nuevos Reclutas sin\nPerder el Rango Actual ......... : %i\nMultiplicador de Experiencia .... : %.2f\nMultiplicador de Actualización .. : %.2f\nExperiencia después del Rango 10 : %i\nMáximo de Nuevos Reclutas para\nMantener el Rango 10 si\nEstá en la Experiencia Máxima : %i", "vcmi.stackExperience.rank.0" : "Básico", @@ -199,6 +389,8 @@ "core.bonus.ENCHANTER.description": "Puede lanzar ${subtype.spell} masivo cada turno", "core.bonus.ENCHANTED.name": "Encantado", "core.bonus.ENCHANTED.description": "Afectado por el hechizo permanente ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignorar ataque (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Al ser atacado, ${val}% del daño del atacante es ignorado", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignorar Defensa (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignora una parte de la defensa al atacar", "core.bonus.FIRE_IMMUNITY.name": "Inmunidad al Fuego", @@ -211,6 +403,8 @@ "core.bonus.FEAR.description": "Causa miedo a un grupo enemigo", "core.bonus.FEARLESS.name": "Inmune al miedo", "core.bonus.FEARLESS.description": "Inmune a la habilidad de miedo", + "core.bonus.FEROCITY.name": "Ferocidad", + "core.bonus.FEROCITY.description": "Ataca ${val} veces adicionales en caso de eliminar a alguien", "core.bonus.FLYING.name": "Volar", "core.bonus.FLYING.description": "Puede volar (ignora obstáculos)", "core.bonus.FREE_SHOOTING.name": "Disparo cercano", @@ -241,8 +435,8 @@ "core.bonus.MANA_DRAIN.description": "Drena ${val} de maná cada turno", "core.bonus.MAGIC_MIRROR.name": "Espejo mágico (${val}%)", "core.bonus.MAGIC_MIRROR.description": "Tiene una probabilidad del ${val}% de redirigir un hechizo ofensivo al enemigo", - "core.bonus.MAGIC_RESISTANCE.name": "Resistencia mágica (${MR}%)", - "core.bonus.MAGIC_RESISTANCE.description": "Tiene una probabilidad del ${MR}% de resistir el hechizo del enemigo", + "core.bonus.MAGIC_RESISTANCE.name": "Resistencia mágica (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "Tiene una probabilidad del ${val}% de resistir el hechizo del enemigo", "core.bonus.MIND_IMMUNITY.name": "Inmunidad a hechizos mentales", "core.bonus.MIND_IMMUNITY.description": "Inmune a hechizos de tipo mental", "core.bonus.NO_DISTANCE_PENALTY.name": "Sin penalización por distancia", @@ -265,10 +459,12 @@ "core.bonus.REBIRTH.description": "El ${val}% del grupo resucitará después de la muerte", "core.bonus.RETURN_AFTER_STRIKE.name": "Atacar y volver", "core.bonus.RETURN_AFTER_STRIKE.description": "Regresa después de un ataque cuerpo a cuerpo", + "core.bonus.REVENGE.name": "Venganza", + "core.bonus.REVENGE.description": "Inflige daño adicional según la salud perdida del atacante en la batalla.", "core.bonus.SHOOTER.name": "A distancia", "core.bonus.SHOOTER.description": "La criatura puede disparar", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Dispara en todas direcciones", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Los ataques a distancia de esta criatura impactan a todos los objetivos en un área pequeña", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Los ataques a distancia de esta criatura impactan a todos los objetivos en un área reducida", "core.bonus.SOUL_STEAL.name": "Roba almas", "core.bonus.SOUL_STEAL.description": "Gana ${val} nuevas criaturas por cada enemigo eliminado", "core.bonus.SPELLCASTER.name": "Lanzador de hechizos", @@ -297,8 +493,8 @@ "core.bonus.TRANSMUTATION.description": "${val}% de probabilidad de transformar la unidad atacada en otro tipo", "core.bonus.UNDEAD.name": "No muerto", "core.bonus.UNDEAD.description": "La criatura es un no muerto", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Retaliaciones ilimitadas", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Puede realizar un número ilimitado de ataques de represalia", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Contraataques ilimitados", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Puede realizar un número ilimitado de contraataques", "core.bonus.WATER_IMMUNITY.name": "Inmunidad al agua", "core.bonus.WATER_IMMUNITY.description": "Inmune a todos los hechizos de la escuela del agua", "core.bonus.WIDE_BREATH.name": "Aliento amplio", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 9773fc7af..50f171df1 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -30,6 +30,11 @@ "vcmi.capitalColors.6" : "Сизий", "vcmi.capitalColors.7" : "Рожевий", + "vcmi.heroOverview.startingArmy" : "Початкові загони", + "vcmi.heroOverview.warMachine" : "Бойові машини", + "vcmi.heroOverview.secondarySkills" : "Навички", + "vcmi.heroOverview.spells" : "Закляття", + "vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот", "vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами", "vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту", @@ -37,8 +42,23 @@ "vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії", "vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот", + "vcmi.radialWheel.heroGetArmy" : "Отримати армію іншого героя", + "vcmi.radialWheel.heroSwapArmy" : "Обміняти армії героїв", + "vcmi.radialWheel.heroExchange" : "Відкрити вікно обміну", + "vcmi.radialWheel.heroGetArtifacts" : "Отримати артефакти іншого героя", + "vcmi.radialWheel.heroSwapArtifacts" : "Обміняти артефакти героїв", + "vcmi.radialWheel.heroDismiss" : "Звільнити цього героя", + + "vcmi.radialWheel.moveTop" : "Перемістити на початок", + "vcmi.radialWheel.moveUp" : "Перемістити вгору", + "vcmi.radialWheel.moveDown" : "Перемістити вниз", + "vcmi.radialWheel.moveBottom" : "Перемістити у кінець", + + "vcmi.spellBook.search" : "шукати...", + "vcmi.mainMenu.serverConnecting" : "Підключення...", "vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:", + "vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання", "vcmi.mainMenu.serverClosing" : "Завершення...", "vcmi.mainMenu.hostTCP" : "Створити TCP/IP гру", "vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри", @@ -46,10 +66,20 @@ "vcmi.lobby.filepath" : "Назва файлу", "vcmi.lobby.creationDate" : "Дата створення", + "vcmi.lobby.scenarioName" : "Scenario name", + "vcmi.lobby.mapPreview" : "Огляд мапи", + "vcmi.lobby.noPreview" : "огляд недоступний", + "vcmi.lobby.noUnderground" : "немає підземелля", + "vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни", + "vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.", "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", + "vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}", "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", + "vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n", + "vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", @@ -84,6 +114,12 @@ "vcmi.systemOptions.framerateButton.help" : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильний відгук", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Розширення інтерфейсу", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Розширення інтерфейсу}\n\nУвімкніть різні розширення інтерфейсу для покращення якості життя. Наприклад, більша книга заклинань, рюкзак тощо. Вимкнути, щоб отримати більш класичний досвід.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Велика книга заклять", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Велика книга заклять}\n\nВмикає більшу книгу заклять, яка вміщує більше заклять на сторінці. Якщо цей параметр увімкнено, анімація зміни сторінок книги заклять не буде відображатися.", + "vcmi.systemOptions.audioMuteFocus.hover" : "Тиша при втраті фокусу", + "vcmi.systemOptions.audioMuteFocus.help" : "{Тиша при втраті фокусу}\n\nВимкнути звук коли вікно не у фокусі. Виняток становлять ігрові сповіщення та звук нового ходу.", "vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна", "vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу", @@ -99,12 +135,18 @@ "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Керування істотами у вікні статусу}\n\nДозволяє впорядковувати істот у вікні статусу замість циклічного перемикання між типовими компонентами", "vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи лівою кнопкою", "vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи лівою кнопкою}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод", + "vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи", + "vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Вимкнути ефекти зникнення", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Вимкнути ефекти зникнення}\n\nЯкщо увімкнено, пропускає зникання об'єктів та подібні ефекти (збирання ресурсів, посадка на корабель тощо). У деяких випадках робить інтерфейс більш реактивним за рахунок естетики. Особливо корисно в PvP-іграх. При максимальній швидкості пересування цей параметр увімкнено завжди, незалежно від цього параметра.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "Встановити швидкість розгортання мапи - дуже повільно", "vcmi.adventureOptions.mapScrollSpeed5.help": "Встановити швидкість розгортання мапи - дуже швидко", "vcmi.adventureOptions.mapScrollSpeed6.help": "Встановити швидкість розгортання мапи - миттєво", + "vcmi.adventureOptions.hideBackground.hover" : "Приховувати тло", + "vcmi.adventureOptions.hideBackground.help" : "{Приховувати тло}\n\nПриховати мапу пригод на задньому тлі і показати замість неї текстуру.", "vcmi.battleOptions.queueSizeLabel.hover": "Вигляд черги ходу істот", "vcmi.battleOptions.queueSizeNoneButton.hover": "ВИМК", @@ -130,6 +172,9 @@ "vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускати вступну музику", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.", + "vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт", + "vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Натисніть будь-яку клавішу, щоб розпочати бій", "vcmi.battleWindow.damageEstimation.melee" : "Атакувати %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Атакувати %CREATURE (%DAMAGE, %KILLS).", @@ -141,9 +186,22 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d одиниця пошкодження", "vcmi.battleWindow.damageEstimation.kills" : "%d загинуть", "vcmi.battleWindow.damageEstimation.kills.1" : "%d загине", + "vcmi.battleWindow.killed" : "Загинуло", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s було вбито влучними пострілами!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s було вбито влучним пострілом!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s було вбито влучними пострілами!", "vcmi.battleResultsWindow.applyResultsLabel" : "Прийняти результат бою", + "vcmi.tutorialWindow.title" : "Використання Сенсорного Екрану", + "vcmi.tutorialWindow.decription.RightClick" : "Торкніться і утримуйте елемент, на якому ви хочете натиснути правою кнопкою миші. Торкніться вільної області, щоб закрити.", + "vcmi.tutorialWindow.decription.MapPanning" : "Торкніться і перетягніть одним пальцем, щоб перемістити мапу.", + "vcmi.tutorialWindow.decription.MapZooming" : "Торкніться двома пальцями, щоб змінити масштаб мапи.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Проводячи пальцем, ви відкриваєте радіальне колесо для різних дій, таких як управління істотами/героями та порядком міст/героїв.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Для того, щоб атакувати з певного напрямку, проведіть пальцем у напрямку, звідки буде здійснено атаку.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Атаку можна скасувати, відвівши палець достатньо далеко.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Щоб скасувати заклинання, торкніться і утримуйте палець.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показувати доступних істот", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показувати доступних істот}\n\n Показує істот, яких можна придбати, замість їхнього приросту у зведенні по місту (нижній лівий кут).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Показувати приріст істот", @@ -164,7 +222,7 @@ "vcmi.townHall.greetingCustomBonus" : "%s дає вам +%d %s%s", "vcmi.townHall.greetingCustomUntil" : " до наступної битви.", "vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.", - + "vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:", "vcmi.logicalExpressions.allOf" : "Все з перерахованого:", "vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:", @@ -192,6 +250,67 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Розподіл команд", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Види доріг", + "vcmi.optionsTab.turnOptions.hover" : "Параметри ходів", + "vcmi.optionsTab.turnOptions.help" : "Виберіть опції таймера ходів та одночасних ходів", + + "vcmi.optionsTab.chessFieldBase.hover" : "Основний таймер", + "vcmi.optionsTab.chessFieldTurn.hover" : "Таймер ходу", + "vcmi.optionsTab.chessFieldBattle.hover" : "Таймер битви", + "vcmi.optionsTab.chessFieldUnit.hover" : "Таймер загону", + "vcmi.optionsTab.chessFieldBase.help" : "Встановлюється один раз на початку гри. Коли вичерпується, поточний хід буде перервано, поточна битва буде програна.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Використовується під час ходу. Встановлюється на початку ходу. Залишок додається до {основного таймеру}", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Використовується під час ходу. Встановлюється на початку ходу. Залишок часу буде втрачено", + "vcmi.optionsTab.chessFieldBattle.help" : "Використовується у боях з ШІ чи у боях з гравцями якщо {таймер загону} вичерпується. Встановлюється на початку кожного бою.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок додається до {таймеру битви}", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок часу буде втрачено.", + + "vcmi.optionsTab.accumulate" : "Накопичувати", + + "vcmi.optionsTab.simturnsTitle" : "Одночасні ходи", + "vcmi.optionsTab.simturnsMin.hover" : "Щонайменше", + "vcmi.optionsTab.simturnsMax.hover" : "Щонайбільше", + "vcmi.optionsTab.simturnsAI.hover" : "(Експериментально) Одночасні ходи ШІ", + "vcmi.optionsTab.simturnsMin.help" : "Грати одночасно обрану кількість днів. Контакти між гравцями у цей період заблоковані", + "vcmi.optionsTab.simturnsMax.help" : "Грати одночасно обрану кількість днів чи до першого контакту з іншим гравцем", + "vcmi.optionsTab.simturnsAI.help" : "{Одночасні ходи ШІ}\nЕкспериментальна опція. Дозволяє гравцям-ШІ діяти одночасно с гравцями-людьми якщо одночасні ходи увімкнені.", + + "vcmi.optionsTab.turnTime.select" : "Типові налаштування таймерів", + "vcmi.optionsTab.turnTime.unlimited" : "Необмежений час ходу", + "vcmi.optionsTab.turnTime.classic.1" : "Класичний таймер: 1 хвилина", + "vcmi.optionsTab.turnTime.classic.2" : "Класичний таймер: 2 хвилини", + "vcmi.optionsTab.turnTime.classic.5" : "Класичний таймер: 5 хвилин", + "vcmi.optionsTab.turnTime.classic.10" : "Класичний таймер: 10 хвилин", + "vcmi.optionsTab.turnTime.classic.20" : "Класичний таймер: 20 хвилин", + "vcmi.optionsTab.turnTime.classic.30" : "Класичний таймер: 30 хвилин", + "vcmi.optionsTab.turnTime.chess.20" : "Шахи: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Шахи: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Шахи: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Шахи: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Шахи: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Шахи: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Типові налаштування одночасних ходів", + "vcmi.optionsTab.simturns.none" : "Без одночасних ходів", + "vcmi.optionsTab.simturns.tillContactMax" : "Одночасно: До контакту", + "vcmi.optionsTab.simturns.tillContact1" : "Одночасно: 1 тиждень, до контакту", + "vcmi.optionsTab.simturns.tillContact2" : "Одночасно: 2 тижні, до контакту", + "vcmi.optionsTab.simturns.tillContact4" : "Одночасно: 1 місяць, до контакту", + "vcmi.optionsTab.simturns.blocked1" : "Одночасно: 1 тиждень, без контактів", + "vcmi.optionsTab.simturns.blocked2" : "Одночасно: 2 тижні, без контактів", + "vcmi.optionsTab.simturns.blocked4" : "Одночасно: 1 місяць, без контактів", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d днів", + "vcmi.optionsTab.simturns.days.1" : " %d день", + "vcmi.optionsTab.simturns.days.2" : " %d дні", + "vcmi.optionsTab.simturns.weeks.0" : " %d тижнів", + "vcmi.optionsTab.simturns.weeks.1" : " %d тиждень", + "vcmi.optionsTab.simturns.weeks.2" : " %d тижні", + "vcmi.optionsTab.simturns.months.0" : " %d місяців", + "vcmi.optionsTab.simturns.months.1" : " %d місяць", + "vcmi.optionsTab.simturns.months.2" : " %d місяці", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!", diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/config/vcmi/vietnamese.json index 890e699d1..5939c1db0 100644 --- a/Mods/vcmi/config/vcmi/vietnamese.json +++ b/Mods/vcmi/config/vcmi/vietnamese.json @@ -198,13 +198,14 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội", "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá", - "vcmi.optionsTab.widgets.chessFieldBase.help": "{Thời gian thêm}\n\nBắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", - "vcmi.optionsTab.widgets.chessFieldTurn.help": "{Thời gian lượt}\n\nBắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", - "vcmi.optionsTab.widgets.chessFieldBattle.help": "{Thời gian trận đánh}\n\nĐếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", - "vcmi.optionsTab.widgets.chessFieldCreature.help": "{Thời gian lính}\n\nBắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", - "vcmi.optionsTab.widgets.labelTimer": "Đồng hồ", - "vcmi.optionsTab.widgets.timerModeSwitch.classic": "Đồng hồ cơ bản", - "vcmi.optionsTab.widgets.timerModeSwitch.chess": "Đồng hồ đánh cờ", + "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm", + "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt", + "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đánh", + "vcmi.optionsTab.chessFieldUnit.hover" : "Thời gian lính", + "vcmi.optionsTab.chessFieldBase.help": "Bắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", + "vcmi.optionsTab.chessFieldTurn.help": "Bắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", + "vcmi.optionsTab.chessFieldBattle.help": "Đếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", + "vcmi.optionsTab.chessFieldUnit.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!", "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!", diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index e1eeccc8a..7d57c29d3 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -98,7 +98,7 @@ ] }, - "version" : "1.3", + "version" : "1.4", "author" : "VCMI Team", "contact" : "http://forum.vcmi.eu/index.php", "modType" : "Graphical", @@ -214,6 +214,10 @@ "SOUNDS/": [ {"type" : "dir", "path" : "/Sounds"} + ], + "VIDEO/": + [ + {"type" : "dir", "path" : "/Video"} ] } } diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 3b7d295ea..5aa13e829 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 33 - versionCode 1400 - versionName "1.4.0" + versionCode 1500 + versionName "1.5.0" setProperty("archivesBaseName", "vcmi") } diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index 756fd510f..50a821563 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -36,12 +36,6 @@ public class NativeMethods public static native void initClassloader(); - public static native void createServer(); - - public static native void notifyServerReady(); - - public static native void notifyServerClosed(); - public static native boolean tryToSaveTheGame(); public static void setupMsg(final Messenger msg) @@ -77,62 +71,6 @@ public class NativeMethods return ctx.getApplicationInfo().nativeLibraryDir; } - @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) - public static void startServer() - { - Log.i("Got server create request"); - final Context ctx = SDL.getContext(); - - if (!(ctx instanceof VcmiSDLActivity)) - { - Log.e("Unexpected context... " + ctx); - return; - } - - Intent intent = new Intent(ctx, SDLActivity.class); - intent.setAction(VcmiSDLActivity.NATIVE_ACTION_CREATE_SERVER); - // I probably do something incorrectly, but sending new intent to the activity "normally" breaks SDL events handling (probably detaches jnienv?) - // so instead let's call onNewIntent directly, as out context SHOULD be SDLActivity anyway - ((VcmiSDLActivity) ctx).hackCallNewIntentDirectly(intent); -// ctx.startActivity(intent); - } - - @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) - public static void killServer() - { - Log.i("Got server close request"); - - final Context ctx = SDL.getContext(); - ctx.stopService(new Intent(ctx, ServerService.class)); - - Messenger messenger = requireServerMessenger(); - try - { - // we need to actually inform client about killing the server, beacuse it needs to unbind service connection before server gets destroyed - messenger.send(Message.obtain(null, VcmiSDLActivity.SERVER_MESSAGE_SERVER_KILLED)); - } - catch (RemoteException e) - { - Log.w("Connection with client process broken?"); - } - } - - @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) - public static void onServerReady() - { - Log.i("Got server ready msg"); - Messenger messenger = requireServerMessenger(); - - try - { - messenger.send(Message.obtain(null, VcmiSDLActivity.SERVER_MESSAGE_SERVER_READY)); - } - catch (RemoteException e) - { - Log.w("Connection with client process broken?"); - } - } - @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) public static void showProgress() { @@ -156,14 +94,6 @@ public class NativeMethods } } - @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) - public static String getFormattedDateTime() - { - String currentDate = new SimpleDateFormat((new SimpleDateFormat()).toLocalizedPattern(), Locale.getDefault()).format(new Date()); - - return currentDate; - } - private static void internalProgressDisplay(final boolean show) { final Context ctx = SDL.getContext(); diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ServerService.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ServerService.java index 0afe30161..b0d2c0afa 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ServerService.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ServerService.java @@ -58,15 +58,6 @@ public class ServerService extends Service void onClientRegistered(Messenger client); } - private static class ServerStartThread extends Thread - { - @Override - public void run() - { - NativeMethods.createServer(); - } - } - private static class IncomingClientMessageHandler extends Handler { private WeakReference mCallbackRef; @@ -88,7 +79,6 @@ public class ServerService extends Service callback.onClientRegistered(msg.replyTo); } NativeMethods.setupMsg(msg.replyTo); - new ServerStartThread().start(); break; default: super.handleMessage(msg); diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java index 056321c53..012c2670c 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java @@ -20,9 +20,6 @@ import eu.vcmi.vcmi.util.Log; public class VcmiSDLActivity extends SDLActivity { - public static final int SERVER_MESSAGE_SERVER_READY = 1000; - public static final int SERVER_MESSAGE_SERVER_KILLED = 1001; - public static final String NATIVE_ACTION_CREATE_SERVER = "SDLActivity.Action.CreateServer"; protected static final int COMMAND_USER = 0x8000; final Messenger mClientMessenger = new Messenger( @@ -96,10 +93,6 @@ public class VcmiSDLActivity extends SDLActivity protected void onNewIntent(final Intent intent) { Log.i(this, "Got new intent with action " + intent.getAction()); - if (NATIVE_ACTION_CREATE_SERVER.equals(intent.getAction())) - { - initService(); - } } @Override @@ -188,26 +181,5 @@ public class VcmiSDLActivity extends SDLActivity { mCallback = callback; } - - @Override - public void handleMessage(Message msg) - { - Log.i(this, "Got server msg " + msg); - switch (msg.what) - { - case SERVER_MESSAGE_SERVER_READY: - NativeMethods.notifyServerReady(); - break; - case SERVER_MESSAGE_SERVER_KILLED: - if (mCallback != null) - { - mCallback.unbindServer(); - } - NativeMethods.notifyServerClosed(); - break; - default: - super.handleMessage(msg); - } - } } } \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/values-cs/strings.xml b/android/vcmi-app/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..de5f125a4 --- /dev/null +++ b/android/vcmi-app/src/main/res/values-cs/strings.xml @@ -0,0 +1,71 @@ + + + https://vcmi.eu + https://github.com/vcmi/vcmi + https://github.com/vcmi/vcmi-android + https://github.com/vcmi/vcmi/blob/master/docs/players/Privacy_Policy.md + + VCMI + Server VCMI + Spouštěč VCMI + Škálování herního rozlišení + Současné: neznámé + Současné: %1$d%% + Spustit VCMI + Současná verze VCMI: %1$s + Modifikace + Nainstalovat nové frakce, přeměty a bonusy + Jazyk + Současný: neznámý + Současný: %1$s + Změnit režim ukazatele + Současný: %1$s + Násobitel rychlosti relativního ukazatele + Současný: %1$s + Hlasitost zvuků + Hlasitost hudby + AI světa + Změnit AI světa + Importovat data VCMI + Zkopírovat soubury VCMI do vestavěného úložiště. Můžete importovat starou složku dat vcmi z vydání 0.99 nebo soubory HOMM3 + Exportovat data VCMI + Udělat kopii dat VCMI před odinstalací nebo pro synchronizaci s desktopovou verzí. Též můžete přímo přistoupit k interním datům. + Kopírování %1$s + Současná verze spouštěče: %1$s + Nelze vytvořit datovou složku VCMI v %1$s. + Nelze najít datovou složku v \'%1$s\'. Vložte do ní své datové soubory HoMM3 nebo použijte tlačítko níže. Možná budete muset také restartovat aplikaci. + Nelze najít nebo rozbalit data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci. + Nelze aktualizovat data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci. + Tato aplikace potřebuje oprávnění k zápisu pro použití obsahu na externím úložišti + Nelze správně vyřešit oprávnění + od %1$s + Zkusit znovu + Inicializae hry + Nastavení + Stáhnout data repozitáře + + Normální + Relativní + O spouštěči + + Nalezené modifikace + Nelze načíst modifikaci ve složce \'%1$s\' + Odebírání %1$s + Jste si jisti odebráním %1$s? + + O aplikaci + Verze aplikace: %1$s + Verze spouštěče: %1$s + Projekt + Právní záležitosti + Hlavní stránka: %1$s + Repozitář projektu: %1$s + Repozitář spouštěče: %1$s + Autoři + Zásady ochrany osobních údajů: %1$s + Nebylo možné otevřít webovou stránku (nenalezena patřičná aplikace) + + Autoři VCMI + Autoři spouštěče + Nelze uložit konfigurační soubor VCMI; důvod: %1$s + diff --git a/android/vcmi-app/src/main/res/values-es/strings.xml b/android/vcmi-app/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..3f4522b94 --- /dev/null +++ b/android/vcmi-app/src/main/res/values-es/strings.xml @@ -0,0 +1,71 @@ + + + https://vcmi.eu + https://github.com/vcmi/vcmi + https://github.com/vcmi/vcmi-android + https://github.com/vcmi/vcmi/blob/master/docs/players/Privacy_Policy.md + + VCMI + VCMI Servidor + VCMI Lanzador + Escalado de resolución + Seleccionado: desconocido + Seleccionado: %1$d%% + Iniciar VCMI + version actual VCMI: %1$s + Mods + Instalar nuevas facciones, Objectos, extras + Idioma + Seleccionado: desconocido + Seleccionado: %1$s + Cambiar modo del puntero + Seleccionado: %1$s + Múltiplo de velocidad relativa del puntero + Seleccionado: %1$s + Sonido + Música + Adventure AI + Change adventure AI + Importar datos VCMI + Copiar ficheros VCMI al almacenamiento interno. Puedes importar datos antiguos a partir de la version 0.99 de VCMI o los ficheros de HOMM3. + Exportar datos VCMI + Hacer una copia interna de los datos VCMI antes de una desinstalación o sincronizar las partidas de la versión de escritorio. Tambien puedes acceder al almacenamiento interno directamente. + Copiando %1$s + Version del lanzador: %1$s + No se puede crear la carpeta de datos VCMI en %1$s. + No se encuentra la carpeta de datos en \'%1$s\'. Coloca tus ficheros de datos de HoMM3 en la carpeta o utiliza el boton de abajo para que se haga automáticamente. Quizás sea necesario reiniciar la aplicación. + No se pudieron encontrar ni extraer datos vcmi de los recursos de la aplicación. Intente reinstalar la aplicación. + No se pudieron actualizar los datos de vcmi desde los recursos de la aplicación. Intente reinstalar la aplicación. + Esta aplicación necesita permisos de escritura para utilizar el contenido almacenado en un almacenamiento externo. + Los permisos no se pueden resolver correctamente + por %1$s + Volver a intentar + Iniciar juego + Configuración + Descargar datos del repositorio + + Normal + Relativo + Acerca + + Mods detectados + No se puede cargar el Mod en la carpeta \'%1$s\' + Eliminando %1$s + Quieres eliminar %1$s + + Acerca de la applicación + App versión: %1$s + Versión del lanzador: %1$s + Proyecto + Legal + Página principal: %1$s + Repositorio del Proyecto: %1$s + Repositorio del lanzador: %1$s + Autores + Política de privacidad: %1$s + No se pudo abrir la página web (no se encontró una aplicacion adecuada) + + VCMI autores + Lanzador autores + No se puede guardar la configuración de VCMI; motivo: %1$s + diff --git a/client/CGameInfo.cpp b/client/CGameInfo.cpp index df2e1372d..bcc0cfc8e 100644 --- a/client/CGameInfo.cpp +++ b/client/CGameInfo.cpp @@ -12,16 +12,13 @@ #include "../lib/VCMI_Lib.h" -const CGameInfo * CGI; +CGameInfo * CGI; CClientState * CCS = nullptr; CServerHandler * CSH; CGameInfo::CGameInfo() { - generaltexth = nullptr; - mh = nullptr; - townh = nullptr; globalServices = nullptr; } diff --git a/client/CGameInfo.h b/client/CGameInfo.h index 332068395..c07bb3c7d 100644 --- a/client/CGameInfo.h +++ b/client/CGameInfo.h @@ -56,7 +56,7 @@ extern CClientState * CCS; /// CGameInfo class /// for allowing different functions for accessing game informations -class CGameInfo : public Services +class CGameInfo final : public Services { public: const ArtifactService * artifacts() const override; @@ -78,19 +78,20 @@ public: const spells::effects::Registry * spellEffects() const override; spells::effects::Registry * spellEffects() override; - ConstTransitivePtr modh; //public? - ConstTransitivePtr battleFieldHandler; - ConstTransitivePtr heroh; - ConstTransitivePtr creh; - ConstTransitivePtr spellh; - ConstTransitivePtr skillh; - ConstTransitivePtr objh; - ConstTransitivePtr terrainTypeHandler; - ConstTransitivePtr objtypeh; - ConstTransitivePtr obstacleHandler; - CGeneralTextHandler * generaltexth; - CMapHandler * mh; - CTownHandler * townh; + std::shared_ptr modh; + std::shared_ptr battleFieldHandler; + std::shared_ptr heroh; + std::shared_ptr creh; + std::shared_ptr spellh; + std::shared_ptr skillh; + std::shared_ptr objh; + std::shared_ptr terrainTypeHandler; + std::shared_ptr objtypeh; + std::shared_ptr obstacleHandler; + std::shared_ptr generaltexth; + std::shared_ptr townh; + + std::shared_ptr mh; void setFromLib(); @@ -98,4 +99,4 @@ public: private: const Services * globalServices; }; -extern const CGameInfo* CGI; +extern CGameInfo* CGI; diff --git a/client/CMT.cpp b/client/CMT.cpp index 8f38897f7..e250a190e 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -24,6 +24,7 @@ #include "CServerHandler.h" #include "ClientCommandManager.h" #include "windows/CMessage.h" +#include "windows/InfoWindows.h" #include "render/IScreenHandler.h" #include "render/Graphics.h" @@ -54,12 +55,13 @@ namespace po = boost::program_options; namespace po_style = boost::program_options::command_line_style; -static po::variables_map vm; +static std::atomic quitRequestedDuringOpeningPlayback = false; #ifndef VCMI_IOS void processCommand(const std::string &message); #endif void playIntro(); +[[noreturn]] static void quitApplication(); static void mainLoop(); static CBasicLogConfigurator *logConfig; @@ -69,7 +71,7 @@ void init() CStopWatch tmh; loadDLLClasses(); - const_cast(CGI)->setFromLib(); + CGI->setFromLib(); logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff()); @@ -115,6 +117,8 @@ int main(int argc, char * argv[]) #endif std::cout << "Starting... " << std::endl; po::options_description opts("Allowed options"); + po::variables_map vm; + opts.add_options() ("help,h", "display help and exit") ("version,v", "display version information and exit") @@ -135,16 +139,7 @@ int main(int argc, char * argv[]) ("nointro,i", "skips intro movies") ("donotstartserver,d","do not attempt to start server and just connect to it instead server") ("serverport", po::value(), "override port specified in config file") - ("savefrequency", po::value(), "limit auto save creation to each N days") - ("lobby", "parameters address, port, uuid to connect ro remote lobby session") - ("lobby-address", po::value(), "address to remote lobby") - ("lobby-port", po::value(), "port to remote lobby") - ("lobby-host", "if this client hosts session") - ("lobby-uuid", po::value(), "uuid to the server") - ("lobby-connections", po::value(), "connections of server") - ("lobby-username", po::value(), "player name") - ("lobby-gamemode", po::value(), "use 0 for new game and 1 for load game") - ("uuid", po::value(), "uuid for the client"); + ("savefrequency", po::value(), "limit auto save creation to each N days"); if(argc > 1) { @@ -179,7 +174,8 @@ int main(int argc, char * argv[]) } // Init old logging system and new (temporary) logging system - CStopWatch total, pomtime; + CStopWatch total; + CStopWatch pomtime; std::cout.flags(std::ios::unitbuf); #ifndef VCMI_IOS console = new CConsoleHandler(); @@ -194,6 +190,7 @@ int main(int argc, char * argv[]) console->start(); #endif + setThreadNameLoggingOnly("MainGUI"); const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt"; logConfig = new CBasicLogConfigurator(logPath, console); logConfig->configureDefault(); @@ -202,20 +199,20 @@ int main(int argc, char * argv[]) logGlobal->info("The log file will be saved to %s", logPath); // Init filesystem and settings - preinitDLL(::console); + preinitDLL(::console, false); Settings session = settings.write["session"]; - auto setSettingBool = [](std::string key, std::string arg) { + auto setSettingBool = [&](std::string key, std::string arg) { Settings s = settings.write(vstd::split(key, "/")); - if(::vm.count(arg)) + if(vm.count(arg)) s->Bool() = true; else if(s->isNull()) s->Bool() = false; }; - auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) { + auto setSettingInteger = [&](std::string key, std::string arg, si64 defaultValue) { Settings s = settings.write(vstd::split(key, "/")); - if(::vm.count(arg)) - s->Integer() = ::vm[arg].as(); + if(vm.count(arg)) + s->Integer() = vm[arg].as(); else if(s->isNull()) s->Integer() = defaultValue; }; @@ -246,7 +243,7 @@ int main(int argc, char * argv[]) // Initialize logging based on settings logConfig->configure(); - logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); + logGlobal->debug("settings = %s", settings.toJsonNode().toString()); // Some basic data validation to produce better error messages in cases of incorrect install auto testFile = [](std::string filename, std::string message) @@ -256,10 +253,10 @@ int main(int argc, char * argv[]) }; testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!"); - testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!"); - testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them."); - testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them."); testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!"); + testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!"); + testFile("DATA/PLAYERS.PAL", "Heroes III data files (Data/H3Bitmap.lod) are incomplete or corruped! Please reinstall them."); + testFile("SPRITES/DEFAULT.DEF", "Heroes III data files (Data/H3Sprite.lod) are incomplete or corruped! Please reinstall them."); srand ( (unsigned int)time(nullptr) ); @@ -312,7 +309,6 @@ int main(int argc, char * argv[]) GH.screenHandler().clearScreen(); } - #ifndef VCMI_NO_THREADED_LOAD #ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds { @@ -326,6 +322,9 @@ int main(int argc, char * argv[]) #endif // ANDROID #endif // THREADED + if (quitRequestedDuringOpeningPlayback) + quitApplication(); + if(!settings["session"]["headless"].Bool()) { pomtime.getDiff(); @@ -345,7 +344,6 @@ int main(int argc, char * argv[]) session["autoSkip"].Bool() = vm.count("autoSkip"); session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); session["aiSolo"].Bool() = false; - std::shared_ptr mmenu; if(vm.count("testmap")) { @@ -361,51 +359,11 @@ int main(int argc, char * argv[]) } else { - mmenu = CMainMenu::create(); + auto mmenu = CMainMenu::create(); GH.curInt = mmenu.get(); } std::vector names; - session["lobby"].Bool() = false; - if(vm.count("lobby")) - { - session["lobby"].Bool() = true; - session["host"].Bool() = false; - session["address"].String() = vm["lobby-address"].as(); - if(vm.count("lobby-username")) - session["username"].String() = vm["lobby-username"].as(); - else - session["username"].String() = settings["launcher"]["lobbyUsername"].String(); - if(vm.count("lobby-gamemode")) - session["gamemode"].Integer() = vm["lobby-gamemode"].as(); - else - session["gamemode"].Integer() = 0; - CSH->uuid = vm["uuid"].as(); - session["port"].Integer() = vm["lobby-port"].as(); - logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid); - if(vm.count("lobby-host")) - { - session["host"].Bool() = true; - session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as()); - session["hostUuid"].String() = vm["lobby-uuid"].as(); - logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String()); - } - - //we should not reconnect to previous game in online mode - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; - - //start lobby immediately - names.push_back(session["username"].String()); - ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame; - mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); - } - - // Restore remote session - start game immediately - if(settings["server"]["reconnect"].Bool()) - { - CSH->restoreLastSession(); - } if(!settings["session"]["headless"].Bool()) { @@ -414,7 +372,7 @@ int main(int argc, char * argv[]) else { while(true) - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(200)); } return 0; @@ -425,15 +383,15 @@ void playIntro() { auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); int sound = CCS->soundh->playSound(audioData); - if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true)) + if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, EVideoType::INTRO)) { audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); sound = CCS->soundh->playSound(audioData); - if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true)) + if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, EVideoType::INTRO)) { audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); sound = CCS->soundh->playSound(audioData); - CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true); + CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, EVideoType::INTRO); } } CCS->soundh->stopSound(sound); @@ -441,25 +399,27 @@ void playIntro() static void mainLoop() { +#ifndef VCMI_UNIX + // on Linux, name of main thread is also name of our process. Which we don't want to change setThreadName("MainGUI"); +#endif while(1) //main SDL events loop { GH.input().fetchEvents(); - CSH->applyPacksOnLobbyScreen(); GH.renderFrame(); } } -static void quitApplication() +[[noreturn]] static void quitApplication() { if(!settings["session"]["headless"].Bool()) { if(CSH->client) CSH->endGameplay(); - } - GH.windows().clear(); + GH.windows().clear(); + } CMM.reset(); @@ -471,6 +431,12 @@ static void quitApplication() CCS->musich->release(); CCS->soundh->release(); + delete CCS->consoleh; + delete CCS->curh; + delete CCS->videoh; + delete CCS->musich; + delete CCS->soundh; + vstd::clear_pointer(CCS); } CMessage::dispose(); @@ -478,9 +444,11 @@ static void quitApplication() vstd::clear_pointer(graphics); } + vstd::clear_pointer(CSH); vstd::clear_pointer(VLC); - vstd::clear_pointer(console);// should be removed after everything else since used by logging + // sometimes leads to a hang. TODO: investigate + //vstd::clear_pointer(console);// should be removed after everything else since used by logging if(!settings["session"]["headless"].Bool()) GH.screenHandler().close(); @@ -494,23 +462,41 @@ static void quitApplication() std::cout << "Ending...\n"; - // this method is always called from event/network threads, which keep interface mutex locked - // unlock it here to avoid assertion failure on GH destruction in exit() - GH.interfaceMutex.unlock(); - exit(0); + // Perform quick exit without executing static destructors and let OS cleanup anything that we did not + // We generally don't care about them and this leads to numerous issues, e.g. + // destruction of locked mutexes (fails an assertion), even in third-party libraries (as well as native libs on Android) + // Android - std::quick_exit is available only starting from API level 21 + // Mingw, macOS and iOS - std::quick_exit is unavailable (at least in current version of CI) +#if (defined(__ANDROID_API__) && __ANDROID_API__ < 21) || (defined(__MINGW32__)) || defined(VCMI_APPLE) + ::exit(0); +#else + std::quick_exit(0); +#endif } void handleQuit(bool ask) { - if(CSH->client && LOCPLINT && ask) - { - CCS->curh->set(Cursor::Map::POINTER); - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); - } - else + // FIXME: avoids crash if player attempts to close game while opening is still playing + // use cursor handler as indicator that loading is not done yet + // proper solution would be to abort init thread (or wait for it to finish) + if(!ask) { quitApplication(); + return; } + + if (!CCS->curh) + { + quitRequestedDuringOpeningPlayback = true; + return; + } + + CCS->curh->set(Cursor::Map::POINTER); + + if (LOCPLINT) + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); + else + CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1)); } void handleFatalError(const std::string & message, bool terminate) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2912d02cf..561dd0201 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -51,7 +51,10 @@ set(client_SRCS lobby/CSavingScreen.cpp lobby/CScenarioInfoScreen.cpp lobby/CSelectionBase.cpp + lobby/TurnOptionsTab.cpp + lobby/ExtraOptionsTab.cpp lobby/OptionsTab.cpp + lobby/OptionsTabBase.cpp lobby/RandomMapTab.cpp lobby/SelectionTab.cpp @@ -92,6 +95,12 @@ set(client_SRCS renderSDL/ScreenHandler.cpp renderSDL/SDL_Extensions.cpp + globalLobby/GlobalLobbyClient.cpp + globalLobby/GlobalLobbyLoginWindow.cpp + globalLobby/GlobalLobbyServerSetup.cpp + globalLobby/GlobalLobbyWidget.cpp + globalLobby/GlobalLobbyWindow.cpp + widgets/Buttons.cpp widgets/CArtifactHolder.cpp widgets/CComponent.cpp @@ -99,6 +108,7 @@ set(client_SRCS widgets/CGarrisonInt.cpp widgets/CreatureCostBox.cpp widgets/ComboBox.cpp + widgets/GraphicalPrimitiveCanvas.cpp widgets/Images.cpp widgets/MiscWidgets.cpp widgets/ObjectLists.cpp @@ -113,7 +123,12 @@ set(client_SRCS widgets/CArtifactsOfHeroBackpack.cpp widgets/CWindowWithArtifacts.cpp widgets/RadialMenu.cpp + widgets/markets/CAltarArtifacts.cpp + widgets/markets/CAltarCreatures.cpp + widgets/markets/CTradeBase.cpp + widgets/markets/TradePanels.cpp + windows/CAltarWindow.cpp windows/CCastleInterface.cpp windows/CCreatureWindow.cpp windows/CHeroOverview.cpp @@ -125,6 +140,7 @@ set(client_SRCS windows/CQuestLog.cpp windows/CSpellWindow.cpp windows/CTradeWindow.cpp + windows/CTutorialWindow.cpp windows/CWindowObject.cpp windows/CreaturePurchaseCard.cpp windows/GUIClasses.cpp @@ -149,6 +165,7 @@ set(client_SRCS HeroMovementController.cpp NetPacksClient.cpp NetPacksLobbyClient.cpp + ServerRunner.cpp ) set(client_HEADERS @@ -208,7 +225,10 @@ set(client_HEADERS lobby/CSavingScreen.h lobby/CScenarioInfoScreen.h lobby/CSelectionBase.h + lobby/TurnOptionsTab.h + lobby/ExtraOptionsTab.h lobby/OptionsTab.h + lobby/OptionsTabBase.h lobby/RandomMapTab.h lobby/SelectionTab.h @@ -258,6 +278,13 @@ set(client_HEADERS renderSDL/SDL_Extensions.h renderSDL/SDL_PixelAccess.h + globalLobby/GlobalLobbyClient.h + globalLobby/GlobalLobbyDefines.h + globalLobby/GlobalLobbyLoginWindow.h + globalLobby/GlobalLobbyServerSetup.h + globalLobby/GlobalLobbyWidget.h + globalLobby/GlobalLobbyWindow.h + widgets/Buttons.h widgets/CArtifactHolder.h widgets/CComponent.h @@ -265,6 +292,7 @@ set(client_HEADERS widgets/CGarrisonInt.h widgets/CreatureCostBox.h widgets/ComboBox.h + widgets/GraphicalPrimitiveCanvas.h widgets/Images.h widgets/MiscWidgets.h widgets/ObjectLists.h @@ -279,7 +307,12 @@ set(client_HEADERS widgets/CArtifactsOfHeroBackpack.h widgets/CWindowWithArtifacts.h widgets/RadialMenu.h + widgets/markets/CAltarArtifacts.h + widgets/markets/CAltarCreatures.h + widgets/markets/CTradeBase.h + widgets/markets/TradePanels.h + windows/CAltarWindow.h windows/CCastleInterface.h windows/CCreatureWindow.h windows/CHeroOverview.h @@ -291,6 +324,7 @@ set(client_HEADERS windows/CQuestLog.h windows/CSpellWindow.h windows/CTradeWindow.h + windows/CTutorialWindow.h windows/CWindowObject.h windows/CreaturePurchaseCard.h windows/GUIClasses.h @@ -315,6 +349,7 @@ set(client_HEADERS ClientNetPackVisitors.h HeroMovementController.h LobbyClientNetPackVisitors.h + ServerRunner.h resource.h ) @@ -342,8 +377,7 @@ else() add_executable(vcmiclient ${client_SRCS} ${client_HEADERS}) endif() -add_dependencies(vcmiclient vcmiserver) -if(NOT ENABLE_STATIC_AI_LIBS) +if(NOT ENABLE_STATIC_LIBS) add_dependencies(vcmiclient BattleAI EmptyAI @@ -420,14 +454,13 @@ elseif(APPLE_IOS) set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main") endif() -if(ENABLE_SINGLE_APP_BUILD) - target_link_libraries(vcmiclient PRIVATE vcmiserver) - if(ENABLE_LAUNCHER) - target_link_libraries(vcmiclient PRIVATE vcmilauncher) - endif() +target_link_libraries(vcmiclient PRIVATE vcmiservercommon) +if(ENABLE_SINGLE_APP_BUILD AND ENABLE_LAUNCHER) + target_link_libraries(vcmiclient PRIVATE vcmilauncher) endif() + target_link_libraries(vcmiclient PRIVATE - ${VCMI_LIB_TARGET} SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF + vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF ) if(ffmpeg_LIBRARIES) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index b6101c7cb..bb8cf46ee 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -17,7 +17,6 @@ #include "eventsSDL/InputHandler.h" #include "gui/CGuiHandler.h" -#include "../lib/JsonNode.h" #include "../lib/GameConstants.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/constants/StringConstants.h" @@ -30,7 +29,7 @@ #define VCMI_SOUND_FILE(y) #y, // sounds mapped to soundBase enum -static std::string sounds[] = { +static const std::string sounds[] = { "", // invalid "", // todo VCMI_SOUND_LIST @@ -183,6 +182,35 @@ void CSoundHandler::ambientStopSound(const AudioPath & soundId) setChannelVolume(ambientChannels[soundId], volume); } +uint32_t CSoundHandler::getSoundDurationMilliseconds(const AudioPath & sound) +{ + if (!initialized || sound.empty()) + return 0; + + auto resourcePath = sound.addPrefix("SOUNDS/"); + + if (!CResourceHandler::get()->existsResource(resourcePath)) + return 0; + + auto data = CResourceHandler::get()->load(resourcePath)->readAll(); + + SDL_AudioSpec spec; + uint32_t audioLen; + uint8_t *audioBuf; + uint32_t miliseconds = 0; + + if(SDL_LoadWAV_RW(SDL_RWFromMem(data.first.get(), (int)data.second), 1, &spec, &audioBuf, &audioLen) != nullptr) + { + SDL_FreeWAV(audioBuf); + uint32_t sampleSize = SDL_AUDIO_BITSIZE(spec.format) / 8; + uint32_t sampleCount = audioLen / sampleSize; + uint32_t sampleLen = sampleCount / spec.channels; + miliseconds = 1000 * sampleLen / spec.freq; + } + + return miliseconds ; +} + // Plays a sound, and return its channel so we can fade it out later int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) { diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index 44f39673c..31db7a416 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -23,8 +23,9 @@ protected: bool initialized; int volume; // from 0 (mute) to 100 -public: CAudioBase(): initialized(false), volume(0) {}; + ~CAudioBase() = default; +public: virtual void init() = 0; virtual void release() = 0; @@ -32,7 +33,7 @@ public: ui32 getVolume() const { return volume; }; }; -class CSoundHandler: public CAudioBase +class CSoundHandler final : public CAudioBase { private: //update volume on configuration change @@ -76,6 +77,7 @@ public: void setChannelVolume(int channel, ui32 percent); // Sounds + uint32_t getSoundDurationMilliseconds(const AudioPath & sound); int playSound(soundBase::soundID soundID, int repeats=0); int playSound(const AudioPath & sound, int repeats=0, bool cache=false); int playSound(std::pair, si64> & data, int repeats=0, bool cache=false); @@ -124,7 +126,7 @@ public: bool stop(int fade_ms=0); }; -class CMusicHandler: public CAudioBase +class CMusicHandler final: public CAudioBase { private: //update volume on configuration change diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 760010a25..0a5a548d1 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -43,11 +43,13 @@ #include "render/CAnimation.h" #include "render/IImage.h" +#include "render/IRenderHandler.h" #include "widgets/Buttons.h" #include "widgets/CComponent.h" #include "widgets/CGarrisonInt.h" +#include "windows/CAltarWindow.h" #include "windows/CCastleInterface.h" #include "windows/CCreatureWindow.h" #include "windows/CHeroWindow.h" @@ -56,6 +58,7 @@ #include "windows/CQuestLog.h" #include "windows/CSpellWindow.h" #include "windows/CTradeWindow.h" +#include "windows/CTutorialWindow.h" #include "windows/GUIClasses.h" #include "windows/InfoWindows.h" @@ -72,7 +75,6 @@ #include "../lib/CTownHandler.h" #include "../lib/CondSh.h" #include "../lib/GameConstants.h" -#include "../lib/JsonNode.h" #include "../lib/RoadHandler.h" #include "../lib/StartInfo.h" #include "../lib/TerrainHandler.h" @@ -80,7 +82,6 @@ #include "../lib/UnlockGuard.h" #include "../lib/VCMIDirs.h" -#include "../lib/bonuses/CBonusSystemNode.h" #include "../lib/bonuses/Limiters.h" #include "../lib/bonuses/Propagators.h" #include "../lib/bonuses/Updaters.h" @@ -109,11 +110,7 @@ // They all assume that interface mutex is locked. #define EVENT_HANDLER_CALLED_BY_CLIENT -#define BATTLE_EVENT_POSSIBLE_RETURN \ - if (LOCPLINT != this) \ - return; \ - if (isAutoFightOn && !battleInt) \ - return; +#define BATTLE_EVENT_POSSIBLE_RETURN if (LOCPLINT != this) return; if (isAutoFightOn && !battleInt) return CPlayerInterface * LOCPLINT; @@ -148,6 +145,7 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; + isAutoFightEndBattle = false; ignoreEvents = false; numOfMovedArts = 0; } @@ -274,6 +272,8 @@ void CPlayerInterface::gamePause(bool pause) void CPlayerInterface::yourTurn(QueryID queryID) { + CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP); + EVENT_HANDLER_CALLED_BY_CLIENT; { LOCPLINT = this; @@ -293,7 +293,7 @@ void CPlayerInterface::yourTurn(QueryID queryID) std::string msg = CGI->generaltexth->allTexts[13]; boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); + cmp.push_back(std::make_shared(ComponentType::FLAG, playerID)); showInfoDialog(msg, cmp); } else @@ -326,7 +326,7 @@ void CPlayerInterface::acceptTurn(QueryID queryID) auto playerColor = *cb->getPlayerID(); std::vector components; - components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0); + components.emplace_back(ComponentType::FLAG, playerColor); MetaString text; const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle; @@ -337,13 +337,13 @@ void CPlayerInterface::acceptTurn(QueryID queryID) if (daysWithoutCastle < 6) { text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); + text.replaceName(playerColor); text.replaceNumber(7 - daysWithoutCastle); } else if (daysWithoutCastle == 6) { text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); + text.replaceName(playerColor); } showInfoDialogAndWait(components, text); @@ -421,7 +421,7 @@ void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, Prim if (which == PrimarySkill::EXPERIENCE) { for (auto ctw : GH.windows().findWindows()) - ctw->setExpToLevel(); + ctw->updateExpToLevel(); } else adventureInt->onHeroChanged(hero); @@ -498,8 +498,9 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) adventureInt->onHeroChanged(nullptr); adventureInt->onTownChanged(town); - for (auto gh : GH.windows().findWindows()) - gh->updateGarrisons(); + for (auto cgh : GH.windows().findWindows()) + if (cgh->holdsGarrison(town)) + cgh->updateGarrisons(); for (auto ki : GH.windows().findWindows()) ki->townChanged(town); @@ -520,49 +521,42 @@ void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownIn void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) { - std::vector instances; + std::vector instances; - if(auto obj = cb->getObj(id1)) + if(auto obj = dynamic_cast(cb->getObjInstance(id1))) instances.push_back(obj); if(id2 != ObjectInstanceID() && id2 != id1) { - if(auto obj = cb->getObj(id2)) + if(auto obj = dynamic_cast(cb->getObjInstance(id2))) instances.push_back(obj); } garrisonsChanged(instances); } -void CPlayerInterface::garrisonsChanged(std::vector objs) +void CPlayerInterface::garrisonsChanged(std::vector objs) { for (auto object : objs) { auto * hero = dynamic_cast(object); auto * town = dynamic_cast(object); + if (town) + adventureInt->onTownChanged(town); + if (hero) { adventureInt->onHeroChanged(hero); - - if(hero->inTownGarrison) - { + if(hero->inTownGarrison && hero->visitedTown != town) adventureInt->onTownChanged(hero->visitedTown); - } } - if (town) - adventureInt->onTownChanged(town); } for (auto cgh : GH.windows().findWindows()) - cgh->updateGarrisons(); - - for (auto cmw : GH.windows().findWindows()) - { - if (vstd::contains(objs, cmw->hero)) - cmw->garrisonChanged(); - } + if (cgh->holdsGarrisons(objs)) + cgh->updateGarrisons(); GH.windows().totalRedraw(); } @@ -789,17 +783,20 @@ void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult * if(!battleInt) { - bool allowManualReplay = queryID != QueryID::NONE; + bool allowManualReplay = queryID != QueryID::NONE && !isAutoFightEndBattle; auto wnd = std::make_shared(*br, *this, allowManualReplay); - if (allowManualReplay) + if (allowManualReplay || isAutoFightEndBattle) { wnd->resultCallback = [=](ui32 selection) { cb->selectionMade(selection, queryID); }; } + + isAutoFightEndBattle = false; + GH.windows().pushWindow(wnd); // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. // Otherwise NewTurn causes freeze. @@ -1071,6 +1068,19 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component { EVENT_HANDLER_CALLED_BY_CLIENT; + std::vector tmpObjects; + if(objects.size() && dynamic_cast(cb->getObj(objects[0]))) + { + // sorting towns (like in client) + std::vector Towns = LOCPLINT->localState->getOwnedTowns(); + for(auto town : Towns) + for(auto item : objects) + if(town == cb->getObj(item)) + tmpObjects.push_back(item); + } + else // other object list than town + tmpObjects = objects; + auto selectCallback = [=](int selection) { cb->sendQueryReply(selection, askID); @@ -1085,9 +1095,9 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component const std::string localDescription = description.toString(); std::vector tempList; - tempList.reserve(objects.size()); + tempList.reserve(tmpObjects.size()); - for(auto item : objects) + for(auto item : tmpObjects) tempList.push_back(item.getNum()); CComponent localIconC(icon); @@ -1095,8 +1105,24 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component std::shared_ptr localIcon = localIconC.image; localIconC.removeChild(localIcon.get(), false); - std::shared_ptr wnd = std::make_shared(tempList, localIcon, localTitle, localDescription, selectCallback); + std::vector> images; + for(auto & obj : tmpObjects) + { + if(!settings["general"]["enableUiEnhancements"].Bool()) + break; + const CGTownInstance * t = dynamic_cast(cb->getObj(obj)); + if(t) + { + std::shared_ptr a = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITPA")); + a->preload(); + images.push_back(a->getImage(t->town->clientInfo.icons[t->hasFort()][false] + 2)->scaleFast(Point(35, 23))); + } + } + + auto wnd = std::make_shared(tempList, localIcon, localTitle, localDescription, selectCallback, 0, images); wnd->onExit = cancelCallback; + wnd->onPopup = [this, tmpObjects](int index) { CRClickPopup::createAndPush(cb->getObj(tmpObjects[index]), GH.getCursorPosition()); }; + wnd->onClicked = [this, tmpObjects](int index) { adventureInt->centerOnObject(cb->getObj(tmpObjects[index])); GH.windows().totalRedraw(); }; GH.windows().pushWindow(wnd); } @@ -1157,16 +1183,16 @@ void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus } } -void CPlayerInterface::saveGame( BinarySerializer & h, const int version ) +void CPlayerInterface::saveGame( BinarySerializer & h ) { EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h, version); + localState->serialize(h); } -void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version ) +void CPlayerInterface::loadGame( BinaryDeserializer & h ) { EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h, version); + localState->serialize(h); firstCall = -1; } @@ -1228,7 +1254,7 @@ void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, con text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated()); // Picture of assembled artifact at bottom. - auto sc = std::make_shared(CComponent::artifact, assembledArtifact->getIndex(), 0); + auto sc = std::make_shared(ComponentType::ARTIFACT, assembledArtifact->getId()); scs.push_back(sc); } else @@ -1242,10 +1268,10 @@ void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, con void CPlayerInterface::requestRealized( PackageApplied *pa ) { - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) movementController->onMoveHeroApplied(); - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) movementController->onQueryReplyApplied(); } @@ -1441,7 +1467,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start) std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked"); boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); + cmp.push_back(std::make_shared(ComponentType::FLAG, playerID)); makingTurn = true; //workaround for stiff showInfoDialog implementation showInfoDialog(msg, cmp); makingTurn = false; @@ -1678,7 +1704,8 @@ void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const C { EVENT_HANDLER_CALLED_BY_CLIENT; auto onWindowClosed = [this, queryID](){ - cb->selectionMade(0, queryID); + if (queryID != QueryID::NONE) + cb->selectionMade(0, queryID); }; GH.windows().createAndPushWindow(object, onWindowClosed); } @@ -1753,8 +1780,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) { - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - if(hero) + if(auto hero = cb->getHero(al.artHolder)) { auto art = hero->getArt(al.slot); if(art == nullptr) @@ -1770,15 +1796,13 @@ void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) void CPlayerInterface::artifactPut(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); } void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); for(auto artWin : GH.windows().findWindows()) artWin->artifactRemoved(al); @@ -1789,8 +1813,7 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), dst.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(dst.artHolder)); bool redraw = true; // If a bulk transfer has arrived, then redrawing only the last art movement. @@ -1815,8 +1838,7 @@ void CPlayerInterface::bulkArtMovementStart(size_t numOfArts) void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); for(auto artWin : GH.windows().findWindows()) artWin->artifactAssembled(al); @@ -1825,8 +1847,7 @@ void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); for(auto artWin : GH.windows().findWindows()) artWin->artifactDisassembled(al); @@ -1848,14 +1869,9 @@ void CPlayerInterface::proposeLoadingGame() CGI->generaltexth->allTexts[68], []() { - GH.dispatchMainThread( - []() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("load"); - } - ); + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("load"); }, nullptr ); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 41245ee5f..de41ed1a9 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -87,6 +87,7 @@ public: // TODO: make private //During battle is quick combat mode is used std::shared_ptr autofightingAI; //AI that makes decisions bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface. + bool isAutoFightEndBattle; //Flag, if battle forced to end with autocombat protected: // Call-ins from server, should not be called directly, but only via GameInterface @@ -145,8 +146,8 @@ protected: // Call-ins from server, should not be called directly, but only via void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface void playerEndsTurn(PlayerColor player) override; - void saveGame(BinarySerializer & h, const int version) override; //saving - void loadGame(BinaryDeserializer & h, const int version) override; //loading + void saveGame(BinarySerializer & h) override; //saving + void loadGame(BinaryDeserializer & h) override; //loading void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; //for battles @@ -223,7 +224,7 @@ private: }; void heroKilled(const CGHeroInstance* hero); - void garrisonsChanged(std::vector objs); + void garrisonsChanged(std::vector objs); void requestReturningToMainMenu(bool won); void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close void initializeHeroTownList(); diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 6e6018f0e..0ac30b561 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -12,10 +12,12 @@ #include "CServerHandler.h" #include "Client.h" #include "CGameInfo.h" +#include "ServerRunner.h" #include "CPlayerInterface.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" +#include "globalLobby/GlobalLobbyClient.h" #include "lobby/CSelectionBase.h" #include "lobby/CLobbyScreen.h" #include "windows/InfoWindows.h" @@ -24,17 +26,6 @@ #include "mainmenu/CPrologEpilogVideo.h" #include "mainmenu/CHighScoreScreen.h" -#ifdef VCMI_ANDROID -#include "../lib/CAndroidVMHelper.h" -#elif defined(VCMI_IOS) -#include "ios/utils.h" -#include -#endif - -#ifdef SINGLE_PROCESS_APP -#include "../server/CVCMIServer.h" -#endif - #include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CThreadHelper.h" @@ -46,37 +37,27 @@ #include "../lib/mapObjects/MiscObjects.h" #include "../lib/modding/ModIncompatibility.h" #include "../lib/rmg/CMapGenOptions.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypes.h" #include "../lib/serializer/Connection.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/registerTypes/RegisterTypesLobbyPacks.h" #include "../lib/serializer/CMemorySerializer.h" +#include "../lib/UnlockGuard.h" #include #include #include -#include #include "../lib/serializer/Cast.h" #include "LobbyClientNetPackVisitors.h" #include -#ifdef VCMI_WINDOWS -#include -#endif - template class CApplyOnLobby; -const std::string CServerHandler::localhostAddress{"127.0.0.1"}; - -#if defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) -extern std::atomic_bool androidTestServerReadyFlag; -#endif - class CBaseForLobbyApply { public: - virtual bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const = 0; - virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const = 0; + virtual bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const = 0; + virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const = 0; virtual ~CBaseForLobbyApply(){}; template static CBaseForLobbyApply * getApplier(const U * t = nullptr) { @@ -87,310 +68,306 @@ public: template class CApplyOnLobby : public CBaseForLobbyApply { public: - bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override + bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - - T * ptr = static_cast(pack); + auto & ref = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); - logNetwork->trace("\tImmediately apply on lobby: %s", typeList.getTypeInfo(ptr)->name()); - ptr->visit(visitor); + logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ref).name()); + ref.visit(visitor); return visitor.getResult(); } - void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override { - T * ptr = static_cast(pack); + auto & ref = static_cast(pack); ApplyOnLobbyScreenNetPackVisitor visitor(*handler, lobby); - logNetwork->trace("\tApply on lobby from queue: %s", typeList.getTypeInfo(ptr)->name()); - ptr->visit(visitor); + logNetwork->trace("\tApply on lobby from queue: %s", typeid(ref).name()); + ref.visit(visitor); } }; template<> class CApplyOnLobby: public CBaseForLobbyApply { public: - bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override + bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); return false; } - void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, void * pack) const override + void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); } }; -static const std::string NAME_AFFIX = "client"; -static const std::string NAME = GameConstants::VCMI_VERSION + std::string(" (") + NAME_AFFIX + ')'; //application name +CServerHandler::~CServerHandler() +{ + if (serverRunner) + serverRunner->shutdown(); + networkHandler->stop(); + try + { + if (serverRunner) + serverRunner->wait(); + serverRunner.reset(); + threadNetwork.join(); + } + catch (const std::runtime_error & e) + { + logGlobal->error("Failed to shut down network thread! Reason: %s", e.what()); + assert(0); + } +} CServerHandler::CServerHandler() - : state(EClientState::NONE), mx(std::make_shared()), client(nullptr), loadMode(0), campaignStateToSend(nullptr), campaignServerRestartLock(false) + : networkHandler(INetworkHandler::createHandler()) + , lobbyClient(std::make_unique()) + , applier(std::make_unique>()) + , threadNetwork(&CServerHandler::threadRunNetwork, this) + , state(EClientState::NONE) + , serverPort(0) + , campaignStateToSend(nullptr) + , screenType(ESelectionScreen::unknown) + , serverMode(EServerMode::NONE) + , loadMode(ELoadMode::NONE) + , client(nullptr) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); - //read from file to restore last session - if(!settings["server"]["uuid"].isNull() && !settings["server"]["uuid"].String().empty()) - uuid = settings["server"]["uuid"].String(); - applier = std::make_shared>(); registerTypesLobbyPacks(*applier); } -void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::vector * names) +void CServerHandler::threadRunNetwork() +{ + logGlobal->info("Starting network thread"); + setThreadName("runNetwork"); + networkHandler->run(); + logGlobal->info("Ending network thread"); +} + +void CServerHandler::resetStateForLobby(EStartMode mode, ESelectionScreen screen, EServerMode newServerMode, const std::vector & playerNames) { hostClientId = -1; - state = EClientState::NONE; + setState(EClientState::NONE); + serverMode = newServerMode; mapToStart = nullptr; th = std::make_unique(); - packsForLobbyScreen.clear(); - c.reset(); + logicConnection.reset(); si = std::make_shared(); - playerNames.clear(); + localPlayerNames.clear(); si->difficulty = 1; si->mode = mode; - myNames.clear(); - if(names && !names->empty()) //if have custom set of player names - use it - myNames = *names; + screenType = screen; + localPlayerNames.clear(); + if(!playerNames.empty()) //if have custom set of player names - use it + localPlayerNames = playerNames; else - myNames.push_back(settings["general"]["playerName"].String()); + localPlayerNames.push_back(settings["general"]["playerName"].String()); } -void CServerHandler::startLocalServerAndConnect() +GlobalLobbyClient & CServerHandler::getGlobalLobby() { - if(threadRunLocalServer) - threadRunLocalServer->join(); + return *lobbyClient; +} - th->update(); - - auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.existingProcess"); - try - { - CConnection testConnection(localhostAddress, getDefaultPort(), NAME, uuid); - logNetwork->error("Port is busy, check if another instance of vcmiserver is working"); - CInfoWindow::showInfoDialog(errorMsg, {}); - return; - } - catch(std::runtime_error & error) - { - //no connection means that port is not busy and we can start local server - } - -#if defined(SINGLE_PROCESS_APP) - boost::condition_variable cond; - std::vector args{"--uuid=" + uuid, "--port=" + std::to_string(getHostPort())}; - if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) - { - args.push_back("--lobby=" + settings["session"]["address"].String()); - args.push_back("--connections=" + settings["session"]["hostConnections"].String()); - args.push_back("--lobby-port=" + std::to_string(settings["session"]["port"].Integer())); - args.push_back("--lobby-uuid=" + settings["session"]["hostUuid"].String()); - } - threadRunLocalServer = std::make_shared([&cond, args, this] { - setThreadName("CVCMIServer"); - CVCMIServer::create(&cond, args); - onServerFinished(); - }); - threadRunLocalServer->detach(); -#elif defined(VCMI_ANDROID) - { - CAndroidVMHelper envHelper; - envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "startServer", true); - } +INetworkHandler & CServerHandler::getNetworkHandler() +{ + return *networkHandler; +} + +void CServerHandler::startLocalServerAndConnect(bool connectToLobby) +{ + logNetwork->trace("\tLocal server startup has been requested"); +#ifdef VCMI_MOBILE + // mobile apps can't spawn separate processes - only thread mode is available + serverRunner.reset(new ServerThreadRunner()); #else - threadRunLocalServer = std::make_shared(&CServerHandler::threadRunServer, this); //runs server executable; -#endif - logNetwork->trace("Setting up thread calling server: %d ms", th->getDiff()); - - th->update(); - -#ifdef SINGLE_PROCESS_APP - { -#ifdef VCMI_IOS - dispatch_sync(dispatch_get_main_queue(), ^{ - iOS_utils::showLoadingIndicator(); - }); + if (settings["server"]["useProcess"].Bool()) + serverRunner.reset(new ServerProcessRunner()); + else + serverRunner.reset(new ServerThreadRunner()); #endif - boost::mutex m; - boost::unique_lock lock{m}; - logNetwork->info("waiting for server"); - cond.wait(lock); - logNetwork->info("server is ready"); - -#ifdef VCMI_IOS - dispatch_sync(dispatch_get_main_queue(), ^{ - iOS_utils::hideLoadingIndicator(); - }); -#endif - } -#elif defined(VCMI_ANDROID) - logNetwork->info("waiting for server"); - while(!androidTestServerReadyFlag.load()) - { - logNetwork->info("still waiting..."); - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); - } - logNetwork->info("waiting for server finished..."); - androidTestServerReadyFlag = false; -#endif - logNetwork->trace("Waiting for server: %d ms", th->getDiff()); - - th->update(); //put breakpoint here to attach to server before it does something stupid - - justConnectToServer(localhostAddress, 0); - - logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff()); + logNetwork->trace("\tStarting local server"); + serverRunner->start(getLocalPort(), connectToLobby); + logNetwork->trace("\tConnecting to local server"); + connectToServer(getLocalHostname(), getLocalPort()); + logNetwork->trace("\tWaiting for connection"); } -void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) +void CServerHandler::connectToServer(const std::string & addr, const ui16 port) { - state = EClientState::CONNECTING; - while(!c && state != EClientState::CONNECTION_CANCELLED) + logNetwork->info("Establishing connection to %s:%d...", addr, port); + setState(EClientState::CONNECTING); + serverHostname = addr; + serverPort = port; + + if (!isServerLocal()) { - try - { - logNetwork->info("Establishing connection..."); - c = std::make_shared( - addr.size() ? addr : getHostAddress(), - port ? port : getHostPort(), - NAME, uuid); - } - catch(std::runtime_error & error) - { - logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what()); - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); - } + Settings remoteAddress = settings.write["server"]["remoteHostname"]; + remoteAddress->String() = addr; + + Settings remotePort = settings.write["server"]["remotePort"]; + remotePort->Integer() = port; } - if(state == EClientState::CONNECTION_CANCELLED) + networkHandler->connectToRemote(*this, addr, port); +} + +void CServerHandler::onConnectionFailed(const std::string & errorMessage) +{ + assert(getState() == EClientState::CONNECTING); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + if (isServerLocal()) + { + // retry - local server might be still starting up + logNetwork->debug("\nCannot establish connection. %s. Retrying...", errorMessage); + networkHandler->createTimer(*this, std::chrono::milliseconds(100)); + } + else + { + // remote server refused connection - show error message + setState(EClientState::NONE); + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.serverConnectionFailed"), {}); + } +} + +void CServerHandler::onTimer() +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + if(getState() == EClientState::CONNECTION_CANCELLED) { logNetwork->info("Connection aborted by player!"); + serverRunner->wait(); + serverRunner.reset(); + if (GH.windows().topWindow() != nullptr) + GH.windows().popWindows(1); return; } - c->handler = std::make_shared(&CServerHandler::threadHandleConnection, this); - - if(!addr.empty() && addr != getHostAddress()) - { - Settings serverAddress = settings.write["server"]["server"]; - serverAddress->String() = addr; - } - if(port && port != getHostPort()) - { - Settings serverPort = settings.write["server"]["port"]; - serverPort->Integer() = port; - } + assert(isServerLocal()); + networkHandler->connectToRemote(*this, getLocalHostname(), getLocalPort()); } -void CServerHandler::applyPacksOnLobbyScreen() +void CServerHandler::onConnectionEstablished(const NetworkConnectionPtr & netConnection) { - if(!c || !c->handler) - return; + assert(getState() == EClientState::CONNECTING); - boost::unique_lock lock(*mx); - while(!packsForLobbyScreen.empty()) + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + networkConnection = netConnection; + + logNetwork->info("Connection established"); + + if (serverMode == EServerMode::LOBBY_GUEST) { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - CPackForLobby * pack = packsForLobbyScreen.front(); - packsForLobbyScreen.pop_front(); - CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier - apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); - GH.windows().totalRedraw(); - delete pack; + // say hello to lobby to switch connection to proxy mode + getGlobalLobby().sendProxyConnectionLogin(netConnection); } + + logicConnection = std::make_shared(netConnection); + logicConnection->uuid = uuid; + logicConnection->enterLobbyConnectionMode(); + sendClientConnecting(); } -void CServerHandler::stopServerConnection() +void CServerHandler::applyPackOnLobbyScreen(CPackForLobby & pack) { - if(c->handler) - { - while(!c->handler->timed_join(boost::chrono::milliseconds(50))) - applyPacksOnLobbyScreen(); - c->handler->join(); - } + const CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(&pack)); //find the applier + apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); + GH.windows().totalRedraw(); } std::set CServerHandler::getHumanColors() { - return clientHumanColors(c->connectionID); + return clientHumanColors(logicConnection->connectionID); } - PlayerColor CServerHandler::myFirstColor() const { - return clientFirstColor(c->connectionID); + return clientFirstColor(logicConnection->connectionID); } bool CServerHandler::isMyColor(PlayerColor color) const { - return isClientColor(c->connectionID, color); + return isClientColor(logicConnection->connectionID, color); } ui8 CServerHandler::myFirstId() const { - return clientFirstId(c->connectionID); + return clientFirstId(logicConnection->connectionID); +} + +EClientState CServerHandler::getState() const +{ + return state; +} + +void CServerHandler::setState(EClientState newState) +{ + if (newState == EClientState::CONNECTION_CANCELLED && serverRunner != nullptr) + serverRunner->shutdown(); + + state = newState; } bool CServerHandler::isServerLocal() const { - if(threadRunLocalServer) - return true; - - return false; + return serverRunner != nullptr; } bool CServerHandler::isHost() const { - return c && hostClientId == c->connectionID; + return logicConnection && hostClientId == logicConnection->connectionID; } bool CServerHandler::isGuest() const { - return !c || hostClientId != c->connectionID; + return !logicConnection || hostClientId != logicConnection->connectionID; } -ui16 CServerHandler::getDefaultPort() +const std::string & CServerHandler::getLocalHostname() const { - return static_cast(settings["server"]["port"].Integer()); + return settings["server"]["localHostname"].String(); } -std::string CServerHandler::getDefaultPortStr() +ui16 CServerHandler::getLocalPort() const { - return std::to_string(getDefaultPort()); + return settings["server"]["localPort"].Integer(); } -std::string CServerHandler::getHostAddress() const +const std::string & CServerHandler::getRemoteHostname() const { - if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) - return settings["server"]["server"].String(); - - if(settings["session"]["host"].Bool()) - return localhostAddress; - - return settings["session"]["address"].String(); + return settings["server"]["remoteHostname"].String(); } -ui16 CServerHandler::getHostPort() const +ui16 CServerHandler::getRemotePort() const { - if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) - return getDefaultPort(); - - if(settings["session"]["host"].Bool()) - return getDefaultPort(); - - return settings["session"]["port"].Integer(); + return settings["server"]["remotePort"].Integer(); +} + +const std::string & CServerHandler::getCurrentHostname() const +{ + return serverHostname; +} + +ui16 CServerHandler::getCurrentPort() const +{ + return serverPort; } void CServerHandler::sendClientConnecting() const { LobbyClientConnected lcc; lcc.uuid = uuid; - lcc.names = myNames; + lcc.names = localPlayerNames; lcc.mode = si->mode; sendLobbyPack(lcc); } @@ -398,13 +375,16 @@ void CServerHandler::sendClientConnecting() const void CServerHandler::sendClientDisconnecting() { // FIXME: This is workaround needed to make sure client not trying to sent anything to non existed server - if(state == EClientState::DISCONNECTING) + if(getState() == EClientState::DISCONNECTING) + { + assert(0); return; + } - state = EClientState::DISCONNECTING; + setState(EClientState::DISCONNECTING); mapToStart = nullptr; LobbyClientDisconnected lcd; - lcd.clientId = c->connectionID; + lcd.clientId = logicConnection->connectionID; logNetwork->info("Connection has been requested to be closed."); if(isServerLocal()) { @@ -416,14 +396,14 @@ void CServerHandler::sendClientDisconnecting() logNetwork->info("Sent leaving signal to the server"); } sendLobbyPack(lcd); - - c->close(); - c.reset(); + networkConnection->close(); + networkConnection.reset(); + logicConnection.reset(); } void CServerHandler::setCampaignState(std::shared_ptr newCampaign) { - state = EClientState::LOBBY_CAMPAIGN; + setState(EClientState::LOBBY_CAMPAIGN); LobbySetCampaign lsc; lsc.ourCampaign = newCampaign; sendLobbyPack(lsc); @@ -431,7 +411,7 @@ void CServerHandler::setCampaignState(std::shared_ptr newCampaign void CServerHandler::setCampaignMap(CampaignScenarioID mapId) const { - if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place + if(getState() == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place return; LobbySetCampaignMap lscm; @@ -441,7 +421,7 @@ void CServerHandler::setCampaignMap(CampaignScenarioID mapId) const void CServerHandler::setCampaignBonus(int bonusId) const { - if(state == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place + if(getState() == EClientState::GAMEPLAY) // FIXME: UI shouldn't sent commands in first place return; LobbySetCampaignBonus lscb; @@ -502,6 +482,13 @@ void CServerHandler::setTurnTimerInfo(const TurnTimerInfo & info) const sendLobbyPack(lstt); } +void CServerHandler::setExtraOptionsInfo(const ExtraOptionsInfo & info) const +{ + LobbySetExtraOptions lseo; + lseo.extraOptionsInfo = info; + sendLobbyPack(lseo); +} + void CServerHandler::sendMessage(const std::string & txt) const { std::istringstream readed; @@ -521,7 +508,8 @@ void CServerHandler::sendMessage(const std::string & txt) const } else if(command == "!forcep") { - std::string connectedId, playerColorId; + std::string connectedId; + std::string playerColorId; readed >> connectedId; readed >> playerColorId; if(connectedId.length() && playerColorId.length()) @@ -557,9 +545,7 @@ void CServerHandler::sendRestartGame() const { GH.windows().createAndPushWindow(); - LobbyEndGame endGame; - endGame.closeConnection = false; - endGame.restart = true; + LobbyRestartGame endGame; sendLobbyPack(endGame); } @@ -599,19 +585,22 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const void CServerHandler::sendStartGame(bool allowOnlyAI) const { verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); - GH.windows().createAndPushWindow(); + + if(!settings["session"]["headless"].Bool()) + GH.windows().createAndPushWindow(); + LobbyPrepareStartGame lpsg; + sendLobbyPack(lpsg); + LobbyStartGame lsg; if(client) { lsg.initializedStartInfo = std::make_shared(* const_cast(client->getStartInfo(true))); - lsg.initializedStartInfo->mode = StartInfo::NEW_GAME; + lsg.initializedStartInfo->mode = EStartMode::NEW_GAME; lsg.initializedStartInfo->seedToBeUsed = lsg.initializedStartInfo->seedPostInit = 0; * si = * lsg.initializedStartInfo; } sendLobbyPack(lsg); - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); } void CServerHandler::startMapAfterConnection(std::shared_ptr to) @@ -623,80 +612,55 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta { if(CMM) CMM->disable(); - client = new CClient(); highScoreCalc = nullptr; switch(si->mode) { - case StartInfo::NEW_GAME: + case EStartMode::NEW_GAME: client->newGame(gameState); break; - case StartInfo::CAMPAIGN: + case EStartMode::CAMPAIGN: client->newGame(gameState); break; - case StartInfo::LOAD_GAME: + case EStartMode::LOAD_GAME: client->loadGame(gameState); break; default: throw std::runtime_error("Invalid mode"); } // After everything initialized we can accept CPackToClient netpacks - c->enterGameplayConnectionMode(client->gameState()); - state = EClientState::GAMEPLAY; - - //store settings to continue game - if(!isServerLocal() && isGuest()) + logicConnection->enterGameplayConnectionMode(client->gameState()); + setState(EClientState::GAMEPLAY); +} + +void CServerHandler::endGameplay() +{ + // Game is ending + // Tell the network thread to reach a stable state + CSH->sendClientDisconnecting(); + logNetwork->info("Closed connection."); + + client->endGame(); + client.reset(); + + if(CMM) { - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = true; - Settings saveUuid = settings.write["server"]["uuid"]; - saveUuid->String() = uuid; - Settings saveNames = settings.write["server"]["names"]; - saveNames->Vector().clear(); - for(auto & name : myNames) - { - JsonNode jsonName; - jsonName.String() = name; - saveNames->Vector().push_back(jsonName); - } + GH.curInt = CMM.get(); + CMM->enable(); + } + else + { + GH.curInt = CMainMenu::create().get(); } } -void CServerHandler::endGameplay(bool closeConnection, bool restart) +void CServerHandler::restartGameplay() { client->endGame(); - vstd::clear_pointer(client); + client.reset(); - if(closeConnection) - { - // Game is ending - // Tell the network thread to reach a stable state - CSH->sendClientDisconnecting(); - logNetwork->info("Closed connection."); - } - if(!restart) - { - if(CMM) - { - GH.curInt = CMM.get(); - CMM->enable(); - } - else - { - GH.curInt = CMainMenu::create().get(); - } - } - - if(c) - { - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); - } - - //reset settings - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; + logicConnection->enterLobbyConnectionMode(); } void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr cs) @@ -717,13 +681,12 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared GH.dispatchMainThread([ourCampaign, this]() { - CSH->campaignServerRestartLock.set(true); CSH->endGameplay(); auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; auto finisher = [=]() { - if(ourCampaign->campaignSet != "") + if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished()) { Settings entry = persistentStorage.write["completedCampaigns"][ourCampaign->getFilename()]; entry->Bool() = true; @@ -740,13 +703,13 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared GH.windows().createAndPushWindow(true, *highScoreCalc); } }; + if(epilogue.hasPrologEpilog) { GH.windows().createAndPushWindow(epilogue, finisher); } else { - CSH->campaignServerRestartLock.waitUntil(false); finisher(); } }); @@ -772,15 +735,15 @@ int CServerHandler::howManyPlayerInterfaces() return playerInts; } -ui8 CServerHandler::getLoadMode() +ELoadMode CServerHandler::getLoadMode() { - if(loadMode != ELoadMode::TUTORIAL && state == EClientState::GAMEPLAY) + if(loadMode != ELoadMode::TUTORIAL && getState() == EClientState::GAMEPLAY) { if(si->campState) return ELoadMode::CAMPAIGN; for(auto pn : playerNames) { - if(pn.second.connection != c->connectionID) + if(pn.second.connection != logicConnection->connectionID) return ELoadMode::MULTI; } if(howManyPlayerInterfaces() > 1) //this condition will work for hotseat mode OR multiplayer with allowed more than 1 color per player to control @@ -791,48 +754,24 @@ ui8 CServerHandler::getLoadMode() return loadMode; } -void CServerHandler::restoreLastSession() -{ - auto loadSession = [this]() - { - uuid = settings["server"]["uuid"].String(); - for(auto & name : settings["server"]["names"].Vector()) - myNames.push_back(name.String()); - resetStateForLobby(StartInfo::LOAD_GAME, &myNames); - screenType = ESelectionScreen::loadGame; - justConnectToServer(settings["server"]["server"].String(), settings["server"]["port"].Integer()); - }; - - auto cleanUpSession = []() - { - //reset settings - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; - }; - - CInfoWindow::showYesNoDialog(VLC->generaltexth->translate("vcmi.server.confirmReconnect"), {}, loadSession, cleanUpSession); -} - void CServerHandler::debugStartTest(std::string filename, bool save) { logGlobal->info("Starting debug test with file: %s", filename); auto mapInfo = std::make_shared(); if(save) { - resetStateForLobby(StartInfo::LOAD_GAME); + resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOCAL, {}); mapInfo->saveInit(ResourcePath(filename, EResType::SAVEGAME)); - screenType = ESelectionScreen::loadGame; } else { - resetStateForLobby(StartInfo::NEW_GAME); + resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOCAL, {}); mapInfo->mapInit(filename); - screenType = ESelectionScreen::newGame; } if(settings["session"]["donotstartserver"].Bool()) - justConnectToServer(localhostAddress, 3030); + connectToServer(getLocalHostname(), getLocalPort()); else - startLocalServerAndConnect(); + startLocalServerAndConnect(false); boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); @@ -875,91 +814,98 @@ public: { } - virtual bool callTyped() override { return false; } + bool callTyped() override { return false; } - virtual void visitForLobby(CPackForLobby & lobbyPack) override + void visitForLobby(CPackForLobby & lobbyPack) override { handler.visitForLobby(lobbyPack); } - virtual void visitForClient(CPackForClient & clientPack) override + void visitForClient(CPackForClient & clientPack) override { handler.visitForClient(clientPack); } }; -void CServerHandler::threadHandleConnection() +void CServerHandler::onPacketReceived(const std::shared_ptr &, const std::vector & message) { - setThreadName("handleConnection"); - c->enterLobbyConnectionMode(); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - try + if(getState() == EClientState::DISCONNECTING) + return; + + CPack * pack = logicConnection->retrievePack(message); + ServerHandlerCPackVisitor visitor(*this); + pack->visit(visitor); +} + +void CServerHandler::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + waitForServerShutdown(); + + if(getState() == EClientState::DISCONNECTING) { - sendClientConnecting(); - while(c && c->connected) - { - while(state == EClientState::STARTING) - boost::this_thread::sleep_for(boost::chrono::milliseconds(10)); - - CPack * pack = c->retrievePack(); - if(state == EClientState::DISCONNECTING) - { - // FIXME: server shouldn't really send netpacks after it's tells client to disconnect - // Though currently they'll be delivered and might cause crash. - vstd::clear_pointer(pack); - } - else - { - ServerHandlerCPackVisitor visitor(*this); - pack->visit(visitor); - } - } + assert(networkConnection == nullptr); + // Note: this branch can be reached on app shutdown, when main thread holds mutex till destruction + logNetwork->info("Successfully closed connection to server!"); + return; } - //catch only asio exceptions - catch(const boost::system::system_error & e) + + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + logNetwork->error("Lost connection to server! Connection has been closed"); + + if(client) { - if(state == EClientState::DISCONNECTING) - { - logNetwork->info("Successfully closed connection to server, ending listening thread!"); - } - else - { - if (e.code() == boost::asio::error::eof) - logNetwork->error("Lost connection to server, ending listening thread! Connection has been closed"); - else - logNetwork->error("Lost connection to server, ending listening thread! Reason: %s", e.what()); - - if(client) - { - state = EClientState::DISCONNECTING; - - GH.dispatchMainThread([]() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("main"); - }); - } - else - { - auto lcd = new LobbyClientDisconnected(); - lcd->clientId = c->connectionID; - boost::unique_lock lock(*mx); - packsForLobbyScreen.push_back(lcd); - } - } + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("main"); + CSH->showServerError(CGI->generaltexth->translate("vcmi.server.errors.disconnected")); } + else + { + LobbyClientDisconnected lcd; + lcd.clientId = logicConnection->connectionID; + applyPackOnLobbyScreen(lcd); + } + + networkConnection.reset(); +} + +void CServerHandler::waitForServerShutdown() +{ + if (!serverRunner) + return; // may not exist for guest in MP + + serverRunner->wait(); + int exitCode = serverRunner->exitCode(); + serverRunner.reset(); + + if (exitCode == 0) + { + logNetwork->info("Server closed correctly"); + } + else + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if (getState() == EClientState::CONNECTING) + { + showServerError(CGI->generaltexth->translate("vcmi.server.errors.existingProcess")); + setState(EClientState::CONNECTION_CANCELLED); // stop attempts to reconnect + } + logNetwork->error("Error: server failed to close correctly or crashed!"); + logNetwork->error("Check log file for more info"); + } + + serverRunner.reset(); } void CServerHandler::visitForLobby(CPackForLobby & lobbyPack) { - if(applier->getApplier(typeList.getTypeID(&lobbyPack))->applyOnLobbyHandler(this, &lobbyPack)) + if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, lobbyPack)) { if(!settings["session"]["headless"].Bool()) - { - boost::unique_lock lock(*mx); - packsForLobbyScreen.push_back(&lobbyPack); - } + applyPackOnLobbyScreen(lobbyPack); } } @@ -968,64 +914,19 @@ void CServerHandler::visitForClient(CPackForClient & clientPack) client->handlePack(&clientPack); } -void CServerHandler::threadRunServer() -{ -#if !defined(VCMI_MOBILE) - setThreadName("runServer"); - const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string(); - std::string comm = VCMIDirs::get().serverPath().string() - + " --port=" + std::to_string(getHostPort()) - + " --run-by-client" - + " --uuid=" + uuid; - if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) - { - comm += " --lobby=" + settings["session"]["address"].String(); - comm += " --connections=" + settings["session"]["hostConnections"].String(); - comm += " --lobby-port=" + std::to_string(settings["session"]["port"].Integer()); - comm += " --lobby-uuid=" + settings["session"]["hostUuid"].String(); - } - - comm += " > \"" + logName + '\"'; - logGlobal->info("Server command line: %s", comm); - -#ifdef VCMI_WINDOWS - int result = -1; - const auto bufSize = ::MultiByteToWideChar(CP_UTF8, 0, comm.c_str(), comm.size(), nullptr, 0); - if(bufSize > 0) - { - std::wstring wComm(bufSize, {}); - const auto convertResult = ::MultiByteToWideChar(CP_UTF8, 0, comm.c_str(), comm.size(), &wComm[0], bufSize); - if(convertResult > 0) - result = ::_wsystem(wComm.c_str()); - else - logNetwork->error("Error " + std::to_string(GetLastError()) + ": failed to convert server launch command to wide string: " + comm); - } - else - logNetwork->error("Error " + std::to_string(GetLastError()) + ": failed to obtain buffer length to convert server launch command to wide string : " + comm); -#else - int result = std::system(comm.c_str()); -#endif - if (result == 0) - { - logNetwork->info("Server closed correctly"); - } - else - { - logNetwork->error("Error: server failed to close correctly or crashed!"); - logNetwork->error("Check %s for more info", logName); - } - onServerFinished(); -#endif -} - -void CServerHandler::onServerFinished() -{ - threadRunLocalServer.reset(); - CSH->campaignServerRestartLock.setn(false); -} void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const { - if(state != EClientState::STARTING) - c->sendPack(&pack); + if(getState() != EClientState::STARTING) + logicConnection->sendPack(&pack); +} + +bool CServerHandler::inLobbyRoom() const +{ + return CSH->serverMode == EServerMode::LOBBY_HOST || CSH->serverMode == EServerMode::LOBBY_GUEST; +} + +bool CServerHandler::inGame() const +{ + return logicConnection != nullptr; } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 92ade9162..01fa71566 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -11,6 +11,7 @@ #include "../lib/CStopWatch.h" +#include "../lib/network/NetworkInterface.h" #include "../lib/StartInfo.h" #include "../lib/CondSh.h" @@ -34,10 +35,15 @@ VCMI_LIB_NAMESPACE_END class CClient; class CBaseForLobbyApply; +class GlobalLobbyClient; +class IServerRunner; class HighScoreCalculation; class HighScoreParameter; +enum class ESelectionScreen : ui8; +enum class ELoadMode : ui8; + // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet enum class EClientState : ui8 { @@ -49,7 +55,14 @@ enum class EClientState : ui8 STARTING, // Gameplay interfaces being created, we pause netpacks retrieving GAMEPLAY, // In-game, used by some UI DISCONNECTING, // We disconnecting, drop all netpacks - CONNECTION_FAILED // We could not connect to server +}; + +enum class EServerMode : uint8_t +{ + NONE = 0, + LOCAL, // no global lobby + LOBBY_HOST, // We are hosting global server available via global lobby + LOBBY_GUEST // Connecting to a remote server via proxy provided by global lobby }; class IServerAPI @@ -72,6 +85,7 @@ public: virtual void setDifficulty(int to) const = 0; virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; virtual void setSimturnsInfo(const SimturnsInfo &) const = 0; + virtual void setExtraOptionsInfo(const ExtraOptionsInfo & info) const = 0; virtual void sendMessage(const std::string & txt) const = 0; virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it? virtual void sendStartGame(bool allowOnlyAI = false) const = 0; @@ -79,58 +93,68 @@ public: }; /// structure to handle running server and connecting to it -class CServerHandler : public IServerAPI, public LobbyInfo +class CServerHandler final : public IServerAPI, public LobbyInfo, public INetworkClientListener, public INetworkTimerListener, boost::noncopyable { friend class ApplyOnLobbyHandlerNetPackVisitor; - - std::shared_ptr> applier; - std::shared_ptr mx; - std::list packsForLobbyScreen; //protected by mx - + std::unique_ptr networkHandler; + std::shared_ptr networkConnection; + std::unique_ptr lobbyClient; + std::unique_ptr> applier; + std::unique_ptr serverRunner; std::shared_ptr mapToStart; - - std::vector myNames; - + std::vector localPlayerNames; std::shared_ptr highScoreCalc; - void threadHandleConnection(); - void threadRunServer(); - void onServerFinished(); + boost::thread threadNetwork; + + std::atomic state; + + void threadRunNetwork(); + void waitForServerShutdown(); + void sendLobbyPack(const CPackForLobby & pack) const override; + void onPacketReceived(const NetworkConnectionPtr &, const std::vector & message) override; + void onConnectionFailed(const std::string & errorMessage) override; + void onConnectionEstablished(const NetworkConnectionPtr &) override; + void onDisconnected(const NetworkConnectionPtr &, const std::string & errorMessage) override; + void onTimer() override; + + void applyPackOnLobbyScreen(CPackForLobby & pack); + + std::string serverHostname; + ui16 serverPort; + + bool isServerLocal() const; + public: - std::atomic state; + /// High-level connection overlay that is capable of (de)serializing network data + std::shared_ptr logicConnection; + //////////////////// // FIXME: Bunch of crutches to glue it all together // For starting non-custom campaign and continue to next mission std::shared_ptr campaignStateToSend; - ui8 screenType; // To create lobby UI only after server is setup - ui8 loadMode; // For saves filtering in SelectionTab + ESelectionScreen screenType; // To create lobby UI only after server is setup + EServerMode serverMode; + ELoadMode loadMode; // For saves filtering in SelectionTab //////////////////// std::unique_ptr th; - std::shared_ptr threadRunLocalServer; - - std::shared_ptr c; - CClient * client; - - CondSh campaignServerRestartLock; - - static const std::string localhostAddress; + std::unique_ptr client; CServerHandler(); + ~CServerHandler(); - std::string getHostAddress() const; - ui16 getHostPort() const; + void resetStateForLobby(EStartMode mode, ESelectionScreen screen, EServerMode serverMode, const std::vector & playerNames); + void startLocalServerAndConnect(bool connectToLobby); + void connectToServer(const std::string & addr, const ui16 port); - void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); - void startLocalServerAndConnect(); - void justConnectToServer(const std::string & addr, const ui16 port); - void applyPacksOnLobbyScreen(); - void stopServerConnection(); + GlobalLobbyClient & getGlobalLobby(); + INetworkHandler & getNetworkHandler(); // Helpers for lobby state access std::set getHumanColors(); @@ -138,12 +162,21 @@ public: bool isMyColor(PlayerColor color) const; ui8 myFirstId() const; // Used by chat only! - bool isServerLocal() const; + EClientState getState() const; + void setState(EClientState newState); + bool isHost() const; bool isGuest() const; + bool inLobbyRoom() const; + bool inGame() const; - static ui16 getDefaultPort(); - static std::string getDefaultPortStr(); + const std::string & getCurrentHostname() const; + const std::string & getLocalHostname() const; + const std::string & getRemoteHostname() const; + + ui16 getCurrentPort() const; + ui16 getLocalPort() const; + ui16 getRemotePort() const; // Lobby server API for UI void sendClientConnecting() const override; @@ -158,6 +191,7 @@ public: void setDifficulty(int to) const override; void setTurnTimerInfo(const TurnTimerInfo &) const override; void setSimturnsInfo(const SimturnsInfo &) const override; + void setExtraOptionsInfo(const ExtraOptionsInfo &) const override; void sendMessage(const std::string & txt) const override; void sendGuiAction(ui8 action) const override; void sendRestartGame() const override; @@ -168,15 +202,14 @@ public: void debugStartTest(std::string filename, bool save = false); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); - void endGameplay(bool closeConnection = true, bool restart = false); + void endGameplay(); + void restartGameplay(); void startCampaignScenario(HighScoreParameter param, std::shared_ptr cs = {}); void showServerError(const std::string & txt) const; // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle int howManyPlayerInterfaces(); - ui8 getLoadMode(); - - void restoreLastSession(); + ELoadMode getLoadMode(); void visitForLobby(CPackForLobby & lobbyPack); void visitForClient(CPackForClient & clientPack); diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index cd3cc8b6c..4595e5977 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -41,8 +41,11 @@ extern "C" { static int lodRead(void* opaque, uint8_t* buf, int size) { auto video = reinterpret_cast(opaque); + int bytes = static_cast(video->data->read(buf, size)); + if(bytes == 0) + return AVERROR_EOF; - return static_cast(video->data->read(buf, size)); + return bytes; } static si64 lodSeek(void * opaque, si64 pos, int whence) @@ -59,8 +62,11 @@ static si64 lodSeek(void * opaque, si64 pos, int whence) static int lodReadAudio(void* opaque, uint8_t* buf, int size) { auto video = reinterpret_cast(opaque); + int bytes = static_cast(video->dataAudio->read(buf, size)); + if(bytes == 0) + return AVERROR_EOF; - return static_cast(video->dataAudio->read(buf, size)); + return bytes; } static si64 lodSeekAudio(void * opaque, si64 pos, int whence) @@ -95,8 +101,8 @@ bool CVideoPlayer::open(const VideoPath & fname, bool scale) } // loop = to loop through the video -// useOverlay = directly write to the screen. -bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale) +// overlay = directly write to the screen. +bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool overlay, bool scale) { close(); @@ -193,7 +199,7 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla } // Allocate a place to put our YUV image on that screen - if (useOverlay) + if (overlay) { texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h); } @@ -609,8 +615,16 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath return dat; } +Point CVideoPlayer::size() +{ + if(frame) + return Point(frame->width, frame->height); + else + return Point(0, 0); +} + // Plays a video. Only works for overlays. -bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) +bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey, bool overlay) { // Note: either the windows player or the linux player is // broken. Compensate here until the bug is found. @@ -620,6 +634,8 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) pos.y = y; frameTime = 0.0; + auto lastTimePoint = boost::chrono::steady_clock::now(); + while(nextFrame()) { if(stopOnKey) @@ -631,6 +647,14 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) SDL_Rect rect = CSDL_Ext::toSDL(pos); + if(overlay) + { + SDL_RenderFillRect(mainRenderer, &rect); + } + else + { + SDL_RenderClear(mainRenderer); + } SDL_RenderCopy(mainRenderer, texture, nullptr, &rect); SDL_RenderPresent(mainRenderer); @@ -639,19 +663,43 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) #else auto packet_duration = frame->duration; #endif - double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base); - uint32_t timeToSleepMillisec = 1000 * (frameDurationSec); + // Framerate delay + double targetFrameTimeSeconds = packet_duration * av_q2d(format->streams[stream]->time_base); + auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * (targetFrameTimeSeconds))); - boost::this_thread::sleep_for(boost::chrono::milliseconds(timeToSleepMillisec)); + auto timePointAfterPresent = boost::chrono::steady_clock::now(); + auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); + + if (targetFrameTime > timeSpentBusy) + boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + + lastTimePoint = boost::chrono::steady_clock::now(); } return true; } -bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale) +bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) { + bool scale; + bool stopOnKey; + bool overlay; + + switch(videoType) + { + case EVideoType::INTRO: + stopOnKey = true; + scale = true; + overlay = false; + break; + case EVideoType::SPELLBOOK: + default: + stopOnKey = false; + scale = false; + overlay = true; + } open(name, false, true, scale); - bool ret = playVideo(x, y, stopOnKey); + bool ret = playVideo(x, y, stopOnKey, overlay); close(); return ret; } diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index e39d9445c..89f8016a2 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -15,6 +15,12 @@ struct SDL_Surface; struct SDL_Texture; +enum class EVideoType : ui8 +{ + INTRO = 0, // use entire window: stopOnKey = true, scale = true, overlay = false + SPELLBOOK // overlay video: stopOnKey = false, scale = false, overlay = true +}; + class IVideoPlayer : boost::noncopyable { public: @@ -31,15 +37,17 @@ public: class IMainVideoPlayer : public IVideoPlayer { public: + virtual ~IMainVideoPlayer() = default; virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr){} - virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) + virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) { return false; } virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; + virtual Point size() { return Point(0, 0); }; }; -class CEmptyVideoPlayer : public IMainVideoPlayer +class CEmptyVideoPlayer final : public IMainVideoPlayer { public: int curFrame() const override {return -1;}; @@ -64,7 +72,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CInputStream; VCMI_LIB_NAMESPACE_END -class CVideoPlayer : public IMainVideoPlayer +class CVideoPlayer final : public IMainVideoPlayer { int stream; // stream index in video AVFormatContext *format; @@ -88,7 +96,7 @@ class CVideoPlayer : public IMainVideoPlayer double frameTime; bool doLoop; // loop through video - bool playVideo(int x, int y, bool stopOnKey); + bool playVideo(int x, int y, bool stopOnKey, bool overlay); bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); public: CVideoPlayer(); @@ -104,10 +112,12 @@ public: void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) - bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; + bool openAndPlayVideo(const VideoPath & name, int x, int y, EVideoType videoType) override; std::pair, si64> getAudio(const VideoPath & videoToOpen) override; + Point size() override; + //TODO: bool wait() override {return false;}; int curFrame() const override {return -1;}; diff --git a/client/Client.cpp b/client/Client.cpp index c6e99d405..ee14b9dae 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -24,16 +24,18 @@ #include "../CCallback.h" #include "../lib/CConfigHandler.h" #include "../lib/gameState/CGameState.h" +#include "../lib/CPlayerState.h" #include "../lib/CThreadHelper.h" #include "../lib/VCMIDirs.h" #include "../lib/UnlockGuard.h" #include "../lib/battle/BattleInfo.h" #include "../lib/serializer/BinaryDeserializer.h" +#include "../lib/serializer/BinarySerializer.h" +#include "../lib/serializer/Connection.h" #include "../lib/mapping/CMapService.h" #include "../lib/pathfinder/CGPathNode.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypes.h" -#include "../lib/serializer/Connection.h" +#include "../lib/registerTypes/RegisterTypesClientPacks.h" #include #include @@ -44,10 +46,6 @@ #ifdef VCMI_ANDROID #include "lib/CAndroidVMHelper.h" - -#ifndef SINGLE_PROCESS_APP -std::atomic_bool androidTestServerReadyFlag; -#endif #endif ThreadSafeVector CClient::waitingRequest; @@ -57,8 +55,8 @@ template class CApplyOnCL; class CBaseForCLApply { public: - virtual void applyOnClAfter(CClient * cl, void * pack) const =0; - virtual void applyOnClBefore(CClient * cl, void * pack) const =0; + virtual void applyOnClAfter(CClient * cl, CPack * pack) const =0; + virtual void applyOnClBefore(CClient * cl, CPack * pack) const =0; virtual ~CBaseForCLApply(){} template static CBaseForCLApply * getApplier(const U * t = nullptr) @@ -70,13 +68,13 @@ public: template class CApplyOnCL : public CBaseForCLApply { public: - void applyOnClAfter(CClient * cl, void * pack) const override + void applyOnClAfter(CClient * cl, CPack * pack) const override { T * ptr = static_cast(pack); ApplyClientNetPackVisitor visitor(*cl, *cl->gameState()); ptr->visit(visitor); } - void applyOnClBefore(CClient * cl, void * pack) const override + void applyOnClBefore(CClient * cl, CPack * pack) const override { T * ptr = static_cast(pack); ApplyFirstClientNetPackVisitor visitor(*cl, *cl->gameState()); @@ -87,12 +85,12 @@ public: template<> class CApplyOnCL: public CBaseForCLApply { public: - void applyOnClAfter(CClient * cl, void * pack) const override + void applyOnClAfter(CClient * cl, CPack * pack) const override { logGlobal->error("Cannot apply on CL plain CPack!"); assert(0); } - void applyOnClBefore(CClient * cl, void * pack) const override + void applyOnClBefore(CClient * cl, CPack * pack) const override { logGlobal->error("Cannot apply on CL plain CPack!"); assert(0); @@ -137,16 +135,11 @@ CClient::CClient() { waitingRequest.clear(); applier = std::make_shared>(); - registerTypesClientPacks1(*applier); - registerTypesClientPacks2(*applier); - IObjectInterface::cb = this; + registerTypesClientPacks(*applier); gs = nullptr; } -CClient::~CClient() -{ - IObjectInterface::cb = nullptr; -} +CClient::~CClient() = default; const Services * CClient::services() const { @@ -177,8 +170,9 @@ void CClient::newGame(CGameState * initializedGameState) { CSH->th->update(); CMapService mapService; - gs = initializedGameState ? initializedGameState : new CGameState(); - gs->preInit(VLC); + assert(initializedGameState); + gs = initializedGameState; + gs->preInit(VLC, this); logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); if(!initializedGameState) { @@ -200,7 +194,7 @@ void CClient::loadGame(CGameState * initializedGameState) logNetwork->info("Game state was transferred over network, loading."); gs = initializedGameState; - gs->preInit(VLC); + gs->preInit(VLC, this); gs->updateOnLoad(CSH->si.get()); logNetwork->info("Game loaded, initialize interfaces."); @@ -228,7 +222,7 @@ void CClient::loadGame(CGameState * initializedGameState) throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); std::unique_ptr loader (new CLoadFile(clientSaveName)); - serialize(loader->serializer, loader->serializer.fileVersion); + serialize(loader->serializer, loader->serializer.version); logNetwork->info("Client data loaded."); } @@ -241,7 +235,7 @@ void CClient::loadGame(CGameState * initializedGameState) initPlayerInterfaces(); } -void CClient::serialize(BinarySerializer & h, const int version) +void CClient::serialize(BinarySerializer & h) { assert(h.saving); ui8 players = static_cast(playerint.size()); @@ -254,20 +248,17 @@ void CClient::serialize(BinarySerializer & h, const int version) h & i->first; h & i->second->dllName; h & i->second->human; - i->second->saveGame(h, version); + i->second->saveGame(h); } #if SCRIPTING_ENABLED - if(version >= 800) - { - JsonNode scriptsState; - clientScripts->serializeState(h.saving, scriptsState); - h & scriptsState; - } + JsonNode scriptsState; + clientScripts->serializeState(h.saving, scriptsState); + h & scriptsState; #endif } -void CClient::serialize(BinaryDeserializer & h, const int version) +void CClient::serialize(BinaryDeserializer & h) { assert(!h.saving); ui8 players = 0; @@ -303,7 +294,7 @@ void CClient::serialize(BinaryDeserializer & h, const int version) bool shouldResetInterface = true; // Client no longer handle this player at all - if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid)) + if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), pid)) { logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid); } @@ -323,7 +314,7 @@ void CClient::serialize(BinaryDeserializer & h, const int version) // loadGame needs to be called after initGameInterface to load paths correctly // initGameInterface is called in installNewPlayerInterface - nInt->loadGame(h, version); + nInt->loadGame(h); if (shouldResetInterface) { @@ -370,7 +361,7 @@ void CClient::endGame() logNetwork->info("Ending current game!"); removeGUI(); - vstd::clear_pointer(const_cast(CGI)->mh); + CGI->mh.reset(); vstd::clear_pointer(gs); logNetwork->info("Deleted mapHandler and gameState."); @@ -392,7 +383,7 @@ void CClient::initMapHandler() // During loading CPlayerInterface from serialized state it's depend on MH if(!settings["session"]["headless"].Bool()) { - const_cast(CGI)->mh = new CMapHandler(gs->map); + CGI->mh = std::make_shared(gs->map); logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); } @@ -403,7 +394,7 @@ void CClient::initPlayerEnvironments() { playerEnvironments.clear(); - auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID); + auto allPlayers = CSH->getAllClientPlayers(CSH->logicConnection->connectionID); bool hasHumanPlayer = false; for(auto & color : allPlayers) { @@ -414,7 +405,7 @@ void CClient::initPlayerEnvironments() hasHumanPlayer = true; } - if(!hasHumanPlayer) + if(!hasHumanPlayer && !settings["session"]["headless"].Bool()) { Settings session = settings.write["session"]; session["spectate"].Bool() = true; @@ -433,13 +424,13 @@ void CClient::initPlayerInterfaces() for(auto & playerInfo : gs->scenarioOps->playerInfos) { PlayerColor color = playerInfo.first; - if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) + if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), color)) continue; if(!vstd::contains(playerint, color)) { logNetwork->info("Preparing interface for player %s", color.toString()); - if(playerInfo.second.isControlledByAI()) + if(playerInfo.second.isControlledByAI() || settings["session"]["onlyai"].Bool()) { bool alliedToHuman = false; for(auto & allyInfo : gs->scenarioOps->playerInfos) @@ -463,7 +454,7 @@ void CClient::initPlayerInterfaces() installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); } - if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) + if(CSH->getAllClientPlayers(CSH->logicConnection->connectionID).count(PlayerColor::NEUTRAL)) installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); @@ -523,27 +514,26 @@ void CClient::installNewBattleInterface(std::shared_ptr ba void CClient::handlePack(CPack * pack) { - CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier + CBaseForCLApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier if(apply) { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); apply->applyOnClBefore(this, pack); - logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); + logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name()); gs->apply(pack); - logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name()); + logNetwork->trace("\tApplied on gs: %s", typeid(pack).name()); apply->applyOnClAfter(this, pack); - logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name()); + logNetwork->trace("\tMade second apply on cl: %s", typeid(pack).name()); } else { - logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); + logNetwork->error("Message %s cannot be applied, cannot find applier!", typeid(pack).name()); } delete pack; } int CClient::sendRequest(const CPackForServer * request, PlayerColor player) { - static ui32 requestCounter = 0; + static ui32 requestCounter = 1; ui32 requestID = requestCounter++; logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); @@ -551,7 +541,7 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player) waitingRequest.pushBack(requestID); request->requestID = requestID; request->player = player; - CSH->c->sendPack(request); + CSH->logicConnection->sendPack(request); if(vstd::contains(playerint, player)) playerint[player]->requestSent(request, requestID); @@ -560,18 +550,17 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player) void CClient::battleStarted(const BattleInfo * info) { + std::shared_ptr att; + std::shared_ptr def; + auto & leftSide = info->sides[0]; + auto & rightSide = info->sides[1]; + for(auto & battleCb : battleCallbacks) { - if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) - || !battleCb.first.isValidPlayer()) - { + if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color) battleCb.second->onBattleStarted(info); - } } - std::shared_ptr att, def; - auto & leftSide = info->sides[0], & rightSide = info->sides[1]; - //If quick combat is not, do not prepare interfaces for battleint auto callBattleStart = [&](PlayerColor color, ui8 side) { @@ -592,14 +581,26 @@ void CClient::battleStarted(const BattleInfo * info) def = std::dynamic_pointer_cast(playerint[rightSide.color]); //Remove player interfaces for auto battle (quickCombat option) - if(att && att->isAutoFightOn) + if((att && att->isAutoFightOn) || (def && def->isAutoFightOn)) { - if (att->cb->getBattle(info->battleID)->battleGetTacticDist()) + auto endTacticPhaseIfEligible = [info](const CPlayerInterface * interface) { - auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID); - auto action = BattleAction::makeEndOFTacticPhase(*side); - att->cb->battleMakeTacticAction(info->battleID, action); - } + if (interface->cb->getBattle(info->battleID)->battleGetTacticDist()) + { + auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID); + + if(interface->playerID == info->sides[info->tacticsSide].color) + { + auto action = BattleAction::makeEndOFTacticPhase(*side); + interface->cb->battleMakeTacticAction(info->battleID, action); + } + } + }; + + if(att && att->isAutoFightOn) + endTacticPhaseIfEligible(att.get()); + else // def && def->isAutoFightOn + endTacticPhaseIfEligible(def.get()); att.reset(); def.reset(); @@ -641,6 +642,9 @@ void CClient::battleFinished(const BattleID & battleID) void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) { + if (battleints.count(color) == 0) + return; // not our combat in MP + auto battleint = battleints.at(color); if (!battleint->human) @@ -670,7 +674,7 @@ std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h if(iter == std::end(pathCache)) { - std::shared_ptr paths = std::make_shared(getMapSize(), h); + auto paths = std::make_shared(getMapSize(), h); gs->calculatePaths(h, *paths.get()); @@ -710,22 +714,6 @@ void CClient::removeGUI() const } #ifdef VCMI_ANDROID -#ifndef SINGLE_PROCESS_APP -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jclass cls) -{ - logNetwork->info("Received server closed signal"); - if (CSH) { - CSH->campaignServerRestartLock.setn(false); - } -} - -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jclass cls) -{ - logNetwork->info("Received server ready signal"); - androidTestServerReadyFlag.store(true); -} -#endif - extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) { logGlobal->info("Received emergency save game request"); diff --git a/client/Client.h b/client/Client.h index 2be88e55a..5fe64b6b1 100644 --- a/client/Client.h +++ b/client/Client.h @@ -131,8 +131,8 @@ public: void newGame(CGameState * gameState); void loadGame(CGameState * gameState); - void serialize(BinarySerializer & h, const int version); - void serialize(BinaryDeserializer & h, const int version); + void serialize(BinarySerializer & h); + void serialize(BinaryDeserializer & h); void save(const std::string & fname); void endGame(); @@ -162,8 +162,9 @@ public: void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}; - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override {}; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; + void giveExperience(const CGHeroInstance * hero, TExpType val) override {}; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; @@ -172,7 +173,7 @@ public: void showTeleportDialog(TeleportDialog * iw) override {}; void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; void giveResource(PlayerColor player, GameResID which, int val) override {}; - virtual void giveResources(PlayerColor player, TResources resources) override {}; + void giveResources(PlayerColor player, TResources resources) override {}; void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; @@ -188,8 +189,7 @@ public: void removeAfterVisit(const CGObjectInstance * object) override {}; bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}; bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} - void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {}; + bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override {return false;}; void removeArtifact(const ArtifactLocation & al) override {}; bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; @@ -202,6 +202,7 @@ public: bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; void giveHeroBonus(GiveBonus * bonus) override {}; void setMovePoints(SetMovePoints * smp) override {}; + void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override {}; void setManaPoints(ObjectInstanceID hid, int val) override {}; void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}; void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}; @@ -212,7 +213,8 @@ public: void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, ETileVisibility mode) override {} - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} + void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {}; + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {}; void showInfoDialog(InfoWindow * iw) override {}; void showInfoDialog(const std::string & msg, PlayerColor player) override {}; diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 412131142..cf02eead0 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -203,7 +203,7 @@ void ClientCommandManager::handleConvertTextCommand() try { // load and drop loaded map - we only need loader to run over all maps - mapService.loadMap(mapName); + mapService.loadMap(mapName, nullptr); } catch(std::exception & e) { @@ -216,7 +216,7 @@ void ClientCommandManager::handleConvertTextCommand() { auto state = CampaignHandler::getCampaign(campaignName.getName()); for (auto const & part : state->allScenarios()) - state->getMap(part); + state->getMap(part, nullptr); } VLC->generaltexth->dumpAllTexts(); @@ -248,13 +248,13 @@ void ClientCommandManager::handleGetConfigCommand() { const JsonNode& object = nameAndObject.second; - std::string name = ModUtility::makeFullIdentifier(object.meta, contentName, nameAndObject.first); + std::string name = ModUtility::makeFullIdentifier(object.getModScope(), contentName, nameAndObject.first); boost::algorithm::replace_all(name, ":", "_"); const boost::filesystem::path filePath = contentOutPath / (name + ".json"); std::ofstream file(filePath.c_str()); - file << object.toJson(); + file << object.toString(); } } } @@ -272,7 +272,7 @@ void ClientCommandManager::handleGetScriptsCommand() boost::filesystem::create_directories(outPath); - for(auto & kv : VLC->scriptHandler->objects) + for(const auto & kv : VLC->scriptHandler->objects) { std::string name = kv.first; boost::algorithm::replace_all(name,":","_"); @@ -358,7 +358,7 @@ void ClientCommandManager::handleBonusesCommand(std::istringstream & singleWordB auto format = [outputFormat](const BonusList & b) -> std::string { if(outputFormat == "json") - return b.toJsonNode().toJson(true); + return b.toJsonNode().toCompactString(); std::ostringstream ss; ss << b; @@ -379,7 +379,8 @@ void ClientCommandManager::handleBonusesCommand(std::istringstream & singleWordB void ClientCommandManager::handleTellCommand(std::istringstream& singleWordBuffer) { std::string what; - int id1, id2; + int id1; + int id2; singleWordBuffer >> what >> id1 >> id2; if(what == "hs") @@ -399,7 +400,8 @@ void ClientCommandManager::handleMpCommand() void ClientCommandManager::handleSetCommand(std::istringstream& singleWordBuffer) { - std::string what, value; + std::string what; + std::string value; singleWordBuffer >> what; Settings config = settings.write["session"][what]; diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index 08c0908e2..51812dedc 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -118,16 +118,16 @@ public: { } - virtual void visitChangeObjPos(ChangeObjPos & pack) override; - virtual void visitRemoveObject(RemoveObject & pack) override; - virtual void visitTryMoveHero(TryMoveHero & pack) override; - virtual void visitGiveHero(GiveHero & pack) override; - virtual void visitBattleStart(BattleStart & pack) override; - virtual void visitBattleNextRound(BattleNextRound & pack) override; - virtual void visitBattleUpdateGateState(BattleUpdateGateState & pack) override; - virtual void visitBattleResult(BattleResult & pack) override; - virtual void visitBattleStackMoved(BattleStackMoved & pack) override; - virtual void visitBattleAttack(BattleAttack & pack) override; - virtual void visitStartAction(StartAction & pack) override; - virtual void visitSetObjectProperty(SetObjectProperty & pack) override; + void visitChangeObjPos(ChangeObjPos & pack) override; + void visitRemoveObject(RemoveObject & pack) override; + void visitTryMoveHero(TryMoveHero & pack) override; + void visitGiveHero(GiveHero & pack) override; + void visitBattleStart(BattleStart & pack) override; + void visitBattleNextRound(BattleNextRound & pack) override; + void visitBattleUpdateGateState(BattleUpdateGateState & pack) override; + void visitBattleResult(BattleResult & pack) override; + void visitBattleStackMoved(BattleStackMoved & pack) override; + void visitBattleAttack(BattleAttack & pack) override; + void visitStartAction(StartAction & pack) override; + void visitSetObjectProperty(SetObjectProperty & pack) override; }; diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 681b8ea4c..6b1152f60 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -100,7 +100,8 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel } } - assert(0); // exit not found? How? + // may happen when hero has path but does not moves alongside it + // for example, while standing on teleporter set path that does not leads throught teleporter and press space LOCPLINT->cb->selectionMade(-1, askID); return; } @@ -359,7 +360,7 @@ void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & p { stopMovementSound(); logGlobal->trace("Requesting hero teleportation to %s", nextNode.coord.toString()); - LOCPLINT->cb->moveHero(h, nextCoord, false); + LOCPLINT->cb->moveHero(h, h->pos, false); return; } else diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index 7e19e614b..b3996cce7 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -32,11 +32,12 @@ public: bool getResult() const { return result; } - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyUpdateState(LobbyUpdateState & pack) override; }; class ApplyOnLobbyScreenNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) @@ -51,11 +52,11 @@ public: { } - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override; - virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; - virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override; - virtual void visitLobbyShowMessage(LobbyShowMessage & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyChatMessage(LobbyChatMessage & pack) override; + void visitLobbyGuiAction(LobbyGuiAction & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; + void visitLobbyUpdateState(LobbyUpdateState & pack) override; + void visitLobbyShowMessage(LobbyShowMessage & pack) override; }; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 26183016b..ff6c037b1 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -27,8 +27,8 @@ #include "../CCallback.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/FileInfo.h" -#include "../lib/serializer/Connection.h" #include "../lib/serializer/BinarySerializer.h" +#include "../lib/serializer/Connection.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/VCMI_Lib.h" @@ -39,6 +39,7 @@ #include "../lib/StartInfo.h" #include "../lib/CConfigHandler.h" #include "../lib/mapObjects/CGMarket.h" +#include "../lib/mapObjects/CGTownInstance.h" #include "../lib/gameState/CGameState.h" #include "../lib/CStack.h" #include "../lib/battle/BattleInfo.h" @@ -156,6 +157,9 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) const CGHeroInstance *h = cl.getHero(pack.hid); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h); + if(settings["session"]["headless"].Bool()) + return; + for (auto window : GH.windows().findWindows()) window->heroManaPointsChanged(h); } @@ -267,14 +271,14 @@ void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalance void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactPut, pack.al); if(pack.askAssemble) - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al); } void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactRemoved, pack.al); } void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) @@ -286,9 +290,9 @@ void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst); }; - moveArtifact(pack.src.owningPlayer()); - if(pack.src.owningPlayer() != pack.dst.owningPlayer()) - moveArtifact(pack.dst.owningPlayer()); + moveArtifact(cl.getOwner(pack.src.artHolder)); + if(cl.getOwner(pack.src.artHolder) != cl.getOwner(pack.dst.artHolder)) + moveArtifact(cl.getOwner(pack.dst.artHolder)); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings } @@ -301,13 +305,13 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) { auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); - MoveArtifact ma(&srcLoc, &dstLoc, false); + MoveArtifact ma(&srcLoc, &dstLoc, pack.askAssemble); visitMoveArtifact(ma); } }; - auto srcOwner = std::get>(pack.srcArtHolder)->tempOwner; - auto dstOwner = std::get>(pack.dstArtHolder)->tempOwner; + auto srcOwner = cl.getOwner(pack.srcArtHolder); + auto dstOwner = cl.getOwner(pack.dstArtHolder); // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); @@ -321,14 +325,14 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings } void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings } @@ -350,15 +354,16 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) cl.invalidatePaths(); switch(pack.who) { - case GiveBonus::ETarget::HERO: + case GiveBonus::ETarget::OBJECT: { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); + const CGHeroInstance *h = gs.getHero(pack.id.as()); + if (h) + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); } break; case GiveBonus::ETarget::PLAYER: { - callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); + callInterfaceIfPresent(cl, pack.id.as(), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); } break; } @@ -419,7 +424,7 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface cl.initPlayerEnvironments(); initInterfaces(); } - else if(pack.playerConnectionId == CSH->c->connectionID) + else if(pack.playerConnectionId == CSH->logicConnection->connectionID) { plSettings.connectedPlayerIDs.insert(pack.playerConnectionId); cl.playerint.clear(); @@ -433,16 +438,17 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) cl.invalidatePaths(); switch(pack.who) { - case GiveBonus::ETarget::HERO: + case GiveBonus::ETarget::OBJECT: { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.whoID)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); + const CGHeroInstance *h = gs.getHero(pack.whoID.as()); + if (h) + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); } break; case GiveBonus::ETarget::PLAYER: { //const PlayerState *p = gs.getPlayerState(pack.id); - callInterfaceIfPresent(cl, PlayerColor(pack.whoID), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); + callInterfaceIfPresent(cl, pack.whoID.as(), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); } break; } @@ -464,7 +470,8 @@ void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) i->second->objectRemoved(o, pack.initiator); } - CGI->mh->waitForOngoingAnimations(); + if(CGI->mh) + CGI->mh->waitForOngoingAnimations(); } void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) @@ -550,9 +557,11 @@ void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack) } // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town, town->getOwner()); - CGI->mh->onObjectInstantAdd(town, town->getOwner()); - + if(CGI->mh) + { + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); + } } void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) { @@ -563,8 +572,11 @@ void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) } // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town, town->getOwner()); - CGI->mh->onObjectInstantAdd(town, town->getOwner()); + if(CGI->mh) + { + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); + } } void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack) @@ -604,7 +616,7 @@ void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) { CGHeroInstance *h = gs.map->heroesOnMap.back(); - if(h->subID != pack.hid) + if(h->getHeroType() != pack.hid) { logNetwork->error("Something wrong with hero recruited!"); } @@ -648,7 +660,7 @@ void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & } // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER) + if (pack.what == ObjProperty::OWNER && CGI->mh) { auto object = gs.getObjInstance(pack.id); CGI->mh->onObjectInstantRemove(object, object->getOwner()); @@ -665,7 +677,7 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) } // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER) + if (pack.what == ObjProperty::OWNER && CGI->mh) { auto object = gs.getObjInstance(pack.id); CGI->mh->onObjectInstantAdd(object, object->getOwner()); @@ -899,7 +911,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack) void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) { - logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString()); + logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.unitTimer, pack.player.toString()); } void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) @@ -946,7 +958,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) case EOpenWindowMode::SHIPYARD_WINDOW: { assert(pack.queryID == QueryID::NONE); - const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.object))); + const auto * sy = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy); } break; @@ -962,7 +974,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) case EOpenWindowMode::UNIVERSITY_WINDOW: { //displays University window (when hero enters University on adventure map) - const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.object))); + const auto * market = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID); } @@ -972,7 +984,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) //displays Thieves' Guild window (when hero enters Den of Thieves) const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); - const IMarket *market = IMarket::castFrom(obj); + const auto *market = dynamic_cast(obj); callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID); } break; @@ -1020,7 +1032,9 @@ void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack) if(gs.isVisible(obj, i->first)) i->second->newObject(obj); } - CGI->mh->waitForOngoingAnimations(); + + if(CGI->mh) + CGI->mh->waitForOngoingAnimations(); } void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack) diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index dc3632757..b36f2f305 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -15,11 +15,17 @@ #include "lobby/OptionsTab.h" #include "lobby/RandomMapTab.h" +#include "lobby/TurnOptionsTab.h" +#include "lobby/ExtraOptionsTab.h" #include "lobby/SelectionTab.h" #include "lobby/CBonusSelection.h" +#include "globalLobby/GlobalLobbyWindow.h" +#include "globalLobby/GlobalLobbyServerSetup.h" +#include "globalLobby/GlobalLobbyClient.h" #include "CServerHandler.h" #include "CGameInfo.h" +#include "Client.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" #include "widgets/Buttons.h" @@ -34,26 +40,50 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon result = false; // Check if it's LobbyClientConnected for our client - if(pack.uuid == handler.c->uuid) + if(pack.uuid == handler.logicConnection->uuid) { - handler.c->connectionID = pack.clientId; + handler.logicConnection->connectionID = pack.clientId; if(handler.mapToStart) + { handler.setMapInfo(handler.mapToStart); + } else if(!settings["session"]["headless"].Bool()) - GH.windows().createAndPushWindow(static_cast(handler.screenType)); - handler.state = EClientState::LOBBY; + { + if (GH.windows().topWindow()) + GH.windows().popWindows(1); + + if (!GH.windows().findWindows().empty()) + { + assert(handler.serverMode == EServerMode::LOBBY_HOST); + // announce opened game room + // TODO: find better approach? + int roomType = settings["lobby"]["roomType"].Integer(); + + if (roomType != 0) + handler.getGlobalLobby().sendOpenPrivateRoom(); + else + handler.getGlobalLobby().sendOpenPublicRoom(); + } + + while (!GH.windows().findWindows().empty()) + { + // if global lobby is open, pop all dialogs on top of it as well as lobby itself + GH.windows().popWindows(1); + } + + GH.windows().createAndPushWindow(handler.screenType); + } + handler.setState(EClientState::LOBBY); } } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { - if(pack.clientId != pack.c->connectionID) + if(pack.clientId != handler.logicConnection->connectionID) { result = false; return; } - - handler.stopServerConnection(); } void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) @@ -72,7 +102,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & lobby->card->chat->addNewMessage(pack.playerName + ": " + pack.message); lobby->card->setChat(true); if(lobby->buttonChat) - lobby->buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); + lobby->buttonChat->setTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); } } @@ -95,33 +125,40 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack case LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS: lobby->toggleTab(lobby->tabRand); break; + case LobbyGuiAction::OPEN_TURN_OPTIONS: + lobby->toggleTab(lobby->tabTurnOptions); + break; + case LobbyGuiAction::OPEN_EXTRA_OPTIONS: + lobby->toggleTab(lobby->tabExtraOptions); + break; } } -void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { - if(handler.state == EClientState::GAMEPLAY) - { - handler.endGameplay(pack.closeConnection, pack.restart); - } - - if(pack.restart) - { - if (handler.validateGameStart()) - handler.sendStartGame(); - } + assert(handler.getState() == EClientState::GAMEPLAY); + + handler.restartGameplay(); + handler.sendStartGame(); +} + +void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) +{ + handler.client = std::make_unique(); + handler.logicConnection->enterLobbyConnectionMode(); + handler.logicConnection->setCallback(handler.client.get()); } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) { - if(pack.clientId != -1 && pack.clientId != handler.c->connectionID) + if(pack.clientId != -1 && pack.clientId != handler.logicConnection->connectionID) { result = false; return; } - handler.state = EClientState::STARTING; - if(handler.si->mode != StartInfo::LOAD_GAME || pack.clientId == handler.c->connectionID) + handler.setState(EClientState::STARTING); + if(handler.si->mode != EStartMode::LOAD_GAME || pack.clientId == handler.logicConnection->connectionID) { auto modeBackup = handler.si->mode; handler.si = pack.initializedStartInfo; @@ -166,7 +203,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & if(!lobby) //stub: ignore message for game mode return; - if(!lobby->bonusSel && handler.si->campState && handler.state == EClientState::LOBBY_CAMPAIGN) + if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN) { lobby->bonusSel = std::make_shared(); GH.windows().pushWindow(lobby->bonusSel); diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 5fb7ed65f..03e948adb 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -204,7 +204,7 @@ const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index) { if(index < wanderingHeroes.size()) return wanderingHeroes[index]; - return nullptr; + throw std::runtime_error("No hero with index " + std::to_string(index)); } void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero) @@ -235,10 +235,12 @@ void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) setSelection(ownedTowns.front()); } -void PlayerLocalState::swapWanderingHero(int pos1, int pos2) +void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2) { assert(wanderingHeroes[pos1] && wanderingHeroes[pos2]); - std::swap(wanderingHeroes[pos1], wanderingHeroes[pos2]); + std::swap(wanderingHeroes.at(pos1), wanderingHeroes.at(pos2)); + + adventureInt->onHeroOrderChanged(); } const std::vector & PlayerLocalState::getOwnedTowns() @@ -250,7 +252,7 @@ const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index) { if(index < ownedTowns.size()) return ownedTowns[index]; - return nullptr; + throw std::runtime_error("No town with index " + std::to_string(index)); } void PlayerLocalState::addOwnedTown(const CGTownInstance * town) @@ -276,10 +278,10 @@ void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) setSelection(ownedTowns.front()); } -void PlayerLocalState::swapOwnedTowns(int pos1, int pos2) +void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2) { assert(ownedTowns[pos1] && ownedTowns[pos2]); - std::swap(ownedTowns[pos1], ownedTowns[pos2]); + std::swap(ownedTowns.at(pos1), ownedTowns.at(pos2)); adventureInt->onTownOrderChanged(); } diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index 85a367f21..b67940bd8 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -42,15 +42,15 @@ public: { //on which page we left spellbook int spellbookLastPageBattle = 0; - int spellbokLastPageAdvmap = 0; + int spellbookLastPageAdvmap = 0; int spellbookLastTabBattle = 4; int spellbookLastTabAdvmap = 4; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & spellbookLastPageBattle; - h & spellbokLastPageAdvmap; + h & spellbookLastPageAdvmap; h & spellbookLastTabBattle; h & spellbookLastTabAdvmap; } @@ -66,14 +66,14 @@ public: const CGTownInstance * getOwnedTown(size_t index); void addOwnedTown(const CGTownInstance * hero); void removeOwnedTown(const CGTownInstance * hero); - void swapOwnedTowns(int pos1, int pos2); + void swapOwnedTowns(size_t pos1, size_t pos2); const std::vector & getWanderingHeroes(); const CGHeroInstance * getWanderingHero(size_t index); const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero); void addWanderingHero(const CGHeroInstance * hero); void removeWanderingHero(const CGHeroInstance * hero); - void swapWanderingHero(int pos1, int pos2); + void swapWanderingHero(size_t pos1, size_t pos2); void setPath(const CGHeroInstance * h, const CGPath & path); bool setPath(const CGHeroInstance * h, const int3 & destination); @@ -94,7 +94,7 @@ public: void setSelection(const CArmedInstance *sel); template - void serialize(Handler & h, int version) + void serialize(Handler & h) { //WARNING: this code is broken and not used. See CClient::loadGame std::map pathsMap; //hero -> dest diff --git a/client/ServerRunner.cpp b/client/ServerRunner.cpp new file mode 100644 index 000000000..59cce1110 --- /dev/null +++ b/client/ServerRunner.cpp @@ -0,0 +1,88 @@ +/* + * ServerRunner.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "ServerRunner.h" + +#include "../lib/VCMIDirs.h" +#include "../lib/CThreadHelper.h" +#include "../server/CVCMIServer.h" + +#ifndef VCMI_MOBILE +#include +#include +#endif + +ServerThreadRunner::ServerThreadRunner() = default; +ServerThreadRunner::~ServerThreadRunner() = default; + +void ServerThreadRunner::start(uint16_t port, bool connectToLobby) +{ + server = std::make_unique(port, connectToLobby, true); + + threadRunLocalServer = boost::thread([this]{ + setThreadName("runServer"); + server->run(); + }); +} + +void ServerThreadRunner::shutdown() +{ + server->setState(EServerState::SHUTDOWN); +} + +void ServerThreadRunner::wait() +{ + threadRunLocalServer.join(); +} + +int ServerThreadRunner::exitCode() +{ + return 0; +} + +#ifndef VCMI_MOBILE + +ServerProcessRunner::ServerProcessRunner() = default; +ServerProcessRunner::~ServerProcessRunner() = default; + +void ServerProcessRunner::shutdown() +{ + child->terminate(); +} + +void ServerProcessRunner::wait() +{ + child->wait(); +} + +int ServerProcessRunner::exitCode() +{ + return child->exit_code(); +} + +void ServerProcessRunner::start(uint16_t port, bool connectToLobby) +{ + boost::filesystem::path serverPath = VCMIDirs::get().serverPath(); + boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "server_log.txt"; + std::vector args; + args.push_back("--port=" + std::to_string(port)); + args.push_back("--run-by-client"); + if(connectToLobby) + args.push_back("--lobby"); + + std::error_code ec; + child = std::make_unique(serverPath, args, ec, boost::process::std_out > logPath); + + if (ec) + throw std::runtime_error("Failed to start server! Reason: " + ec.message()); +} + +#endif diff --git a/client/ServerRunner.h b/client/ServerRunner.h new file mode 100644 index 000000000..115dcebfa --- /dev/null +++ b/client/ServerRunner.h @@ -0,0 +1,61 @@ +/* + * ServerRunner.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class CVCMIServer; + +class IServerRunner +{ +public: + virtual void start(uint16_t port, bool connectToLobby) = 0; + virtual void shutdown() = 0; + virtual void wait() = 0; + virtual int exitCode() = 0; + + virtual ~IServerRunner() = default; +}; + +/// Class that runs server instance as a thread of client process +class ServerThreadRunner : public IServerRunner, boost::noncopyable +{ + std::unique_ptr server; + boost::thread threadRunLocalServer; +public: + void start(uint16_t port, bool connectToLobby) override; + void shutdown() override; + void wait() override; + int exitCode() override; + + ServerThreadRunner(); + ~ServerThreadRunner(); +}; + +#ifndef VCMI_MOBILE + +namespace boost::process { +class child; +} + +/// Class that runs server instance as a child process +/// Available only on desktop systems where process management is allowed +class ServerProcessRunner : public IServerRunner, boost::noncopyable +{ + std::unique_ptr child; + +public: + void start(uint16_t port, bool connectToLobby) override; + void shutdown() override; + void wait() override; + int exitCode() override; + + ServerProcessRunner(); + ~ServerProcessRunner(); +}; +#endif diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 7d879a0ea..7fd0528c7 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -31,6 +31,7 @@ #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../CMT.h" #include "../PlayerLocalState.h" #include "../CPlayerInterface.h" @@ -65,8 +66,8 @@ AdventureMapInterface::AdventureMapInterface(): shortcuts->setState(EAdventureState::MAKING_TURN); widget->getMapView()->onViewMapActivated(); - if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled()) - watches = std::make_shared(); + if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.turnTimer != 0) + watches = std::make_shared(Point(24, 24)); addUsedEvents(KEYBOARD | TIME); } @@ -168,6 +169,15 @@ void AdventureMapInterface::show(Canvas & to) void AdventureMapInterface::dim(Canvas & to) { + if(settings["adventure"]["hideBackground"].Bool()) + for (auto window : GH.windows().findWindows()) + { + if(!std::dynamic_pointer_cast(window) && std::dynamic_pointer_cast(window) && std::dynamic_pointer_cast(window)->pos.w >= 800 && std::dynamic_pointer_cast(window)->pos.w >= 600) + { + to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck"))); + return; + } + } for (auto window : GH.windows().findWindows()) { if (!std::dynamic_pointer_cast(window) && !std::dynamic_pointer_cast(window) && !window->isPopupWindow()) @@ -304,7 +314,7 @@ void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel) auto town = dynamic_cast(sel); widget->getInfoBar()->showTownSelection(town); - widget->getTownList()->updateWidget();; + widget->getTownList()->updateWidget(); widget->getTownList()->select(town); widget->getHeroList()->select(nullptr); onHeroChanged(nullptr); @@ -331,6 +341,11 @@ void AdventureMapInterface::onTownOrderChanged() widget->getTownList()->updateWidget(); } +void AdventureMapInterface::onHeroOrderChanged() +{ + widget->getHeroList()->updateWidget(); +} + void AdventureMapInterface::onMapTilesChanged(boost::optional> positions) { if (positions) @@ -441,7 +456,10 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) if(auto iw = GH.windows().topWindow()) iw->close(); - hotkeyEndingTurn(); + GH.dispatchMainThread([this]() + { + hotkeyEndingTurn(); + }); } } @@ -459,6 +477,18 @@ void AdventureMapInterface::hotkeyEndingTurn() LOCPLINT->cb->endTurn(); mapAudio->onPlayerTurnEnded(); + + // Normally, game will receive PlayerStartsTurn call almost instantly with new player ID that will switch UI to waiting mode + // However, when simturns are active it is possible for such call not to come because another player is still acting + // So find first player other than ours that is acting at the moment and update UI as if he had started turn + for (auto player = PlayerColor(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + if (player != LOCPLINT->playerID && LOCPLINT->cb->isPlayerMakingTurn(player)) + { + onEnemyTurnStarted(player, LOCPLINT->cb->getStartInfo()->playerInfos.at(player).isControlledByHuman()); + break; + } + } } const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos) @@ -671,7 +701,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos) if(pathNode->layer == EPathfindingLayer::LAND) CCS->curh->set(cursorMove[turns]); else - CCS->curh->set(cursorSailVisit[turns]); + CCS->curh->set(cursorSail[turns]); break; case EPathNodeAction::VISIT: @@ -686,6 +716,15 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos) } else if(pathNode->layer == EPathfindingLayer::LAND) CCS->curh->set(cursorVisit[turns]); + else if (pathNode->layer == EPathfindingLayer::SAIL && + objAtTile && + objAtTile->isCoastVisitable() && + pathNode->theNodeBefore && + pathNode->theNodeBefore->layer == EPathfindingLayer::LAND ) + { + // exception - when visiting shipwreck located on coast from land - show 'horse' cursor, not 'ship' cursor + CCS->curh->set(cursorVisit[turns]); + } else CCS->curh->set(cursorSailVisit[turns]); break; @@ -813,7 +852,7 @@ Rect AdventureMapInterface::terrainAreaPixels() const const IShipyard * AdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const { - const IShipyard *ret = IShipyard::castFrom(obj); + const auto *ret = dynamic_cast(obj); if(!ret || obj->tempOwner != currentPlayerID || diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index 3a8c96add..bc1752c3a 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -149,6 +149,9 @@ public: /// Called when town order changes void onTownOrderChanged(); + /// Called when hero order changes + void onHeroOrderChanged(); + /// Called when map audio should be paused, e.g. on combat or town screen access void onAudioPaused(); diff --git a/client/adventureMap/AdventureOptions.cpp b/client/adventureMap/AdventureOptions.cpp index 265f8e5a0..24bafa2fa 100644 --- a/client/adventureMap/AdventureOptions.cpp +++ b/client/adventureMap/AdventureOptions.cpp @@ -23,6 +23,7 @@ #include "../../CCallback.h" #include "../../lib/StartInfo.h" +#include "../../lib/CGeneralTextHandler.h" AdventureOptions::AdventureOptions() : CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS")) @@ -30,12 +31,7 @@ AdventureOptions::AdventureOptions() OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); viewWorld = std::make_shared(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); - viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); }); - - exit = std::make_shared(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); - - scenInfo = std::make_shared(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); - scenInfo->addCallback(AdventureOptions::showScenarioInfo); + viewWorld->addCallback([] { LOCPLINT->viewWorldMap(); }); puzzle = std::make_shared(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE); puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT)); @@ -45,6 +41,14 @@ AdventureOptions::AdventureOptions() dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h)); else dig->block(true); + + scenInfo = std::make_shared(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); + scenInfo->addCallback(AdventureOptions::showScenarioInfo); + + replay = std::make_shared(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); }); + replay->addCallback([]{ LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.replayOpponentTurnNotImplemented")); }); + + exit = std::make_shared(Point(203, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); } void AdventureOptions::showScenarioInfo() diff --git a/client/adventureMap/AdventureOptions.h b/client/adventureMap/AdventureOptions.h index 41b47c56e..b049e74a0 100644 --- a/client/adventureMap/AdventureOptions.h +++ b/client/adventureMap/AdventureOptions.h @@ -21,7 +21,7 @@ class AdventureOptions : public CWindowObject std::shared_ptr puzzle; std::shared_ptr dig; std::shared_ptr scenInfo; - /*std::shared_ptr replay*/ + std::shared_ptr replay; public: AdventureOptions(); diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 555923631..8efabe249 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -151,6 +151,9 @@ void CInGameConsole::keyPressed (EShortcut key) break; case EShortcut::GAME_ACTIVATE_CONSOLE: + if(GH.isKeyboardAltDown()) + return; //QoL for alt-tab operating system shortcut + if(!enteredText.empty()) endEnteringText(false); else @@ -240,6 +243,9 @@ void CInGameConsole::startEnteringText() if (!isActive()) return; + if(enteredText != "") + return; + assert(currentStatusBar.expired());//effectively, nullptr check currentStatusBar = GH.statusbar(); diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 360dd4946..c7c97481e 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -134,7 +134,8 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() halls.at(hallLevel)++; } - std::vector allies, enemies; + std::vector allies; + std::vector enemies; //generate list of allies and enemies for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++) @@ -376,47 +377,51 @@ void CInfoBar::pushComponents(const std::vector & components, std::st std::array, int>, 10> reward_map; for(const auto & c : components) { - switch(c.id) + switch(c.type) { - case Component::EComponentType::PRIM_SKILL: - case Component::EComponentType::EXPERIENCE: + case ComponentType::PRIM_SKILL: + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + case ComponentType::MANA: reward_map.at(0).first.push_back(c); reward_map.at(0).second = 8; //At most 8, cannot be more break; - case Component::EComponentType::SEC_SKILL: + case ComponentType::SEC_SKILL: reward_map.at(1).first.push_back(c); reward_map.at(1).second = 4; //At most 4 break; - case Component::EComponentType::SPELL: + case ComponentType::SPELL: reward_map.at(2).first.push_back(c); reward_map.at(2).second = 4; //At most 4 break; - case Component::EComponentType::ARTIFACT: + case ComponentType::ARTIFACT: + case ComponentType::SPELL_SCROLL: reward_map.at(3).first.push_back(c); reward_map.at(3).second = 4; //At most 4, too long names break; - case Component::EComponentType::CREATURE: + case ComponentType::CREATURE: reward_map.at(4).first.push_back(c); reward_map.at(4).second = 4; //At most 4, too long names break; - case Component::EComponentType::RESOURCE: + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: reward_map.at(5).first.push_back(c); reward_map.at(5).second = 7; //At most 7 break; - case Component::EComponentType::MORALE: - case Component::EComponentType::LUCK: + case ComponentType::MORALE: + case ComponentType::LUCK: reward_map.at(6).first.push_back(c); reward_map.at(6).second = 2; //At most 2 - 1 for morale + 1 for luck break; - case Component::EComponentType::BUILDING: + case ComponentType::BUILDING: reward_map.at(7).first.push_back(c); reward_map.at(7).second = 1; //At most 1 - only large icons available AFAIK break; - case Component::EComponentType::HERO_PORTRAIT: + case ComponentType::HERO_PORTRAIT: reward_map.at(8).first.push_back(c); reward_map.at(8).second = 1; //I do not think than we even can get more than 1 hero break; - case Component::EComponentType::FLAG: + case ComponentType::FLAG: reward_map.at(9).first.push_back(c); reward_map.at(9).second = 1; //I do not think than we even can get more than 1 player in notification break; diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 49f3a383f..c535e80cf 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -218,8 +218,7 @@ CHeroList::CEmptyHeroItem::CEmptyHeroItem() CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) : CListItem(parent), - hero(Hero), - parentList(parent) + hero(Hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); movement = std::make_shared(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); @@ -264,7 +263,7 @@ void CHeroList::CHeroItem::showTooltip() std::string CHeroList::CHeroItem::getHoverText() { - return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->type->heroClass->getNameTranslated()); + return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->getClassNameTranslated()); } void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) @@ -280,24 +279,22 @@ void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const if(heroes.size() < 2) return; - int heroPos = vstd::find_pos(heroes, hero); - const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes[heroPos - 1]; - const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes[heroPos + 1]; + size_t heroPos = vstd::find_pos(heroes, hero); + const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes.at(heroPos - 1); + const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes.at(heroPos + 1); std::vector menuElements = { - { RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [this, heroPos]() + { RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [heroPos]() { - for (int i = heroPos; i > 0; i--) + for (size_t i = heroPos; i > 0; i--) LOCPLINT->localState->swapWanderingHero(i, i - 1); - parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [this, heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [this, heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, heroPos, heroes]() + { RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); } }, + { RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); } }, + { RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [heroPos, heroes]() { for (int i = heroPos; i < heroes.size() - 1; i++) LOCPLINT->localState->swapWanderingHero(i, i + 1); - parentList->updateWidget(); } }, }; @@ -329,7 +326,7 @@ void CHeroList::updateElement(const CGHeroInstance * hero) void CHeroList::updateWidget() { - auto & heroes = LOCPLINT->localState->getWanderingHeroes(); + const auto & heroes = LOCPLINT->localState->getWanderingHeroes(); listBox->resize(heroes.size()); @@ -340,7 +337,7 @@ void CHeroList::updateWidget() if (!item) continue; - if (item->hero == heroes[i]) + if (item->hero == heroes.at(i)) { item->update(); } @@ -366,11 +363,8 @@ std::shared_ptr CTownList::createItem(size_t index) CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): CListItem(parent), - parentList(parent) + town(Town) { - const std::vector towns = LOCPLINT->localState->getOwnedTowns(); - townIndex = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town)); - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); picture = std::make_shared(AnimationPath::builtin("ITPA"), 0); pos = picture->pos; @@ -386,7 +380,6 @@ std::shared_ptr CTownList::CTownItem::genSelection() void CTownList::CTownItem::update() { - const CGTownInstance * town = LOCPLINT->localState->getOwnedTowns()[townIndex]; size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; picture->setFrame(iconIndex + 2); @@ -396,17 +389,17 @@ void CTownList::CTownItem::update() void CTownList::CTownItem::select(bool on) { if(on) - LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTowns()[townIndex]); + LOCPLINT->localState->setSelection(town); } void CTownList::CTownItem::open() { - LOCPLINT->openTownWindow(LOCPLINT->localState->getOwnedTowns()[townIndex]); + LOCPLINT->openTownWindow(town); } void CTownList::CTownItem::showTooltip() { - CRClickPopup::createAndPush(LOCPLINT->localState->getOwnedTowns()[townIndex], GH.getCursorPosition()); + CRClickPopup::createAndPush(town, GH.getCursorPosition()); } void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) @@ -415,8 +408,9 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const return; const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + size_t townIndex = vstd::find_pos(towns, town); - if(townIndex < 0 || townIndex > towns.size() - 1 || !towns[townIndex]) + if(townIndex + 1 > towns.size() || !towns.at(townIndex)) return; if(towns.size() < 2) @@ -426,19 +420,17 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; std::vector menuElements = { - { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [this]() + { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [townIndex]() { for (int i = townIndex; i > 0; i--) LOCPLINT->localState->swapOwnedTowns(i, i - 1); - parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, towns]() + { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); } }, + { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); } }, + { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [townIndex, towns]() { for (int i = townIndex; i < towns.size() - 1; i++) LOCPLINT->localState->swapOwnedTowns(i, i + 1); - parentList->updateWidget(); } }, }; @@ -447,7 +439,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const std::string CTownList::CTownItem::getHoverText() { - return LOCPLINT->localState->getOwnedTowns()[townIndex]->getObjectName(); + return town->getObjectName(); } CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount) @@ -479,7 +471,15 @@ void CTownList::updateWidget() if (!item) continue; - listBox->reset(); + if (item->town == towns[i]) + { + item->update(); + } + else + { + listBox->reset(); + break; + } } if (LOCPLINT->localState->getCurrentTown()) diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index 01312e725..5b03f2b1e 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -117,7 +117,6 @@ class CHeroList : public CList std::shared_ptr movement; std::shared_ptr mana; std::shared_ptr portrait; - CHeroList *parentList; public: const CGHeroInstance * const hero; @@ -152,9 +151,8 @@ class CTownList : public CList class CTownItem : public CListItem { std::shared_ptr picture; - CTownList *parentList; public: - int townIndex; + const CGTownInstance * const town; CTownItem(CTownList *parent, const CGTownInstance * town); diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 64218498a..81a7cf002 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -173,8 +173,10 @@ void MapAudioPlayer::updateMusic() { if(audioPlaying && playerMakingTurn && currentSelection) { - const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType; - CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false); + const auto * tile = LOCPLINT->cb->getTile(currentSelection->visitablePos()); + + if (tile) + CCS->musich->playMusicFromSet("terrain", tile->terType->getJsonKey(), true, false); } if(audioPlaying && enemyMakingTurn) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 0d3aaa571..c97d67f94 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -15,54 +15,86 @@ #include "../CPlayerInterface.h" #include "../battle/BattleInterface.h" #include "../battle/BattleStacksController.h" - -#include "../render/EFont.h" -#include "../render/Graphics.h" #include "../gui/CGuiHandler.h" -#include "../gui/TextAlignment.h" +#include "../render/Graphics.h" #include "../widgets/Images.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/TextControls.h" + #include "../../CCallback.h" -#include "../../lib/CStack.h" #include "../../lib/CPlayerState.h" -#include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/CStack.h" +#include "../../lib/StartInfo.h" -TurnTimerWidget::DrawRect::DrawRect(const Rect & r, const ColorRGBA & c): - CIntObject(), rect(r), color(c) -{ -} +TurnTimerWidget::TurnTimerWidget(const Point & position) + : TurnTimerWidget(position, PlayerColor::NEUTRAL) +{} -void TurnTimerWidget::DrawRect::showAll(Canvas & to) +TurnTimerWidget::TurnTimerWidget(const Point & position, PlayerColor player) + : CIntObject(TIME) + , lastSoundCheckSeconds(0) + , isBattleMode(player.isValidPlayer()) { - to.drawColor(rect, color); - - CIntObject::showAll(to); -} + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; -TurnTimerWidget::TurnTimerWidget(): - InterfaceObjectConfigurable(TIME), - turnTime(0), lastTurnTime(0), cachedTurnTime(0), lastPlayer(PlayerColor::CANNOT_DETERMINE) -{ - REGISTER_BUILDER("drawRect", &TurnTimerWidget::buildDrawRect); - + pos += position; + pos.w = 0; + pos.h = 0; recActions &= ~DEACTIVATE; - - const JsonNode config(JsonPath::builtin("config/widgets/turnTimer.json")); - - build(config); - - std::transform(variables["notificationTime"].Vector().begin(), - variables["notificationTime"].Vector().end(), - std::inserter(notifications, notifications.begin()), - [](const JsonNode & node){ return node.Integer(); }); -} + const auto & timers = LOCPLINT->cb->getStartInfo()->turnTimerInfo; -std::shared_ptr TurnTimerWidget::buildDrawRect(const JsonNode & config) const -{ - logGlobal->debug("Building widget TurnTimerWidget::DrawRect"); - auto rect = readRect(config["rect"]); - auto color = readColor(config["color"]); - return std::make_shared(rect, color); + backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); // 1 px smaller on all sides + + if (isBattleMode) + backgroundBorder = std::make_shared(pos, ColorRGBA(0, 0, 0, 128), Colors::BRIGHT_YELLOW); + else + backgroundBorder = std::make_shared(pos, ColorRGBA(0, 0, 0, 128), Colors::BLACK); + + if (isBattleMode) + { + pos.w = 76; + + pos.h += 20; + playerLabelsMain[player] = std::make_shared(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], ""); + + if (timers.battleTimer != 0) + { + pos.h += 20; + playerLabelsBattle[player] = std::make_shared(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], ""); + } + + if (!timers.accumulatingUnitTimer && timers.unitTimer != 0) + { + pos.h += 20; + playerLabelsUnit[player] = std::make_shared(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], ""); + } + + updateTextLabel(player, LOCPLINT->cb->getPlayerTurnTime(player)); + } + else + { + if (!timers.accumulatingTurnTimer && timers.baseTimer != 0) + pos.w = 120; + else + pos.w = 60; + + for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + if (LOCPLINT->cb->getStartInfo()->playerInfos.count(player) == 0) + continue; + + if (!LOCPLINT->cb->getStartInfo()->playerInfos.at(player).isControlledByHuman()) + continue; + + pos.h += 20; + playerLabelsMain[player] = std::make_shared(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], ""); + + updateTextLabel(player, LOCPLINT->cb->getPlayerTurnTime(player)); + } + } + + backgroundTexture->pos = Rect::createAround(pos, -1); + backgroundBorder->pos = pos; } void TurnTimerWidget::show(Canvas & to) @@ -70,98 +102,95 @@ void TurnTimerWidget::show(Canvas & to) showAll(to); } -void TurnTimerWidget::setTime(PlayerColor player, int time) +void TurnTimerWidget::updateNotifications(PlayerColor player, int timeMs) { - int newTime = time / 1000; - if(player == LOCPLINT->playerID - && newTime != turnTime - && notifications.count(newTime)) - { - CCS->soundh->playSound(AudioPath::fromJson(variables["notificationSound"])); - } + if(player != LOCPLINT->playerID) + return; - turnTime = newTime; + int newTimeSeconds = timeMs / 1000; - if(auto w = widget("timer")) + if (newTimeSeconds != lastSoundCheckSeconds && notificationThresholds.count(newTimeSeconds)) + CCS->soundh->playSound(AudioPath::builtin("WE5")); + + lastSoundCheckSeconds = newTimeSeconds; +} + +static std::string msToString(int timeMs) +{ + int timeSeconds = timeMs / 1000; + std::ostringstream oss; + oss << timeSeconds / 60 << ":" << std::setw(2) << std::setfill('0') << timeSeconds % 60; + return oss.str(); +} + +void TurnTimerWidget::updateTextLabel(PlayerColor player, const TurnTimerInfo & timer) +{ + const auto & timerSettings = LOCPLINT->cb->getStartInfo()->turnTimerInfo; + auto mainLabel = playerLabelsMain[player]; + + if (isBattleMode) { - std::ostringstream oss; - oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60; - w->setText(oss.str()); - - if(graphics && LOCPLINT && LOCPLINT->cb - && variables["textColorFromPlayerColor"].Bool() - && player.isValidPlayer()) + mainLabel->setText(msToString(timer.baseTimer + timer.turnTimer)); + + if (timerSettings.battleTimer != 0) { - w->setColor(graphics->playerColors[player]); + auto battleLabel = playerLabelsBattle[player]; + if (timer.battleTimer != 0) + { + if (timerSettings.accumulatingUnitTimer) + battleLabel->setText("+" + msToString(timer.battleTimer + timer.unitTimer)); + else + battleLabel->setText("+" + msToString(timer.battleTimer)); + } + else + battleLabel->setText(""); } + + if (!timerSettings.accumulatingUnitTimer && timerSettings.unitTimer != 0) + { + auto unitLabel = playerLabelsUnit[player]; + if (timer.unitTimer != 0) + unitLabel->setText("+" + msToString(timer.unitTimer)); + else + unitLabel->setText(""); + } + } + else + { + if (!timerSettings.accumulatingTurnTimer && timerSettings.baseTimer != 0) + mainLabel->setText(msToString(timer.baseTimer) + "+" + msToString(timer.turnTimer)); + else + mainLabel->setText(msToString(timer.baseTimer + timer.turnTimer)); } } void TurnTimerWidget::updateTimer(PlayerColor player, uint32_t msPassed) { - const auto & time = LOCPLINT->cb->getPlayerTurnTime(player); - if(time.isActive) - cachedTurnTime -= msPassed; - - if(cachedTurnTime < 0) - cachedTurnTime = 0; //do not go below zero - - if(lastPlayer != player) - { - lastPlayer = player; - lastTurnTime = 0; - } - - auto timeCheckAndUpdate = [&](int time) - { - if(time / 1000 != lastTurnTime / 1000) - { - //do not update timer on this tick - lastTurnTime = time; - cachedTurnTime = time; - } - else - setTime(player, cachedTurnTime); - }; - - auto * playerInfo = LOCPLINT->cb->getPlayer(player); - if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman())) - { - if(time.isBattle) - timeCheckAndUpdate(time.creatureTimer); - else - timeCheckAndUpdate(time.turnTimer); - } - else - timeCheckAndUpdate(0); + const auto & gamestateTimer = LOCPLINT->cb->getPlayerTurnTime(player); + updateNotifications(player, gamestateTimer.valueMs()); + updateTextLabel(player, gamestateTimer); } void TurnTimerWidget::tick(uint32_t msPassed) { - if(!LOCPLINT || !LOCPLINT->cb) - return; - - if(LOCPLINT->battleInt) + for(const auto & player : playerLabelsMain) { - if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack()) - updateTimer(stack->getOwner(), msPassed); - else - updateTimer(PlayerColor::NEUTRAL, msPassed); - } - else - { - if(LOCPLINT->makingTurn) - updateTimer(LOCPLINT->playerID, msPassed); - else + if (LOCPLINT->battleInt) { - for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) - { - if(LOCPLINT->cb->isPlayerMakingTurn(p)) - { - updateTimer(p, msPassed); - break; - } - } + const auto & battle = LOCPLINT->battleInt->getBattle(); + + bool isDefender = battle->sideToPlayer(BattleSide::DEFENDER) == player.first; + bool isAttacker = battle->sideToPlayer(BattleSide::ATTACKER) == player.first; + bool isMakingUnitTurn = battle->battleActiveUnit() && battle->battleActiveUnit()->unitOwner() == player.first; + bool isEngagedInBattle = isDefender || isAttacker; + + // Due to way our network message queue works during battle animation + // client actually does not receives updates from server as to which timer is active when game has battle animations playing + // so during battle skip updating timer unless game is waiting for player to select action + if (isEngagedInBattle && !isMakingUnitTurn) + continue; } + + updateTimer(player.first, msPassed); } } diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h index f8b4b97fc..1077db031 100644 --- a/client/adventureMap/TurnTimerWidget.h +++ b/client/adventureMap/TurnTimerWidget.h @@ -11,50 +11,43 @@ #pragma once #include "../gui/CIntObject.h" -#include "../gui/InterfaceObjectConfigurable.h" -#include "../render/Canvas.h" #include "../render/Colors.h" +#include "../../lib/TurnTimerInfo.h" class CAnimImage; class CLabel; +class CFilledTexture; +class TransparentFilledRectangle; VCMI_LIB_NAMESPACE_BEGIN - class PlayerColor; - VCMI_LIB_NAMESPACE_END -class TurnTimerWidget : public InterfaceObjectConfigurable +class TurnTimerWidget : public CIntObject { -private: - - class DrawRect : public CIntObject - { - const Rect rect; - const ColorRGBA color; - - public: - DrawRect(const Rect &, const ColorRGBA &); - void showAll(Canvas & to) override; - }; + int lastSoundCheckSeconds; + bool isBattleMode; + + const std::set notificationThresholds = {1, 2, 3, 4, 5, 10, 20, 30}; + + std::map> playerLabelsMain; + std::map> playerLabelsBattle; + std::map> playerLabelsUnit; + std::shared_ptr backgroundTexture; + std::shared_ptr backgroundBorder; - int turnTime; - int lastTurnTime; - int cachedTurnTime; - PlayerColor lastPlayer; - - std::set notifications; - - std::shared_ptr buildDrawRect(const JsonNode & config) const; - void updateTimer(PlayerColor player, uint32_t msPassed); -public: - void show(Canvas & to) override; void tick(uint32_t msPassed) override; - void setTime(PlayerColor player, int time); + void updateNotifications(PlayerColor player, int timeMs); + void updateTextLabel(PlayerColor player, const TurnTimerInfo & timer); - TurnTimerWidget(); +public: + /// Activates adventure map mode in which widget will display timer for all players + TurnTimerWidget(const Point & position); + + /// Activates battle mode in which timer displays only timer of specific player + TurnTimerWidget(const Point & position, PlayerColor player); }; diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 7e476340c..713458d1c 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -29,6 +29,7 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CRandomGenerator.h" #include "../../lib/CStack.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/spells/CSpellHandler.h" @@ -568,7 +569,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B switch (action.get()) { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - return (targetStack && targetStackOwned && targetStack->speed() > 0); + return (targetStack && targetStackOwned && targetStack->getMovementRange() > 0); case PossiblePlayerBattleAction::CREATURE_INFO: return (targetStack && targetStackOwned && targetStack->alive()); @@ -616,8 +617,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures { - int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE); - return spellID > -1; + SpellID spellID = owner.getBattle()->getRandomBeneficialSpell(CRandomGenerator::getDefault(), owner.stacksController->getActiveStack(), targetStack); + return spellID != SpellID::NONE; } return false; @@ -750,7 +751,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B if (!spellcastingModeActive()) { - if (action.spell().toSpell()) + if (action.spell().hasValue()) { owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell()); } @@ -887,17 +888,17 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS { // faerie dragon can cast only one, randomly selected spell until their next move //TODO: faerie dragon type spell should be selected by server - const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); + const auto spellToCast = owner.getBattle()->getRandomCastedSpell(CRandomGenerator::getDefault(), casterStack); - if (spellToCast) - creatureSpells.push_back(spellToCast); + if (spellToCast.hasValue()) + creatureSpells.push_back(spellToCast.toSpell()); } TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER)); for(const auto & bonus : *bl) { - if (bonus->additionalInfo[0] <= 0) + if (bonus->additionalInfo[0] <= 0 && bonus->subtype.as().hasValue()) creatureSpells.push_back(bonus->subtype.as().toSpell()); } } @@ -905,7 +906,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS const spells::Caster * BattleActionsController::getCurrentSpellcaster() const { if (heroSpellToCast) - return owner.getActiveHero(); + return owner.currentHero(); else return owner.stacksController->getActiveStack(); } @@ -999,7 +1000,7 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex) return action.spellcast(); }; - bool isCurrentStackInSpellcastMode = std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); + bool isCurrentStackInSpellcastMode = !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); if (spellcastingModeActive() || isCurrentStackInSpellcastMode) { diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index f56298930..2bff6e8ce 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -363,7 +363,7 @@ bool MovementAnimation::init() Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack); - progressPerSecond = AnimationControls::getMovementDistance(stack->unitType()); + progressPerSecond = AnimationControls::getMovementRange(stack->unitType()); begX = begPosition.x; begY = begPosition.y; diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index b0ee095b5..d55c22360 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -11,6 +11,7 @@ #include "BattleFieldController.h" #include "BattleInterface.h" +#include "BattleWindow.h" #include "BattleActionsController.h" #include "BattleInterfaceClasses.h" #include "BattleEffectsController.h" @@ -360,10 +361,7 @@ std::set BattleFieldController::getMovementRangeForHoveredStack() if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) return result; - auto hoveredHex = getHoveredHex(); - - // add possible movement hexes for stack under mouse - const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + auto hoveredStack = getHoveredStack(); if(hoveredStack) { std::vector v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr); @@ -591,10 +589,9 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) std::set hoveredMoveHexes = getHighlightedHexesForMovementTarget(); BattleHex hoveredHex = getHoveredHex(); - if(hoveredHex == BattleHex::INVALID) - return; - const CStack * hoveredStack = getHoveredStack(); + if(!hoveredStack && hoveredHex == BattleHex::INVALID) + return; // skip range limit calculations if unit hovered is not a shooter if(hoveredStack && hoveredStack->isShooter()) @@ -608,7 +605,7 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts); } - auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; + auto const & hoveredMouseHexes = hoveredHex != BattleHex::INVALID && owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex) { @@ -676,6 +673,14 @@ const CStack* BattleFieldController::getHoveredStack() auto hoveredHex = getHoveredHex(); const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(owner.windowObject->getQueueHoveredUnitId().has_value()) + { + auto stacks = owner.getBattle()->battleGetAllStacks(); + for(const CStack * stack : stacks) + if(stack->unitId() == *owner.windowObject->getQueueHoveredUnitId()) + hoveredStack = stack; + } + return hoveredStack; } diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 8cdb5a7e7..d534f0556 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -29,6 +29,7 @@ #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../windows/CTutorialWindow.h" #include "../render/Canvas.h" #include "../adventureMap/AdventureMapInterface.h" @@ -57,6 +58,7 @@ BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet * , curInt(att) , battleID(battleID) , battleOpeningDelayActive(true) + , round(0) { if(spectatorInt) { @@ -106,7 +108,8 @@ void BattleInterface::playIntroSoundAndUnlockInterface() { auto onIntroPlayed = [this]() { - if(LOCPLINT->battleInt) + // Make sure that battle have not ended while intro was playing AND that a different one has not started + if(LOCPLINT->battleInt.get() == this) onIntroSoundPlayed(); }; @@ -148,6 +151,8 @@ void BattleInterface::openingEnd() tacticNextStack(nullptr); activateStack(); battleOpeningDelayActive = false; + + CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_BATTLE); } BattleInterface::~BattleInterface() @@ -231,6 +236,7 @@ void BattleInterface::newRoundFirst() void BattleInterface::newRound() { console->addText(CGI->generaltexth->allTexts[412]); + round++; } void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell) @@ -346,13 +352,13 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) CCS->curh->set(Cursor::Combat::BLOCKED); const SpellID spellID = sc->spellID; + + if(!spellID.hasValue()) + return; + const CSpell * spell = spellID.toSpell(); auto targetedTile = sc->tile; - assert(spell); - if(!spell) - return; - const AudioPath & castSoundPath = spell->getCastSound(); if (!castSoundPath.empty()) @@ -634,7 +640,7 @@ void BattleInterface::tacticPhaseEnd() static bool immobile(const CStack *s) { - return !s->speed(0, true); //should bound stacks be immobile? + return s->getMovementRange() == 0; //should bound stacks be immobile? } void BattleInterface::tacticNextStack(const CStack * current) diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index 31e64ef29..4216b2e77 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -136,6 +136,7 @@ public: const CGHeroInstance *defendingHeroInstance; bool tacticsMode; + ui32 round; std::unique_ptr projectilesController; std::unique_ptr siegeController; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 8a5ce5261..eb97fdcab 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -34,7 +34,9 @@ #include "../widgets/Buttons.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../windows/CMessage.h" +#include "../windows/CCreatureWindow.h" #include "../windows/CSpellWindow.h" #include "../render/CAnimation.h" #include "../render/IRenderHandler.h" @@ -446,6 +448,117 @@ void HeroInfoBasicPanel::show(Canvas & to) CIntObject::show(to); } + +StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, bool initializeBackground) + : CIntObject(0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(initializeBackground) + { + background = std::make_shared(ImagePath::builtin("CCRPOP")); + background->pos.y += 37; + background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + background->colorize(stack->getOwner()); + background2 = std::make_shared(ImagePath::builtin("CHRPOP")); + background2->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + background2->colorize(stack->getOwner()); + } + + initializeData(stack); +} + +void StackInfoBasicPanel::initializeData(const CStack * stack) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), stack->creatureId() + 2, 0, 10, 6)); + labels.push_back(std::make_shared(10 + 58, 6 + 64, FONT_MEDIUM, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, TextOperations::formatMetric(stack->getCount(), 4))); + + auto attack = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getAttack(stack->isShooter())) + "(" + std::to_string(stack->getAttack(stack->isShooter())) + ")"; + auto defense = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getDefense(stack->isShooter())) + "(" + std::to_string(stack->getDefense(stack->isShooter())) + ")"; + auto damage = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getMinDamage(stack->isShooter())) + "-" + std::to_string(stack->getMaxDamage(stack->isShooter())); + auto health = CGI->creatures()->getByIndex(stack->creatureIndex())->getMaxHealth(); + auto morale = stack->moraleVal(); + auto luck = stack->luckVal(); + + auto killed = stack->getKilled(); + auto healthRemaining = TextOperations::formatMetric(std::max(stack->getAvailableHealth() - (stack->getCount() - 1) * health, (si64)0), 4); + + //primary stats*/ + labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); + labels.push_back(std::make_shared(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":")); + labels.push_back(std::make_shared(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[386] + ":")); + labels.push_back(std::make_shared(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[389] + ":")); + + labels.push_back(std::make_shared(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, attack)); + labels.push_back(std::make_shared(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, defense)); + labels.push_back(std::make_shared(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, damage)); + labels.push_back(std::make_shared(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(health))); + + //morale+luck + labels.push_back(std::make_shared(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":")); + labels.push_back(std::make_shared(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":")); + + icons.push_back(std::make_shared(AnimationPath::builtin("IMRL22"), morale + 3, 0, 47, 131)); + icons.push_back(std::make_shared(AnimationPath::builtin("ILCK22"), luck + 3, 0, 47, 143)); + + //extra information + labels.push_back(std::make_shared(9, 168, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, VLC->generaltexth->translate("vcmi.battleWindow.killed") + ":")); + labels.push_back(std::make_shared(9, 180, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[389] + ":")); + + labels.push_back(std::make_shared(69, 180, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(killed))); + labels.push_back(std::make_shared(69, 192, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, healthRemaining)); + + //spells + static const Point firstPos(15, 206); // position of 1st spell box + static const Point offset(0, 38); // offset of each spell box from previous + + for(int i = 0; i < 3; i++) + icons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), 78, 0, firstPos.x + offset.x * i, firstPos.y + offset.y * i)); + + int printed=0; //how many effect pics have been printed + std::vector spells = stack->activeSpells(); + for(SpellID effect : spells) + { + //not all effects have graphics (for eg. Acid Breath) + //for modded spells iconEffect is added to SpellInt.def + const bool hasGraphics = (effect < SpellID::THUNDERBOLT) || (effect >= SpellID::AFTER_LAST); + + if (hasGraphics) + { + //FIXME: support permanent duration + int duration = stack->getFirstBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain; + + icons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); + if(settings["general"]["enableUiEnhancements"].Bool()) + labels.push_back(std::make_shared(firstPos.x + offset.x * printed + 46, firstPos.y + offset.y * printed + 36, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(duration))); + if(++printed >= 3 || (printed == 2 && spells.size() > 3)) // interface limit reached + break; + } + } + + if(spells.size() == 0) + labelsMultiline.push_back(std::make_shared(Rect(firstPos.x, firstPos.y, 48, 36), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[674])); + if(spells.size() > 3) + labelsMultiline.push_back(std::make_shared(Rect(firstPos.x + offset.x * 2, firstPos.y + offset.y * 2 - 4, 48, 36), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "...")); +} + +void StackInfoBasicPanel::update(const CStack * updatedInfo) +{ + icons.clear(); + labels.clear(); + labelsMultiline.clear(); + + initializeData(updatedInfo); +} + +void StackInfoBasicPanel::show(Canvas & to) +{ + showAll(to); + CIntObject::show(to); +} + HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP")) { @@ -470,7 +583,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface exit = std::make_shared(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); exit->setBorderColor(Colors::METALLIC_GOLD); - if(allowReplay) + if(allowReplay || owner.cb->getStartInfo()->extraOptionsInfo.unlimitedReplay) { repeat = std::make_shared(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); repeat->setBorderColor(Colors::METALLIC_GOLD); @@ -739,12 +852,19 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) owner(owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + uint32_t queueSize = QUEUE_SIZE_BIG; + if(embedded) { - pos.w = QUEUE_SIZE * 41; + int32_t queueSmallOutsideYOffset = 65; + bool queueSmallOutside = settings["battle"]["queueSmallOutside"].Bool() && (pos.y - queueSmallOutsideYOffset) >= 0; + queueSize = std::clamp(static_cast(settings["battle"]["queueSmallSlots"].Float()), 1, queueSmallOutside ? GH.screenDimensions().x / 41 : 19); + + pos.w = queueSize * 41; pos.h = 49; pos.x += parent->pos.w/2 - pos.w/2; - pos.y += 10; + pos.y += queueSmallOutside ? -queueSmallOutsideYOffset : 10; icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL")); stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); @@ -765,7 +885,7 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) } stateIcons->preload(); - stackBoxes.resize(QUEUE_SIZE); + stackBoxes.resize(queueSize); for (int i = 0; i < stackBoxes.size(); i++) { stackBoxes[i] = std::make_shared(this); @@ -787,11 +907,16 @@ void StackQueue::update() owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0); size_t boxIndex = 0; + ui32 tmpTurn = -1; for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++) { for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++) - stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn); + { + ui32 currentTurn = owner.round + turn; + stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn, tmpTurn != currentTurn && owner.round != 0 && (!embedded || tmpTurn != -1) ? (std::optional)currentTurn : std::nullopt); + tmpTurn = currentTurn; + } } for(; boxIndex < stackBoxes.size(); boxIndex++) @@ -829,11 +954,14 @@ StackQueue::StackBox::StackBox(StackQueue * owner): { icon = std::make_shared(owner->icons, 0, 0, 5, 2); amount = std::make_shared(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + roundRect = std::make_shared(Rect(0, 0, 2, 48), ColorRGBA(0, 0, 0, 255), ColorRGBA(0, 255, 0, 255)); } else { icon = std::make_shared(owner->icons, 0, 0, 9, 1); amount = std::make_shared(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + roundRect = std::make_shared(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255)); + round = std::make_shared(4, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); int icon_x = pos.w - 17; int icon_y = pos.h - 18; @@ -841,9 +969,10 @@ StackQueue::StackBox::StackBox(StackQueue * owner): stateIcon = std::make_shared(owner->stateIcons, 0, 0, icon_x, icon_y); stateIcon->visible = false; } + roundRect->disable(); } -void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn) +void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std::optional currentTurn) { if(unit) { @@ -860,7 +989,18 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn) if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS) icon->setFrame(owner->getSiegeShooterIconID(), 1); + roundRect->setEnabled(currentTurn.has_value()); + if(!owner->embedded) + round->setEnabled(currentTurn.has_value()); + amount->setText(TextOperations::formatMetric(unit->getCount(), 4)); + if(currentTurn && !owner->embedded) + { + std::string tmp = std::to_string(*currentTurn); + int len = graphics->fonts[FONT_SMALL]->getStringWidth(tmp); + roundRect->pos.w = len + 6; + round->setText(tmp); + } if(stateIcon) { @@ -919,3 +1059,11 @@ void StackQueue::StackBox::show(Canvas & to) if(isBoundUnitHighlighted()) to.drawBorder(background->pos, Colors::CYAN, 2); } + +void StackQueue::StackBox::showPopupWindow(const Point & cursorPosition) +{ + auto stacks = owner->owner.getBattle()->battleGetAllStacks(); + for(const CStack * stack : stacks) + if(boundUnitID.has_value() && stack->unitId() == *boundUnitID) + GH.windows().createAndPushWindow(stack, true); +} diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index ac2431e94..d924d8347 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -35,10 +35,11 @@ class BattleInterface; class CPicture; class CFilledTexture; class CButton; -class CToggleButton; class CLabel; +class CMultiLineLabel; class CTextBox; class CAnimImage; +class TransparentFilledRectangle; class CPlayerInterface; class BattleRenderer; @@ -144,6 +145,23 @@ public: void update(const InfoAboutHero & updatedInfo); }; +class StackInfoBasicPanel : public CIntObject +{ +private: + std::shared_ptr background; + std::shared_ptr background2; + std::vector> labels; + std::vector> labelsMultiline; + std::vector> icons; +public: + StackInfoBasicPanel(const CStack * stack, bool initializeBackground = true); + + void show(Canvas & to) override; + + void initializeData(const CStack * stack); + void update(const CStack * updatedInfo); +}; + class HeroInfoWindow : public CWindowObject { private: @@ -206,19 +224,21 @@ class StackQueue : public CIntObject std::shared_ptr icon; std::shared_ptr amount; std::shared_ptr stateIcon; + std::shared_ptr round; + std::shared_ptr roundRect; void show(Canvas & to) override; void showAll(Canvas & to) override; + void showPopupWindow(const Point & cursorPosition) override; bool isBoundUnitHighlighted() const; public: StackBox(StackQueue * owner); - void setUnit(const battle::Unit * unit, size_t turn = 0); + void setUnit(const battle::Unit * unit, size_t turn = 0, std::optional currentTurn = std::nullopt); std::optional getBoundUnitID() const; - }; - static const int QUEUE_SIZE = 10; + static const int QUEUE_SIZE_BIG = 10; std::shared_ptr background; std::vector> stackBoxes; BattleInterface & owner; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 0bb6e0bd2..0c6e7e809 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -27,6 +27,7 @@ #include "../../CCallback.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/ObstacleHandler.h" +#include "../../lib/serializer/JsonDeserializer.h" BattleObstacleController::BattleObstacleController(BattleInterface & owner): owner(owner), @@ -77,7 +78,14 @@ void BattleObstacleController::obstacleRemoved(const std::vectorpreload(); auto first = animation->getImage(0, 0); @@ -88,7 +96,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector if we know how to blit obstacle, let's blit the effect in the same place Point whereTo = getObstaclePosition(first, obstacle); //AFAIK, in H3 there is no sound of obstacle removal - owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true)); + owner.stacksController->addNewAnim(new EffectAnimation(owner, animationPath, whereTo, obstacle["position"].Integer(), 0, true)); obstacleAnimations.erase(oi.id); //so when multiple obstacles are removed, they show up one after another diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 1609745eb..5528b0bb4 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -26,14 +26,18 @@ #include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../../CCallback.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleHex.h" +#include "../../lib/CRandomGenerator.h" #include "../../lib/CStack.h" #include "../../lib/CondSh.h" #include "../../lib/TextOperations.h" @@ -326,10 +330,10 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14); } - Point textPosition = amountBG->dimensions()/2 + boxPosition; + Point textPosition = Point(amountBG->dimensions().x/2 + boxPosition.x, boxPosition.y + graphics->fonts[EFonts::FONT_TINY]->getLineHeight() - 6); canvas.draw(amountBG, boxPosition); - canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4)); + canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::TOPCENTER, TextOperations::formatMetric(stack->getCount(), 4)); } void BattleStacksController::showStack(Canvas & canvas, const CStack * stack) @@ -534,7 +538,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vectorhasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementFlying))) + if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementTeleporting))) { owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() { @@ -691,6 +695,8 @@ void BattleStacksController::endAction(const BattleAction & action) void BattleStacksController::startAction(const BattleAction & action) { + // if timer run out and we did not act in time - deactivate current stack + setActiveStack(nullptr); removeExpiredColorFilters(); } @@ -810,6 +816,9 @@ void BattleStacksController::updateHoveredStacks() { auto newStacks = selectHoveredStacks(); + if(newStacks.size() == 0) + owner.windowObject->updateStackInfoWindow(nullptr); + for(const auto * stack : mouseHoveredStacks) { if (vstd::contains(newStacks, stack)) @@ -826,11 +835,15 @@ void BattleStacksController::updateHoveredStacks() if (vstd::contains(mouseHoveredStacks, stack)) continue; + owner.windowObject->updateStackInfoWindow(newStacks.size() == 1 && vstd::find_pos(newStacks, stack) == 0 ? stack : nullptr); stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder()); if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen()) stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON); } + if(mouseHoveredStacks != newStacks) + GH.windows().totalRedraw(); //fix for frozen stack info window and blue border in action bar + mouseHoveredStacks = newStacks; } diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index ccf66c5a9..010602306 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -94,7 +94,6 @@ class BattleStacksController void tickFrameBattleAnimations(uint32_t msPassed); void updateBattleAnimations(uint32_t msPassed); - void updateHoveredStacks(); std::vector selectHoveredStacks(); @@ -127,6 +126,8 @@ public: void showAliveStack(Canvas & canvas, const CStack * stack); void showStack(Canvas & canvas, const CStack * stack); + void updateHoveredStacks(); + void collectRenderableObjects(BattleRenderer & renderer); /// Adds new color filter effect targeting stack diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index c5f1734d9..790d09d04 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -31,6 +31,7 @@ #include "../render/Canvas.h" #include "../render/IRenderHandler.h" #include "../adventureMap/CInGameConsole.h" +#include "../adventureMap/TurnTimerWidget.h" #include "../../CCallback.h" #include "../../lib/CGeneralTextHandler.h" @@ -39,6 +40,9 @@ #include "../../lib/CStack.h" #include "../../lib/CConfigHandler.h" #include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/StartInfo.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/CPlayerState.h" #include "../windows/settings/SettingsMainWindow.h" BattleWindow::BattleWindow(BattleInterface & owner): @@ -50,6 +54,12 @@ BattleWindow::BattleWindow(BattleInterface & owner): pos.h = 600; pos = center(); + PlayerColor defenderColor = owner.getBattle()->getBattle()->getSidePlayer(BattleSide::DEFENDER); + PlayerColor attackerColor = owner.getBattle()->getBattle()->getSidePlayer(BattleSide::ATTACKER); + bool isDefenderHuman = defenderColor.isValidPlayer() && LOCPLINT->cb->getStartInfo()->playerInfos.at(defenderColor).isControlledByHuman(); + bool isAttackerHuman = attackerColor.isValidPlayer() && LOCPLINT->cb->getStartInfo()->playerInfos.at(attackerColor).isControlledByHuman(); + onlyOnePlayerHuman = isDefenderHuman != isAttackerHuman; + REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json")); @@ -58,6 +68,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this)); addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this)); + addShortcut(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, std::bind(&BattleWindow::endWithAutocombat, this)); addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this)); addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this)); addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this)); @@ -83,6 +94,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): createQueue(); createStickyHeroInfoWindows(); + createTimerInfoWindows(); if ( owner.tacticsMode ) tacticPhaseStarted(); @@ -127,19 +139,13 @@ void BattleWindow::createStickyHeroInfoWindows() { InfoAboutHero info; info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); - Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x + pos.w + 15, pos.y) - : Point(pos.x + pos.w -79, pos.y + 135); - defenderHeroWindow = std::make_shared(info, &position); + defenderHeroWindow = std::make_shared(info, nullptr); } if(owner.attackingHeroInstance) { InfoAboutHero info; info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); - Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x - 93, pos.y) - : Point(pos.x + 1, pos.y + 135); - attackerHeroWindow = std::make_shared(info, &position); + attackerHeroWindow = std::make_shared(info, nullptr); } bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); @@ -152,6 +158,35 @@ void BattleWindow::createStickyHeroInfoWindows() if(defenderHeroWindow) defenderHeroWindow->disable(); } + + setPositionInfoWindow(); +} + +void BattleWindow::createTimerInfoWindows() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.battleTimer != 0 || LOCPLINT->cb->getStartInfo()->turnTimerInfo.unitTimer != 0) + { + PlayerColor attacker = owner.getBattle()->sideToPlayer(BattleSide::ATTACKER); + PlayerColor defender = owner.getBattle()->sideToPlayer(BattleSide::DEFENDER); + + if (attacker.isValidPlayer()) + { + if (GH.screenDimensions().x >= 1000) + attackerTimerWidget = std::make_shared(Point(-92, 1), attacker); + else + attackerTimerWidget = std::make_shared(Point(1, 135), attacker); + } + + if (defender.isValidPlayer()) + { + if (GH.screenDimensions().x >= 1000) + defenderTimerWidget = std::make_shared(Point(pos.w + 16, 1), defender); + else + defenderTimerWidget = std::make_shared(Point(pos.w - 78, 135), defender); + } + } } BattleWindow::~BattleWindow() @@ -192,6 +227,7 @@ void BattleWindow::hideQueue() pos.h -= queue->pos.h; pos = center(); } + setPositionInfoWindow(); GH.windows().totalRedraw(); } @@ -205,6 +241,7 @@ void BattleWindow::showQueue() createQueue(); updateQueue(); + setPositionInfoWindow(); GH.windows().totalRedraw(); } @@ -251,12 +288,69 @@ void BattleWindow::updateQueue() queue->update(); } +void BattleWindow::setPositionInfoWindow() +{ + if(defenderHeroWindow) + { + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x + pos.w + 15, pos.y + 60) + : Point(pos.x + pos.w -79, pos.y + 195); + defenderHeroWindow->moveTo(position); + } + if(attackerHeroWindow) + { + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x - 93, pos.y + 60) + : Point(pos.x + 1, pos.y + 195); + attackerHeroWindow->moveTo(position); + } + if(defenderStackWindow) + { + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y + 60) + : Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 195); + defenderStackWindow->moveTo(position); + } + if(attackerStackWindow) + { + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y + 60) + : Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 195); + attackerStackWindow->moveTo(position); + } +} + void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero) { std::shared_ptr panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow; panelToUpdate->update(hero); } +void BattleWindow::updateStackInfoWindow(const CStack * stack) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); + + if(stack && stack->unitSide() == BattleSide::DEFENDER) + { + defenderStackWindow = std::make_shared(stack); + defenderStackWindow->setEnabled(showInfoWindows); + } + else + defenderStackWindow = nullptr; + + if(stack && stack->unitSide() == BattleSide::ATTACKER) + { + attackerStackWindow = std::make_shared(stack); + attackerStackWindow->setEnabled(showInfoWindows); + } + else + attackerStackWindow = nullptr; + + setPositionInfoWindow(); +} + void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) { if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance) @@ -390,7 +484,7 @@ void BattleWindow::bFleef() if ( owner.getBattle()->battleCanFlee() ) { - CFunctionList ony = std::bind(&BattleWindow::reallyFlee,this); + auto ony = std::bind(&BattleWindow::reallyFlee,this); owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? } else @@ -469,9 +563,8 @@ void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]); break; } - - auto anim = GH.renderHandler().loadAnimation(iconName); - w->setImage(anim); + + w->setImage(iconName); w->redraw(); } @@ -492,6 +585,12 @@ void BattleWindow::bAutofightf() if (owner.actionsController->spellcastingModeActive()) return; + if(settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) + { + endWithAutocombat(); + return; + } + //Stop auto-fight mode if(owner.curInt->isAutoFightOn) { @@ -542,7 +641,7 @@ void BattleWindow::bSpellf() { //TODO: move to spell mechanics, add more information to spell cast problem //Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible - auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC)); + auto blockingBonus = owner.currentHero()->getFirstBonus(Selector::type()(BonusType::BLOCK_ALL_MAGIC)); if (!blockingBonus) return; @@ -557,6 +656,15 @@ void BattleWindow::bSpellf() LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683]) % heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated())); } + else if(blockingBonus->source == BonusSource::OBJECT_TYPE) + { + if(blockingBonus->sid.as() == Obj::GARRISON || blockingBonus->sid.as() == Obj::GARRISON2) + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[684]); + } + } + else + { + logGlobal->warn("Unexpected problem with readiness to cast spell"); } } @@ -653,7 +761,8 @@ void BattleWindow::blockUI(bool on) setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait); setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode); setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode); - setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive()); + setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->spellcastingModeActive() : owner.actionsController->spellcastingModeActive()); + setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || owner.tacticsMode || !onlyOnePlayerHuman || owner.actionsController->spellcastingModeActive()); setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode); setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode); setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode); @@ -665,12 +774,45 @@ std::optional BattleWindow::getQueueHoveredUnitId() return queue->getHoveredUnitIdIfAny(); } +void BattleWindow::endWithAutocombat() +{ + if(!owner.makingTurn() || owner.tacticsMode) + return; + + LOCPLINT->showYesNoDialog( + VLC->generaltexth->translate("vcmi.battleWindow.endWithAutocombat"), + [this]() + { + owner.curInt->isAutoFightEndBattle = true; + + auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); + + AutocombatPreferences autocombatPreferences = AutocombatPreferences(); + autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); + + ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences); + ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false); + + owner.curInt->isAutoFightOn = true; + owner.curInt->cb->registerBattleInterface(ai); + owner.curInt->autofightingAI = ai; + + owner.requestAutofightingAIToTakeAction(); + + close(); + + owner.curInt->battleInt.reset(); + }, + nullptr + ); +} + void BattleWindow::showAll(Canvas & to) { CIntObject::showAll(to); if (GH.screenDimensions().x != 800 || GH.screenDimensions().y !=600) - CMessage::drawBorder(owner.curInt->playerID, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); + CMessage::drawBorder(owner.curInt->playerID, to, pos.w+28, pos.h+29, pos.x-14, pos.y-15); } void BattleWindow::show(Canvas & to) diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index 761d5183b..654ab71a7 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -24,7 +24,9 @@ class BattleInterface; class BattleConsole; class BattleRenderer; class StackQueue; +class TurnTimerWidget; class HeroInfoBasicPanel; +class StackInfoBasicPanel; /// GUI object that handles functionality of panel at the bottom of combat screen class BattleWindow : public InterfaceObjectConfigurable @@ -35,6 +37,11 @@ class BattleWindow : public InterfaceObjectConfigurable std::shared_ptr console; std::shared_ptr attackerHeroWindow; std::shared_ptr defenderHeroWindow; + std::shared_ptr attackerStackWindow; + std::shared_ptr defenderStackWindow; + + std::shared_ptr attackerTimerWidget; + std::shared_ptr defenderTimerWidget; /// button press handling functions void bOptionsf(); @@ -65,9 +72,12 @@ class BattleWindow : public InterfaceObjectConfigurable void toggleStickyHeroWindowsVisibility(); void createStickyHeroInfoWindows(); + void createTimerInfoWindows(); std::shared_ptr buildBattleConsole(const JsonNode &) const; + bool onlyOnePlayerHuman; + public: BattleWindow(BattleInterface & owner ); ~BattleWindow(); @@ -92,9 +102,15 @@ public: /// Refresh queue after turn order changes void updateQueue(); + // Set positions for hero & stack info window + void setPositionInfoWindow(); + /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero); + /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side + void updateStackInfoWindow(const CStack * stack); + /// Get mouse-hovered battle queue unit ID if any found std::optional getQueueHoveredUnitId(); @@ -114,5 +130,8 @@ public: /// Set possible alternative options. If more than 1 - the last will be considered as default option void setAlternativeActions(const std::list &); + + /// ends battle with autocombat + void endWithAutocombat(); }; diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index d3c6f7632..2addb4c3d 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -148,7 +148,7 @@ float AnimationControls::getSpellEffectSpeed() return static_cast(getAnimationSpeedFactor() * 10); } -float AnimationControls::getMovementDistance(const CCreature * creature) +float AnimationControls::getMovementRange(const CCreature * creature) { // H3 speed: 2/4/6 tiles per second return static_cast( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index 9029f7437..686e941b5 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -50,7 +50,7 @@ namespace AnimationControls float getSpellEffectSpeed(); /// returns speed of movement animation across the screen, in tiles per second - float getMovementDistance(const CCreature * creature); + float getMovementRange(const CCreature * creature); /// returns speed of movement animation across the screen, in pixels per seconds float getFlightDistance(const CCreature * creature); diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 23aa2ea25..ae4494db4 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -116,7 +116,11 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) if(ev.type == SDL_QUIT) { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); +#ifdef VCMI_ANDROID handleQuit(false); +#else + handleQuit(true); +#endif return; } else if(ev.type == SDL_KEYDOWN) @@ -141,7 +145,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) Settings full = settings.write["video"]["fullscreen"]; full->Bool() = !full->Bool(); - GH.onScreenResize(); + GH.onScreenResize(false); return; } } @@ -159,14 +163,14 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) #ifndef VCMI_IOS { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - GH.onScreenResize(); + GH.onScreenResize(false); } #endif break; case SDL_WINDOWEVENT_FOCUS_GAINED: { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - if(settings["general"]["enableUiEnhancements"].Bool()) { + if(settings["general"]["audioMuteFocus"].Bool()) { CCS->musich->setVolume(settings["general"]["music"].Integer()); CCS->soundh->setVolume(settings["general"]["sound"].Integer()); } @@ -175,7 +179,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) case SDL_WINDOWEVENT_FOCUS_LOST: { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - if(settings["general"]["enableUiEnhancements"].Bool()) { + if(settings["general"]["audioMuteFocus"].Bool()) { CCS->musich->setVolume(0); CCS->soundh->setVolume(0); } @@ -300,7 +304,7 @@ void InputHandler::dispatchMainThread(const std::function & functor) auto heapFunctor = new std::function(functor); SDL_Event event; - event.type = SDL_USEREVENT; + event.user.type = SDL_USEREVENT; event.user.code = 0; event.user.data1 = static_cast (heapFunctor); event.user.data2 = nullptr; @@ -312,6 +316,8 @@ void InputHandler::handleUserEvent(const SDL_UserEvent & current) auto heapFunctor = static_cast*>(current.data1); (*heapFunctor)(); + + delete heapFunctor; } const Point & InputHandler::getCursorPosition() const diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index 01b7c990e..0901dc420 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -16,6 +16,8 @@ #include "../gui/CGuiHandler.h" #include "../gui/EventDispatcher.h" #include "../gui/ShortcutHandler.h" +#include "../CServerHandler.h" +#include "../globalLobby/GlobalLobbyClient.h" #include #include @@ -31,6 +33,8 @@ InputSourceKeyboard::InputSourceKeyboard() void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) { + assert(key.state == SDL_PRESSED); + if (SDL_IsTextInputActive() == SDL_TRUE) { if(key.keysym.sym == SDLK_v && isKeyboardCtrlDown()) @@ -51,7 +55,11 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) return; // ignore periodic event resends } - assert(key.state == SDL_PRESSED); + + if(key.keysym.sym == SDLK_TAB && isKeyboardCtrlDown()) + { + CSH->getGlobalLobby().activateInterface(); + } if(key.keysym.sym >= SDLK_F1 && key.keysym.sym <= SDLK_F15 && settings["session"]["spectate"].Bool()) { diff --git a/client/eventsSDL/InputSourceText.cpp b/client/eventsSDL/InputSourceText.cpp index 72afc978d..96edf5fe1 100644 --- a/client/eventsSDL/InputSourceText.cpp +++ b/client/eventsSDL/InputSourceText.cpp @@ -21,10 +21,6 @@ #include -#ifdef VCMI_APPLE -# include -#endif - void InputSourceText::handleEventTextInput(const SDL_TextInputEvent & text) { GH.events().dispatchTextInput(text.text); @@ -37,38 +33,27 @@ void InputSourceText::handleEventTextEditing(const SDL_TextEditingEvent & text) void InputSourceText::startTextInput(const Rect & whereInput) { - -#ifdef VCMI_APPLE - dispatch_async(dispatch_get_main_queue(), ^{ -#endif - - Rect rectInScreenCoordinates = GH.screenHandler().convertLogicalPointsToWindow(whereInput); - SDL_Rect textInputRect = CSDL_Ext::toSDL(rectInScreenCoordinates); - - SDL_SetTextInputRect(&textInputRect); - - if (SDL_IsTextInputActive() == SDL_FALSE) + GH.dispatchMainThread([whereInput]() { - SDL_StartTextInput(); - } + Rect rectInScreenCoordinates = GH.screenHandler().convertLogicalPointsToWindow(whereInput); + SDL_Rect textInputRect = CSDL_Ext::toSDL(rectInScreenCoordinates); -#ifdef VCMI_APPLE + SDL_SetTextInputRect(&textInputRect); + + if (SDL_IsTextInputActive() == SDL_FALSE) + { + SDL_StartTextInput(); + } }); -#endif } void InputSourceText::stopTextInput() { -#ifdef VCMI_APPLE - dispatch_async(dispatch_get_main_queue(), ^{ -#endif - - if (SDL_IsTextInputActive() == SDL_TRUE) + GH.dispatchMainThread([]() { - SDL_StopTextInput(); - } - -#ifdef VCMI_APPLE + if (SDL_IsTextInputActive() == SDL_TRUE) + { + SDL_StopTextInput(); + } }); -#endif } diff --git a/client/globalLobby/GlobalLobbyClient.cpp b/client/globalLobby/GlobalLobbyClient.cpp new file mode 100644 index 000000000..b5e398b9a --- /dev/null +++ b/client/globalLobby/GlobalLobbyClient.cpp @@ -0,0 +1,366 @@ +/* + * GlobalLobbyClient.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "GlobalLobbyClient.h" + +#include "GlobalLobbyLoginWindow.h" +#include "GlobalLobbyWindow.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../windows/InfoWindows.h" +#include "../CServerHandler.h" +#include "../mainmenu/CMainMenu.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/MetaString.h" +#include "../../lib/TextOperations.h" + +GlobalLobbyClient::GlobalLobbyClient() = default; +GlobalLobbyClient::~GlobalLobbyClient() = default; + +static std::string getCurrentTimeFormatted(int timeOffsetSeconds = 0) +{ + // FIXME: better/unified way to format date + auto timeNowChrono = std::chrono::system_clock::now(); + timeNowChrono += std::chrono::seconds(timeOffsetSeconds); + + return TextOperations::getFormattedTimeLocal(std::chrono::system_clock::to_time_t(timeNowChrono)); +} + +void GlobalLobbyClient::onPacketReceived(const std::shared_ptr &, const std::vector & message) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + JsonNode json(message.data(), message.size()); + + if(json["type"].String() == "accountCreated") + return receiveAccountCreated(json); + + if(json["type"].String() == "operationFailed") + return receiveOperationFailed(json); + + if(json["type"].String() == "loginSuccess") + return receiveLoginSuccess(json); + + if(json["type"].String() == "chatHistory") + return receiveChatHistory(json); + + if(json["type"].String() == "chatMessage") + return receiveChatMessage(json); + + if(json["type"].String() == "activeAccounts") + return receiveActiveAccounts(json); + + if(json["type"].String() == "activeGameRooms") + return receiveActiveGameRooms(json); + + if(json["type"].String() == "joinRoomSuccess") + return receiveJoinRoomSuccess(json); + + if(json["type"].String() == "inviteReceived") + return receiveInviteReceived(json); + + logGlobal->error("Received unexpected message from lobby server: %s", json["type"].String()); +} + +void GlobalLobbyClient::receiveAccountCreated(const JsonNode & json) +{ + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection finished without active login window!"); + + { + Settings configID = settings.write["lobby"]["accountID"]; + configID->String() = json["accountID"].String(); + + Settings configName = settings.write["lobby"]["displayName"]; + configName->String() = json["displayName"].String(); + + Settings configCookie = settings.write["lobby"]["accountCookie"]; + configCookie->String() = json["accountCookie"].String(); + } + + sendClientLogin(); +} + +void GlobalLobbyClient::receiveOperationFailed(const JsonNode & json) +{ + auto loginWindowPtr = loginWindow.lock(); + + if(loginWindowPtr) + loginWindowPtr->onConnectionFailed(json["reason"].String()); + + // TODO: handle errors in lobby menu +} + +void GlobalLobbyClient::receiveLoginSuccess(const JsonNode & json) +{ + { + Settings configCookie = settings.write["lobby"]["accountCookie"]; + configCookie->String() = json["accountCookie"].String(); + + Settings configName = settings.write["lobby"]["displayName"]; + configName->String() = json["displayName"].String(); + } + + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection finished without active login window!"); + + loginWindowPtr->onConnectionSuccess(); +} + +void GlobalLobbyClient::receiveChatHistory(const JsonNode & json) +{ + for(const auto & entry : json["messages"].Vector()) + { + std::string accountID = entry["accountID"].String(); + std::string displayName = entry["displayName"].String(); + std::string messageText = entry["messageText"].String(); + int ageSeconds = entry["ageSeconds"].Integer(); + std::string timeFormatted = getCurrentTimeFormatted(-ageSeconds); + + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted); + } +} + +void GlobalLobbyClient::receiveChatMessage(const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + std::string displayName = json["displayName"].String(); + std::string messageText = json["messageText"].String(); + std::string timeFormatted = getCurrentTimeFormatted(); + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onGameChatMessage(displayName, messageText, timeFormatted); +} + +void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json) +{ + activeAccounts.clear(); + + for (auto const & jsonEntry : json["accounts"].Vector()) + { + GlobalLobbyAccount account; + + account.accountID = jsonEntry["accountID"].String(); + account.displayName = jsonEntry["displayName"].String(); + account.status = jsonEntry["status"].String(); + + activeAccounts.push_back(account); + } + + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onActiveAccounts(activeAccounts); +} + +void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json) +{ + activeRooms.clear(); + + for (auto const & jsonEntry : json["gameRooms"].Vector()) + { + GlobalLobbyRoom room; + + room.gameRoomID = jsonEntry["gameRoomID"].String(); + room.hostAccountID = jsonEntry["hostAccountID"].String(); + room.hostAccountDisplayName = jsonEntry["hostAccountDisplayName"].String(); + room.description = jsonEntry["description"].String(); + room.playersCount = jsonEntry["playersCount"].Integer(); + room.playersLimit = jsonEntry["playersLimit"].Integer(); + + activeRooms.push_back(room); + } + + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + lobbyWindowPtr->onActiveRooms(activeRooms); +} + +void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json) +{ + assert(0); //TODO +} + +void GlobalLobbyClient::receiveJoinRoomSuccess(const JsonNode & json) +{ + Settings configRoom = settings.write["lobby"]["roomID"]; + configRoom->String() = json["gameRoomID"].String(); + + if (json["proxyMode"].Bool()) + { + CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_GUEST, {}); + CSH->loadMode = ELoadMode::MULTI; + + std::string hostname = settings["lobby"]["hostname"].String(); + int16_t port = settings["lobby"]["port"].Integer(); + CSH->connectToServer(hostname, port); + } +} + +void GlobalLobbyClient::onConnectionEstablished(const std::shared_ptr & connection) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + networkConnection = connection; + + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection established without active login window!"); + + loginWindowPtr->onConnectionSuccess(); +} + +void GlobalLobbyClient::sendClientRegister(const std::string & accountName) +{ + JsonNode toSend; + toSend["type"].String() = "clientRegister"; + toSend["displayName"].String() = accountName; + sendMessage(toSend); +} + +void GlobalLobbyClient::sendClientLogin() +{ + JsonNode toSend; + toSend["type"].String() = "clientLogin"; + toSend["accountID"] = settings["lobby"]["accountID"]; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + sendMessage(toSend); +} + +void GlobalLobbyClient::onConnectionFailed(const std::string & errorMessage) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + auto loginWindowPtr = loginWindow.lock(); + + if(!loginWindowPtr || !GH.windows().topWindow()) + throw std::runtime_error("lobby connection failed without active login window!"); + + logGlobal->warn("Connection to game lobby failed! Reason: %s", errorMessage); + loginWindowPtr->onConnectionFailed(errorMessage); +} + +void GlobalLobbyClient::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + assert(connection == networkConnection); + networkConnection.reset(); + + while (!GH.windows().findWindows().empty()) + { + // if global lobby is open, pop all dialogs on top of it as well as lobby itself + GH.windows().popWindows(1); + } + + CInfoWindow::showInfoDialog("Connection to game lobby was lost!", {}); +} + +void GlobalLobbyClient::sendMessage(const JsonNode & data) +{ + networkConnection->sendPacket(data.toBytes()); +} + +void GlobalLobbyClient::sendOpenPublicRoom() +{ + JsonNode toSend; + toSend["type"].String() = "openGameRoom"; + toSend["hostAccountID"] = settings["lobby"]["accountID"]; + toSend["roomType"].String() = "public"; + sendMessage(toSend); +} + +void GlobalLobbyClient::sendOpenPrivateRoom() +{ + JsonNode toSend; + toSend["type"].String() = "openGameRoom"; + toSend["hostAccountID"] = settings["lobby"]["accountID"]; + toSend["roomType"].String() = "private"; + sendMessage(toSend); +} + +void GlobalLobbyClient::connect() +{ + std::string hostname = settings["lobby"]["hostname"].String(); + int16_t port = settings["lobby"]["port"].Integer(); + CSH->getNetworkHandler().connectToRemote(*this, hostname, port); +} + +bool GlobalLobbyClient::isConnected() const +{ + return networkConnection != nullptr; +} + +std::shared_ptr GlobalLobbyClient::createLoginWindow() +{ + auto loginWindowPtr = loginWindow.lock(); + if(loginWindowPtr) + return loginWindowPtr; + + auto loginWindowNew = std::make_shared(); + loginWindow = loginWindowNew; + + return loginWindowNew; +} + +std::shared_ptr GlobalLobbyClient::createLobbyWindow() +{ + auto lobbyWindowPtr = lobbyWindow.lock(); + if(lobbyWindowPtr) + return lobbyWindowPtr; + + lobbyWindowPtr = std::make_shared(); + lobbyWindow = lobbyWindowPtr; + lobbyWindowLock = lobbyWindowPtr; + return lobbyWindowPtr; +} + +const std::vector & GlobalLobbyClient::getActiveAccounts() const +{ + return activeAccounts; +} + +const std::vector & GlobalLobbyClient::getActiveRooms() const +{ + return activeRooms; +} + +void GlobalLobbyClient::activateInterface() +{ + if (!GH.windows().findWindows().empty()) + return; + + if (!GH.windows().findWindows().empty()) + return; + + if (isConnected()) + GH.windows().pushWindow(createLobbyWindow()); + else + GH.windows().pushWindow(createLoginWindow()); +} + +void GlobalLobbyClient::sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection) +{ + JsonNode toSend; + toSend["type"].String() = "clientProxyLogin"; + toSend["accountID"] = settings["lobby"]["accountID"]; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + toSend["gameRoomID"] = settings["lobby"]["roomID"]; + + netConnection->sendPacket(toSend.toBytes()); +} diff --git a/client/globalLobby/GlobalLobbyClient.h b/client/globalLobby/GlobalLobbyClient.h new file mode 100644 index 000000000..b269c7018 --- /dev/null +++ b/client/globalLobby/GlobalLobbyClient.h @@ -0,0 +1,70 @@ +/* + * GlobalLobbyClient.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "GlobalLobbyDefines.h" +#include "../../lib/network/NetworkInterface.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class GlobalLobbyLoginWindow; +class GlobalLobbyWindow; + +class GlobalLobbyClient final : public INetworkClientListener, boost::noncopyable +{ + std::vector activeAccounts; + std::vector activeRooms; + + std::shared_ptr networkConnection; + + std::weak_ptr loginWindow; + std::weak_ptr lobbyWindow; + std::shared_ptr lobbyWindowLock; // helper strong reference to prevent window destruction on closing + + void onPacketReceived(const std::shared_ptr &, const std::vector & message) override; + void onConnectionFailed(const std::string & errorMessage) override; + void onConnectionEstablished(const std::shared_ptr &) override; + void onDisconnected(const std::shared_ptr &, const std::string & errorMessage) override; + + void receiveAccountCreated(const JsonNode & json); + void receiveOperationFailed(const JsonNode & json); + void receiveLoginSuccess(const JsonNode & json); + void receiveChatHistory(const JsonNode & json); + void receiveChatMessage(const JsonNode & json); + void receiveActiveAccounts(const JsonNode & json); + void receiveActiveGameRooms(const JsonNode & json); + void receiveJoinRoomSuccess(const JsonNode & json); + void receiveInviteReceived(const JsonNode & json); + + std::shared_ptr createLoginWindow(); + std::shared_ptr createLobbyWindow(); + +public: + explicit GlobalLobbyClient(); + ~GlobalLobbyClient(); + + const std::vector & getActiveAccounts() const; + const std::vector & getActiveRooms() const; + + /// Activate interface and pushes lobby UI as top window + void activateInterface(); + void sendMessage(const JsonNode & data); + void sendClientRegister(const std::string & accountName); + void sendClientLogin(); + void sendOpenPublicRoom(); + void sendOpenPrivateRoom(); + + void sendProxyConnectionLogin(const NetworkConnectionPtr & netConnection); + + void connect(); + bool isConnected() const; +}; diff --git a/client/globalLobby/GlobalLobbyDefines.h b/client/globalLobby/GlobalLobbyDefines.h new file mode 100644 index 000000000..ae61d3516 --- /dev/null +++ b/client/globalLobby/GlobalLobbyDefines.h @@ -0,0 +1,27 @@ +/* + * GlobalLobbyClient.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +struct GlobalLobbyAccount +{ + std::string accountID; + std::string displayName; + std::string status; +}; + +struct GlobalLobbyRoom +{ + std::string gameRoomID; + std::string hostAccountID; + std::string hostAccountDisplayName; + std::string description; + int playersCount; + int playersLimit; +}; diff --git a/client/globalLobby/GlobalLobbyLoginWindow.cpp b/client/globalLobby/GlobalLobbyLoginWindow.cpp new file mode 100644 index 000000000..02fbcc342 --- /dev/null +++ b/client/globalLobby/GlobalLobbyLoginWindow.cpp @@ -0,0 +1,129 @@ +/* + * GlobalLobbyLoginWindow.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "GlobalLobbyLoginWindow.h" + +#include "GlobalLobbyClient.h" +#include "GlobalLobbyWindow.h" + +#include "../CGameInfo.h" +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/TextControls.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/MetaString.h" + +GlobalLobbyLoginWindow::GlobalLobbyLoginWindow() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos.w = 284; + pos.h = 220; + + filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title")); + labelUsername = std::make_shared( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username")); + backgroundUsername = std::make_shared(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); + inputUsername = std::make_shared(Rect(15, 93, 260, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true); + buttonLogin = std::make_shared(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }); + buttonClose = std::make_shared(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }); + labelStatus = std::make_shared( "", Rect(15, 115, 255, 60), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + + auto buttonRegister = std::make_shared(Point(10, 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + auto buttonLogin = std::make_shared(Point(146, 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + buttonRegister->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.login.create"), EFonts::FONT_SMALL, Colors::YELLOW); + buttonLogin->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.login.login"), EFonts::FONT_SMALL, Colors::YELLOW); + + toggleMode = std::make_shared(nullptr); + toggleMode->addToggle(0, buttonRegister); + toggleMode->addToggle(1, buttonLogin); + toggleMode->setSelected(settings["lobby"]["roomType"].Integer()); + toggleMode->addCallback([this](int index){onLoginModeChanged(index);}); + + if (settings["lobby"]["accountID"].String().empty()) + { + buttonLogin->block(true); + toggleMode->setSelected(0); + } + else + toggleMode->setSelected(1); + + filledBackground->playerColored(PlayerColor(1)); + inputUsername->cb += [this](const std::string & text) + { + this->buttonLogin->block(text.empty()); + }; + + center(); +} + +void GlobalLobbyLoginWindow::onLoginModeChanged(int value) +{ + if (value == 0) + { + inputUsername->setText(""); + } + else + { + inputUsername->setText(settings["lobby"]["displayName"].String()); + } +} + +void GlobalLobbyLoginWindow::onClose() +{ + close(); + // TODO: abort ongoing connection attempt, if any +} + +void GlobalLobbyLoginWindow::onLogin() +{ + labelStatus->setText(CGI->generaltexth->translate("vcmi.lobby.login.connecting")); + if(!CSH->getGlobalLobby().isConnected()) + CSH->getGlobalLobby().connect(); + else + onConnectionSuccess(); + + buttonClose->block(true); +} + +void GlobalLobbyLoginWindow::onConnectionSuccess() +{ + std::string accountID = settings["lobby"]["accountID"].String(); + + if(toggleMode->getSelected() == 0) + CSH->getGlobalLobby().sendClientRegister(inputUsername->getText()); + else + CSH->getGlobalLobby().sendClientLogin(); +} + +void GlobalLobbyLoginWindow::onLoginSuccess() +{ + close(); + CSH->getGlobalLobby().activateInterface(); +} + +void GlobalLobbyLoginWindow::onConnectionFailed(const std::string & reason) +{ + MetaString formatter; + formatter.appendTextID("vcmi.lobby.login.error"); + formatter.replaceRawString(reason); + + labelStatus->setText(formatter.toString()); + buttonClose->block(false); +} diff --git a/client/globalLobby/GlobalLobbyLoginWindow.h b/client/globalLobby/GlobalLobbyLoginWindow.h new file mode 100644 index 000000000..a589ac650 --- /dev/null +++ b/client/globalLobby/GlobalLobbyLoginWindow.h @@ -0,0 +1,45 @@ +/* + * GlobalLobbyLoginWindow.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../windows/CWindowObject.h" + +class CLabel; +class CTextBox; +class CTextInput; +class CToggleGroup; +class FilledTexturePlayerColored; +class TransparentFilledRectangle; +class CButton; + +class GlobalLobbyLoginWindow : public CWindowObject +{ + std::shared_ptr filledBackground; + std::shared_ptr labelTitle; + std::shared_ptr labelUsername; + std::shared_ptr labelStatus; + std::shared_ptr backgroundUsername; + std::shared_ptr inputUsername; + + std::shared_ptr buttonLogin; + std::shared_ptr buttonClose; + std::shared_ptr toggleMode; // create account or use existing + + void onLoginModeChanged(int value); + void onClose(); + void onLogin(); + +public: + GlobalLobbyLoginWindow(); + + void onConnectionSuccess(); + void onLoginSuccess(); + void onConnectionFailed(const std::string & reason); +}; diff --git a/client/globalLobby/GlobalLobbyServerSetup.cpp b/client/globalLobby/GlobalLobbyServerSetup.cpp new file mode 100644 index 000000000..3e1caa98a --- /dev/null +++ b/client/globalLobby/GlobalLobbyServerSetup.cpp @@ -0,0 +1,142 @@ +/* + * GlobalLobbyServerSetup.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "GlobalLobbyServerSetup.h" + +#include "../CGameInfo.h" +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../mainmenu/CMainMenu.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/MetaString.h" + +GlobalLobbyServerSetup::GlobalLobbyServerSetup() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos.w = 284; + pos.h = 340; + + filledBackground = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + labelTitle = std::make_shared( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.create")); + labelPlayerLimit = std::make_shared( pos.w / 2, 48, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.players.limit")); + labelRoomType = std::make_shared( pos.w / 2, 108, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.type")); + labelGameMode = std::make_shared( pos.w / 2, 158, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.mode")); + + togglePlayerLimit = std::make_shared(nullptr); + togglePlayerLimit->addToggle(2, std::make_shared(Point(10 + 39*0, 60), AnimationPath::builtin("RanNum2"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(3, std::make_shared(Point(10 + 39*1, 60), AnimationPath::builtin("RanNum3"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(4, std::make_shared(Point(10 + 39*2, 60), AnimationPath::builtin("RanNum4"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(5, std::make_shared(Point(10 + 39*3, 60), AnimationPath::builtin("RanNum5"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(6, std::make_shared(Point(10 + 39*4, 60), AnimationPath::builtin("RanNum6"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(7, std::make_shared(Point(10 + 39*5, 60), AnimationPath::builtin("RanNum7"), CButton::tooltip(), 0)); + togglePlayerLimit->addToggle(8, std::make_shared(Point(10 + 39*6, 60), AnimationPath::builtin("RanNum8"), CButton::tooltip(), 0)); + togglePlayerLimit->setSelected(settings["lobby"]["roomPlayerLimit"].Integer()); + togglePlayerLimit->addCallback([this](int index){onPlayerLimitChanged(index);}); + + auto buttonPublic = std::make_shared(Point(10, 120), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + auto buttonPrivate = std::make_shared(Point(146, 120), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + buttonPublic->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.public"), EFonts::FONT_SMALL, Colors::YELLOW); + buttonPrivate->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.private"), EFonts::FONT_SMALL, Colors::YELLOW); + + toggleRoomType = std::make_shared(nullptr); + toggleRoomType->addToggle(0, buttonPublic); + toggleRoomType->addToggle(1, buttonPrivate); + toggleRoomType->setSelected(settings["lobby"]["roomType"].Integer()); + toggleRoomType->addCallback([this](int index){onRoomTypeChanged(index);}); + + auto buttonNewGame = std::make_shared(Point(10, 170), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + auto buttonLoadGame = std::make_shared(Point(146, 170), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0); + buttonNewGame->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.new"), EFonts::FONT_SMALL, Colors::YELLOW); + buttonLoadGame->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.room.load"), EFonts::FONT_SMALL, Colors::YELLOW); + + toggleGameMode = std::make_shared(nullptr); + toggleGameMode->addToggle(0, buttonNewGame); + toggleGameMode->addToggle(1, buttonLoadGame); + toggleGameMode->setSelected(settings["lobby"]["roomMode"].Integer()); + toggleGameMode->addCallback([this](int index){onGameModeChanged(index);}); + + labelDescription = std::make_shared("", Rect(10, 195, pos.w - 20, 80), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + + buttonCreate = std::make_shared(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); }); + buttonClose = std::make_shared(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }); + + filledBackground->playerColored(PlayerColor(1)); + + updateDescription(); + center(); +} + +void GlobalLobbyServerSetup::updateDescription() +{ + MetaString description; + description.appendRawString("%s %s %s"); + if(toggleRoomType->getSelected() == 0) + description.replaceTextID("vcmi.lobby.room.description.public"); + else + description.replaceTextID("vcmi.lobby.room.description.private"); + + if(toggleGameMode->getSelected() == 0) + description.replaceTextID("vcmi.lobby.room.description.new"); + else + description.replaceTextID("vcmi.lobby.room.description.load"); + + description.replaceTextID("vcmi.lobby.room.description.limit"); + description.replaceNumber(togglePlayerLimit->getSelected()); + + labelDescription->setText(description.toString()); +} + +void GlobalLobbyServerSetup::onPlayerLimitChanged(int value) +{ + Settings config = settings.write["lobby"]["roomPlayerLimit"]; + config->Integer() = value; + updateDescription(); +} + +void GlobalLobbyServerSetup::onRoomTypeChanged(int value) +{ + Settings config = settings.write["lobby"]["roomType"]; + config->Integer() = value; + updateDescription(); +} + +void GlobalLobbyServerSetup::onGameModeChanged(int value) +{ + Settings config = settings.write["lobby"]["roomMode"]; + config->Integer() = value; + updateDescription(); +} + +void GlobalLobbyServerSetup::onCreate() +{ + if(toggleGameMode->getSelected() == 0) + CSH->resetStateForLobby(EStartMode::NEW_GAME, ESelectionScreen::newGame, EServerMode::LOBBY_HOST, {}); + else + CSH->resetStateForLobby(EStartMode::LOAD_GAME, ESelectionScreen::loadGame, EServerMode::LOBBY_HOST, {}); + + CSH->loadMode = ELoadMode::MULTI; + CSH->startLocalServerAndConnect(true); + + buttonCreate->block(true); + buttonClose->block(true); +} + +void GlobalLobbyServerSetup::onClose() +{ + close(); +} diff --git a/client/globalLobby/GlobalLobbyServerSetup.h b/client/globalLobby/GlobalLobbyServerSetup.h new file mode 100644 index 000000000..f8fa09d34 --- /dev/null +++ b/client/globalLobby/GlobalLobbyServerSetup.h @@ -0,0 +1,49 @@ +/* + * GlobalLobbyServerSetup.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../windows/CWindowObject.h" + +class CLabel; +class CTextBox; +class FilledTexturePlayerColored; +class CButton; +class CToggleGroup; + +class GlobalLobbyServerSetup : public CWindowObject +{ + std::shared_ptr filledBackground; + std::shared_ptr labelTitle; + + std::shared_ptr labelPlayerLimit; + std::shared_ptr labelRoomType; + std::shared_ptr labelGameMode; + + std::shared_ptr togglePlayerLimit; // 2-8 + std::shared_ptr toggleRoomType; // public or private + std::shared_ptr toggleGameMode; // new game or load game + + std::shared_ptr labelDescription; + std::shared_ptr labelStatus; + + std::shared_ptr buttonCreate; + std::shared_ptr buttonClose; + + void updateDescription(); + void onPlayerLimitChanged(int value); + void onRoomTypeChanged(int value); + void onGameModeChanged(int value); + + void onCreate(); + void onClose(); + +public: + GlobalLobbyServerSetup(); +}; diff --git a/client/globalLobby/GlobalLobbyWidget.cpp b/client/globalLobby/GlobalLobbyWidget.cpp new file mode 100644 index 000000000..0ffe23254 --- /dev/null +++ b/client/globalLobby/GlobalLobbyWidget.cpp @@ -0,0 +1,155 @@ +/* + * GlobalLobbyWidget.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "GlobalLobbyWidget.h" + +#include "GlobalLobbyClient.h" +#include "GlobalLobbyWindow.h" + +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/ObjectLists.h" +#include "../widgets/TextControls.h" + +#include "../../lib/MetaString.h" +GlobalLobbyWidget::GlobalLobbyWidget(GlobalLobbyWindow * window) + : window(window) +{ + addCallback("closeWindow", [](int) { GH.windows().popWindows(1); }); + addCallback("sendMessage", [this](int) { this->window->doSendChatMessage(); }); + addCallback("createGameRoom", [this](int) { this->window->doCreateGameRoom(); }); + + REGISTER_BUILDER("accountList", &GlobalLobbyWidget::buildAccountList); + REGISTER_BUILDER("roomList", &GlobalLobbyWidget::buildRoomList); + + const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json")); + build(config); +} + +std::shared_ptr GlobalLobbyWidget::buildAccountList(const JsonNode & config) const +{ + const auto & createCallback = [this](size_t index) -> std::shared_ptr + { + const auto & accounts = CSH->getGlobalLobby().getActiveAccounts(); + + if(index < accounts.size()) + return std::make_shared(this->window, accounts[index]); + return std::make_shared(); + }; + + auto position = readPosition(config["position"]); + auto itemOffset = readPosition(config["itemOffset"]); + auto sliderPosition = readPosition(config["sliderPosition"]); + auto sliderSize = readPosition(config["sliderSize"]); + size_t visibleSize = 4; // FIXME: how many items can fit into UI? + size_t totalSize = 4; //FIXME: how many items are there in total + int sliderMode = 1 | 4; // present, vertical, blue + int initialPos = 0; + + return std::make_shared(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) ); +} + +std::shared_ptr GlobalLobbyWidget::buildRoomList(const JsonNode & config) const +{ + const auto & createCallback = [this](size_t index) -> std::shared_ptr + { + const auto & rooms = CSH->getGlobalLobby().getActiveRooms(); + + if(index < rooms.size()) + return std::make_shared(this->window, rooms[index]); + return std::make_shared(); + }; + + auto position = readPosition(config["position"]); + auto itemOffset = readPosition(config["itemOffset"]); + auto sliderPosition = readPosition(config["sliderPosition"]); + auto sliderSize = readPosition(config["sliderSize"]); + size_t visibleSize = 4; // FIXME: how many items can fit into UI? + size_t totalSize = 4; //FIXME: how many items are there in total + int sliderMode = 1 | 4; // present, vertical, blue + int initialPos = 0; + + return std::make_shared(createCallback, position, itemOffset, visibleSize, totalSize, initialPos, sliderMode, Rect(sliderPosition, sliderSize) ); +} + +std::shared_ptr GlobalLobbyWidget::getAccountNameLabel() +{ + return widget("accountNameLabel"); +} + +std::shared_ptr GlobalLobbyWidget::getMessageInput() +{ + return widget("messageInput"); +} + +std::shared_ptr GlobalLobbyWidget::getGameChat() +{ + return widget("gameChat"); +} + +std::shared_ptr GlobalLobbyWidget::getAccountList() +{ + return widget("accountList"); +} + +std::shared_ptr GlobalLobbyWidget::getRoomList() +{ + return widget("roomList"); +} + +GlobalLobbyAccountCard::GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + const auto & onInviteClicked = [window, accountID=accountDescription.accountID]() + { + window->doInviteAccount(accountID); + }; + + pos.w = 130; + pos.h = 40; + + backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); + labelName = std::make_shared( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, accountDescription.displayName); + labelStatus = std::make_shared( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, accountDescription.status); + + if (CSH->inLobbyRoom()) + buttonInvite = std::make_shared(Point(95, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onInviteClicked); +} + +GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + const auto & onJoinClicked = [window, roomID=roomDescription.gameRoomID]() + { + window->doJoinRoom(roomID); + }; + + auto roomSizeText = MetaString::createFromRawString("%d/%d"); + roomSizeText.replaceNumber(roomDescription.playersCount); + roomSizeText.replaceNumber(roomDescription.playersLimit); + + pos.w = 230; + pos.h = 40; + + backgroundOverlay = std::make_shared(Rect(0, 0, pos.w, pos.h), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64)); + labelName = std::make_shared( 5, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, roomDescription.hostAccountDisplayName); + labelStatus = std::make_shared( 5, 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomDescription.description); + labelRoomSize = std::make_shared( 160, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, roomSizeText.toString()); + + if (!CSH->inGame()) + buttonJoin = std::make_shared(Point(195, 8), AnimationPath::builtin("settingsWindow/button32"), CButton::tooltip(), onJoinClicked); +} diff --git a/client/globalLobby/GlobalLobbyWidget.h b/client/globalLobby/GlobalLobbyWidget.h new file mode 100644 index 000000000..c4a52e688 --- /dev/null +++ b/client/globalLobby/GlobalLobbyWidget.h @@ -0,0 +1,57 @@ +/* + * GlobalLobbyWidget.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../gui/InterfaceObjectConfigurable.h" + +class GlobalLobbyWindow; +struct GlobalLobbyAccount; +struct GlobalLobbyRoom; +class CListBox; + +class GlobalLobbyWidget : public InterfaceObjectConfigurable +{ + GlobalLobbyWindow * window; + + std::shared_ptr buildAccountList(const JsonNode &) const; + std::shared_ptr buildRoomList(const JsonNode &) const; + +public: + explicit GlobalLobbyWidget(GlobalLobbyWindow * window); + + std::shared_ptr getAccountNameLabel(); + std::shared_ptr getMessageInput(); + std::shared_ptr getGameChat(); + std::shared_ptr getAccountList(); + std::shared_ptr getRoomList(); +}; + +class GlobalLobbyAccountCard : public CIntObject +{ +public: + GlobalLobbyAccountCard(GlobalLobbyWindow * window, const GlobalLobbyAccount & accountDescription); + + std::shared_ptr backgroundOverlay; + std::shared_ptr labelName; + std::shared_ptr labelStatus; + std::shared_ptr buttonInvite; +}; + +class GlobalLobbyRoomCard : public CIntObject +{ +public: + GlobalLobbyRoomCard(GlobalLobbyWindow * window, const GlobalLobbyRoom & roomDescription); + + std::shared_ptr backgroundOverlay; + std::shared_ptr labelName; + std::shared_ptr labelRoomSize; + std::shared_ptr labelStatus; + std::shared_ptr buttonJoin; +}; diff --git a/client/globalLobby/GlobalLobbyWindow.cpp b/client/globalLobby/GlobalLobbyWindow.cpp new file mode 100644 index 000000000..5ef963cfc --- /dev/null +++ b/client/globalLobby/GlobalLobbyWindow.cpp @@ -0,0 +1,105 @@ +/* + * GlobalLobbyWindow.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "GlobalLobbyWindow.h" + +#include "GlobalLobbyClient.h" +#include "GlobalLobbyServerSetup.h" +#include "GlobalLobbyWidget.h" + +#include "../CServerHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/TextControls.h" +#include "../widgets/ObjectLists.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/MetaString.h" + +GlobalLobbyWindow::GlobalLobbyWindow() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + widget = std::make_shared(this); + pos = widget->pos; + center(); + + widget->getAccountNameLabel()->setText(settings["lobby"]["displayName"].String()); +} + +void GlobalLobbyWindow::doSendChatMessage() +{ + std::string messageText = widget->getMessageInput()->getText(); + + JsonNode toSend; + toSend["type"].String() = "sendChatMessage"; + toSend["messageText"].String() = messageText; + + CSH->getGlobalLobby().sendMessage(toSend); + + widget->getMessageInput()->setText(""); +} + +void GlobalLobbyWindow::doCreateGameRoom() +{ + GH.windows().createAndPushWindow(); +} + +void GlobalLobbyWindow::doInviteAccount(const std::string & accountID) +{ + JsonNode toSend; + toSend["type"].String() = "sendInvite"; + toSend["accountID"].String() = accountID; + + CSH->getGlobalLobby().sendMessage(toSend); +} + +void GlobalLobbyWindow::doJoinRoom(const std::string & roomID) +{ + JsonNode toSend; + toSend["type"].String() = "joinGameRoom"; + toSend["gameRoomID"].String() = roomID; + + CSH->getGlobalLobby().sendMessage(toSend); +} + +void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when) +{ + MetaString chatMessageFormatted; + chatMessageFormatted.appendRawString("[%s] {%s}: %s\n"); + chatMessageFormatted.replaceRawString(when); + chatMessageFormatted.replaceRawString(sender); + chatMessageFormatted.replaceRawString(message); + + chatHistory += chatMessageFormatted.toString(); + + widget->getGameChat()->setText(chatHistory); +} + +void GlobalLobbyWindow::onActiveAccounts(const std::vector & accounts) +{ + widget->getAccountList()->reset(); +} + +void GlobalLobbyWindow::onActiveRooms(const std::vector & rooms) +{ + widget->getRoomList()->reset(); +} + +void GlobalLobbyWindow::onJoinedRoom() +{ + widget->getAccountList()->reset(); +} + +void GlobalLobbyWindow::onLeftRoom() +{ + widget->getAccountList()->reset(); +} diff --git a/client/globalLobby/GlobalLobbyWindow.h b/client/globalLobby/GlobalLobbyWindow.h new file mode 100644 index 000000000..d6d868653 --- /dev/null +++ b/client/globalLobby/GlobalLobbyWindow.h @@ -0,0 +1,38 @@ +/* + * GlobalLobbyWindow.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../windows/CWindowObject.h" + +class GlobalLobbyWidget; +struct GlobalLobbyAccount; +struct GlobalLobbyRoom; + +class GlobalLobbyWindow : public CWindowObject +{ + std::string chatHistory; + + std::shared_ptr widget; + +public: + GlobalLobbyWindow(); + + void doSendChatMessage(); + void doCreateGameRoom(); + + void doInviteAccount(const std::string & accountID); + void doJoinRoom(const std::string & roomID); + + void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when); + void onActiveAccounts(const std::vector & accounts); + void onActiveRooms(const std::vector & rooms); + void onJoinedRoom(); + void onLeftRoom(); +}; diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index db3838b61..d8d56eb2c 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -114,16 +114,20 @@ void CGuiHandler::renderFrame() { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - if(nullptr != curInt) + if (nullptr != curInt) curInt->update(); - if(settings["video"]["showfps"].Bool()) + if (settings["video"]["showfps"].Bool()) drawFPSCounter(); + } - SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); + SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); - SDL_RenderClear(mainRenderer); - SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); + SDL_RenderClear(mainRenderer); + SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); + + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); CCS->curh->render(); @@ -183,11 +187,17 @@ Point CGuiHandler::screenDimensions() const void CGuiHandler::drawFPSCounter() { - static SDL_Rect overlay = { 0, 0, 24, 24}; + int x = 7; + int y = screen->h-20; + int width3digitFPSIncludingPadding = 48; + int heightFPSTextIncludingPadding = 11; + SDL_Rect overlay = { x, y, width3digitFPSIncludingPadding, heightFPSTextIncludingPadding}; uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10); SDL_FillRect(screen, &overlay, black); - std::string fps = std::to_string(framerate().getFramerate()); - graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(4, 2)); + + std::string fps = std::to_string(framerate().getFramerate())+" FPS"; + + graphics->fonts[FONT_SMALL]->renderTextLeft(screen, fps, Colors::WHITE, Point(8, screen->h-22)); } bool CGuiHandler::amIGuiThread() @@ -241,8 +251,11 @@ void CGuiHandler::setStatusbar(std::shared_ptr newStatusBar) currentStatusBar = newStatusBar; } -void CGuiHandler::onScreenResize() +void CGuiHandler::onScreenResize(bool resolutionChanged) { - screenHandler().onScreenResize(); + if(resolutionChanged) + { + screenHandler().onScreenResize(); + } windows().onScreenResize(); } diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index 4f651c159..a8a555264 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -92,8 +92,8 @@ public: void init(); void renderFrame(); - /// called whenever user selects different resolution, requiring to center/resize all windows - void onScreenResize(); + /// called whenever SDL_WINDOWEVENT_RESTORED is reported or the user selects a different resolution, requiring to center/resize all windows + void onScreenResize(bool resolutionChanged); void handleEvents(); //takes events from queue and calls interested objects void fakeMouseMove(); diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 150e55b8a..71bda7911 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -19,6 +19,10 @@ class CGuiHandler; class CPicture; class Canvas; +VCMI_LIB_NAMESPACE_BEGIN +class CArmedInstance; +VCMI_LIB_NAMESPACE_END + class IUpdateable { public: @@ -104,7 +108,7 @@ public: bool isPopupWindow() const override; /// called only for windows whenever screen size changes - /// default behavior is to re-center, can be overriden + /// default behavior is to re-center, can be overridden void onScreenResize() override; /// returns true if UI elements wants to handle event of specific type (LCLICK, SHOW_POPUP ...) @@ -150,6 +154,15 @@ protected: class IGarrisonHolder { public: + bool holdsGarrisons(std::vector armies) + { + for (auto const * army : armies) + if (holdsGarrison(army)) + return true; + return false; + } + + virtual bool holdsGarrison(const CArmedInstance * army) = 0; virtual void updateGarrisons() = 0; }; diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 935dc88da..320d999d9 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -22,14 +22,15 @@ #include "../widgets/CComponent.h" #include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" -#include "../widgets/MiscWidgets.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/ObjectLists.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" -#include "../../lib//constants/StringConstants.h" +#include "../../lib/constants/StringConstants.h" +#include "../../lib/json/JsonUtils.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/filesystem/ResourcePath.h" @@ -56,6 +57,7 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset) REGISTER_BUILDER("layout", &InterfaceObjectConfigurable::buildLayout); REGISTER_BUILDER("comboBox", &InterfaceObjectConfigurable::buildComboBox); REGISTER_BUILDER("textInput", &InterfaceObjectConfigurable::buildTextInput); + REGISTER_BUILDER("graphicalPrimitive", &InterfaceObjectConfigurable::buildGraphicalPrimitive); REGISTER_BUILDER("transparentFilledRectangle", &InterfaceObjectConfigurable::buildTransparentFilledRectangle); REGISTER_BUILDER("textBox", &InterfaceObjectConfigurable::buildTextBox); } @@ -112,8 +114,20 @@ void InterfaceObjectConfigurable::build(const JsonNode &config) { if (!config["library"].isNull()) { - const JsonNode library(JsonPath::fromJson(config["library"])); - loadCustomBuilders(library); + if (config["library"].isString()) + { + const JsonNode library(JsonPath::fromJson(config["library"])); + loadCustomBuilders(library); + } + + if (config["library"].isVector()) + { + for (auto const & entry : config["library"].Vector()) + { + const JsonNode library(JsonPath::fromJson(entry)); + loadCustomBuilders(library); + } + } } loadCustomBuilders(config["customTypes"]); @@ -129,6 +143,12 @@ void InterfaceObjectConfigurable::build(const JsonNode &config) for(const auto & item : items->Vector()) addWidget(item["name"].String(), buildWidget(item)); + + // load only if set + if (!config["width"].isNull()) + pos.w = config["width"].Integer(); + if (!config["height"].isNull()) + pos.h = config["height"].Integer(); } void InterfaceObjectConfigurable::addConditional(const std::string & name, bool active) @@ -301,7 +321,7 @@ EShortcut InterfaceObjectConfigurable::readHotkey(const JsonNode & config) const EShortcut result = GH.shortcuts().findShortcut(config.String()); if (result == EShortcut::NONE) logGlobal->error("Invalid hotkey '%s' in interface configuration!", config.String()); - return result;; + return result; } std::shared_ptr InterfaceObjectConfigurable::buildPicture(const JsonNode & config) const @@ -310,8 +330,6 @@ std::shared_ptr InterfaceObjectConfigurable::buildPicture(const JsonNo auto image = ImagePath::fromJson(config["image"]); auto position = readPosition(config["position"]); auto pic = std::make_shared(image, position.x, position.y); - if(!config["visible"].isNull()) - pic->visible = config["visible"].Bool(); if ( config["playerColored"].Bool() && LOCPLINT) pic->colorize(LOCPLINT->playerID); @@ -378,7 +396,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildToggleButton(co { for(const auto & item : config["items"].Vector()) { - button->addOverlay(buildWidget(item)); + button->setOverlay(buildWidget(item)); } } if(!config["selected"].isNull()) @@ -404,7 +422,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildButton(const JsonNode { for(const auto & item : config["items"].Vector()) { - button->addOverlay(buildWidget(item)); + button->setOverlay(buildWidget(item)); } } if(!config["imageOrder"].isNull()) @@ -501,12 +519,25 @@ std::shared_ptr InterfaceObjectConfigurable::buildSlider(const JsonNode auto position = readPosition(config["position"]); int length = config["size"].Integer(); auto style = config["style"].String() == "brown" ? CSlider::BROWN : CSlider::BLUE; - auto itemsVisible = config["itemsVisible"].Integer(); - auto itemsTotal = config["itemsTotal"].Integer(); auto value = config["selected"].Integer(); bool horizontal = config["orientation"].String() == "horizontal"; - const auto & result = - std::make_shared(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL, style); + auto orientation = horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL; + + std::shared_ptr result; + + if (config["items"].isNull()) + { + auto itemsVisible = config["itemsVisible"].Integer(); + auto itemsTotal = config["itemsTotal"].Integer(); + + result = std::make_shared(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, orientation, style); + } + else + { + auto items = config["items"].convertTo>(); + result = std::make_shared(position, length, callbacks_int.at(config["callback"].String()), items, value, orientation, style); + } + if(!config["scrollBounds"].isNull()) { @@ -549,14 +580,16 @@ std::shared_ptr InterfaceObjectConfigurable::buildComboBox(const JsonN { logGlobal->debug("Building widget ComboBox"); auto position = readPosition(config["position"]); + auto dropDownPosition = readPosition(config["dropDownPosition"]); auto image = AnimationPath::fromJson(config["image"]); auto help = readHintText(config["help"]); - auto result = std::make_shared(position, image, help, config["dropDown"]); + auto result = std::make_shared(position, image, help, config["dropDown"], dropDownPosition); + if(!config["items"].isNull()) { for(const auto & item : config["items"].Vector()) { - result->addOverlay(buildWidget(item)); + result->setOverlay(buildWidget(item)); } } if(!config["imageOrder"].isNull()) @@ -687,6 +720,31 @@ std::shared_ptr InterfaceObjectConfigurable::buildAnimation(const return anim; } +std::shared_ptr InterfaceObjectConfigurable::buildGraphicalPrimitive(const JsonNode & config) const +{ + logGlobal->debug("Building widget GraphicalPrimitiveCanvas"); + + auto rect = readRect(config["rect"]); + auto widget = std::make_shared(rect); + + for (auto const & entry : config["primitives"].Vector()) + { + auto color = readColor(entry["color"]); + auto typeString = entry["type"].String(); + auto pointA = readPosition(entry["a"]); + auto pointB = readPosition(entry["b"]); + + if (typeString == "line") + widget->addLine(pointA, pointB, color); + if (typeString == "filledBox") + widget->addBox(pointA, pointB, color); + if (typeString == "rectangle") + widget->addRectangle(pointA, pointB, color); + } + + return widget; +} + std::shared_ptr InterfaceObjectConfigurable::buildTransparentFilledRectangle(const JsonNode & config) const { logGlobal->debug("Building widget TransparentFilledRectangle"); diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index cc812299e..b1bbbde93 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -14,7 +14,7 @@ #include "TextAlignment.h" #include "../render/EFont.h" -#include "../../lib/JsonNode.h" +#include "../../lib/json/JsonNode.h" class CPicture; class CLabel; @@ -108,6 +108,7 @@ protected: std::shared_ptr buildComboBox(const JsonNode &); std::shared_ptr buildTextInput(const JsonNode &) const; std::shared_ptr buildTransparentFilledRectangle(const JsonNode & config) const; + std::shared_ptr buildGraphicalPrimitive(const JsonNode & config) const; std::shared_ptr buildTextBox(const JsonNode & config) const; //composite widgets diff --git a/client/gui/Shortcut.h b/client/gui/Shortcut.h index 39ad21008..2fc864ebd 100644 --- a/client/gui/Shortcut.h +++ b/client/gui/Shortcut.h @@ -125,6 +125,7 @@ enum class EShortcut BATTLE_SURRENDER, BATTLE_RETREAT, BATTLE_AUTOCOMBAT, + BATTLE_END_WITH_AUTOCOMBAT, BATTLE_CAST_SPELL, BATTLE_WAIT, BATTLE_DEFEND, diff --git a/client/gui/ShortcutHandler.cpp b/client/gui/ShortcutHandler.cpp index 13f4f7307..839a3fc99 100644 --- a/client/gui/ShortcutHandler.cpp +++ b/client/gui/ShortcutHandler.cpp @@ -124,6 +124,7 @@ std::vector ShortcutHandler::translateKeycode(SDL_Keycode key) const {SDLK_s, EShortcut::BATTLE_SURRENDER }, {SDLK_r, EShortcut::BATTLE_RETREAT }, {SDLK_a, EShortcut::BATTLE_AUTOCOMBAT }, + {SDLK_e, EShortcut::BATTLE_END_WITH_AUTOCOMBAT}, {SDLK_c, EShortcut::BATTLE_CAST_SPELL }, {SDLK_w, EShortcut::BATTLE_WAIT }, {SDLK_d, EShortcut::BATTLE_DEFEND }, @@ -265,6 +266,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const {"battleSurrender", EShortcut::BATTLE_SURRENDER }, {"battleRetreat", EShortcut::BATTLE_RETREAT }, {"battleAutocombat", EShortcut::BATTLE_AUTOCOMBAT }, + {"battleAutocombatEnd", EShortcut::BATTLE_END_WITH_AUTOCOMBAT}, {"battleCastSpell", EShortcut::BATTLE_CAST_SPELL }, {"battleWait", EShortcut::BATTLE_WAIT }, {"battleDefend", EShortcut::BATTLE_DEFEND }, diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 2566bf075..496d2d7e6 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -15,6 +15,7 @@ #include #include "CSelectionBase.h" +#include "ExtraOptionsTab.h" #include "../CGameInfo.h" #include "../CMusicHandler.h" @@ -72,6 +73,7 @@ CBonusSelection::CBonusSelection() buttonStart = std::make_shared(Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT); buttonRestart = std::make_shared(Point(475, 536), AnimationPath::builtin("CBRESTB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT); + buttonVideo = std::make_shared(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), [this](){ GH.windows().createAndPushWindow(getCampaign()->scenario(CSH->campaignMap).prolog, [this](){ redraw(); }); }); buttonBack = std::make_shared(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL); campaignName = std::make_shared(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName()); @@ -85,7 +87,7 @@ CBonusSelection::CBonusSelection() labelMapDescription = std::make_shared(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); mapDescription = std::make_shared("", Rect(480, 278, 292, 108), 1); - labelChooseBonus = std::make_shared(511, 432, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[71]); + labelChooseBonus = std::make_shared(475, 432, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[71]); groupBonuses = std::make_shared(std::bind(&IServerAPI::setCampaignBonus, CSH, _1)); flagbox = std::make_shared(Rect(486, 407, 335, 23)); @@ -93,17 +95,17 @@ CBonusSelection::CBonusSelection() std::vector difficulty; std::string difficultyString = CGI->generaltexth->allTexts[492]; boost::split(difficulty, difficultyString, boost::is_any_of(" ")); - labelDifficulty = std::make_shared(689, 432, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, difficulty.back()); + labelDifficulty = std::make_shared(724, settings["general"]["enableUiEnhancements"].Bool() ? 457 : 432, FONT_MEDIUM, ETextAlignment::TOPCENTER, Colors::WHITE, difficulty.back()); for(size_t b = 0; b < difficultyIcons.size(); ++b) { - difficultyIcons[b] = std::make_shared(AnimationPath::builtinTODO("GSPBUT" + std::to_string(b + 3) + ".DEF"), 0, 0, 709, 455); + difficultyIcons[b] = std::make_shared(AnimationPath::builtinTODO("GSPBUT" + std::to_string(b + 3) + ".DEF"), 0, 0, 709, settings["general"]["enableUiEnhancements"].Bool() ? 480 : 455); } if(getCampaign()->playerSelectedDifficulty()) { - buttonDifficultyLeft = std::make_shared(Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); - buttonDifficultyRight = std::make_shared(Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); + buttonDifficultyLeft = std::make_shared(settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); + buttonDifficultyRight = std::make_shared(settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); } for(auto scenarioID : getCampaign()->allScenarios()) @@ -116,6 +118,16 @@ CBonusSelection::CBonusSelection() if (!getCampaign()->getMusic().empty()) CCS->musich->playMusic( getCampaign()->getMusic(), true, false); + + if(settings["general"]["enableUiEnhancements"].Bool()) + { + tabExtraOptions = std::make_shared(); + tabExtraOptions->recActions = UPDATE | SHOWALL | LCLICK | RCLICK_POPUP; + tabExtraOptions->recreate(true); + tabExtraOptions->setEnabled(false); + buttonExtraOptions = std::make_shared(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::NONE); + buttonExtraOptions->setTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, Colors::WHITE); + } } void CBonusSelection::createBonusesIcons() @@ -125,7 +137,7 @@ void CBonusSelection::createBonusesIcons() const std::vector & bonDescs = scenario.travelOptions.bonusesToChoose; groupBonuses = std::make_shared(std::bind(&IServerAPI::setCampaignBonus, CSH, _1)); - static const char * bonusPics[] = + constexpr std::array bonusPics = { "SPELLBON.DEF", // Spell "TWCRPORT.DEF", // Monster @@ -151,17 +163,17 @@ void CBonusSelection::createBonusesIcons() { case CampaignBonusType::SPELL: desc.appendLocalString(EMetaText::GENERAL_TXT, 715); - desc.replaceLocalString(EMetaText::SPELL_NAME, bonDescs[i].info2); + desc.replaceName(SpellID(bonDescs[i].info2)); break; case CampaignBonusType::MONSTER: picNumber = bonDescs[i].info2 + 2; desc.appendLocalString(EMetaText::GENERAL_TXT, 717); desc.replaceNumber(bonDescs[i].info3); - desc.replaceLocalString(EMetaText::CRE_PL_NAMES, bonDescs[i].info2); + desc.replaceNamePlural(bonDescs[i].info2); break; case CampaignBonusType::BUILDING: { - int faction = -1; + FactionID faction; for(auto & elem : CSH->si->playerInfos) { if(elem.second.isControlledByHuman()) @@ -187,11 +199,11 @@ void CBonusSelection::createBonusesIcons() } case CampaignBonusType::ARTIFACT: desc.appendLocalString(EMetaText::GENERAL_TXT, 715); - desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2); + desc.replaceName(ArtifactID(bonDescs[i].info2)); break; case CampaignBonusType::SPELL_SCROLL: desc.appendLocalString(EMetaText::GENERAL_TXT, 716); - desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2); + desc.replaceName(ArtifactID(bonDescs[i].info2)); break; case CampaignBonusType::PRIMARY_SKILL: { @@ -229,44 +241,36 @@ void CBonusSelection::createBonusesIcons() case CampaignBonusType::SECONDARY_SKILL: desc.appendLocalString(EMetaText::GENERAL_TXT, 718); desc.replaceTextID(TextIdentifier("core", "genrltxt", "levels", bonDescs[i].info3 - 1).get()); - desc.replaceLocalString(EMetaText::SEC_SKILL_NAME, bonDescs[i].info2); + desc.replaceName(SecondarySkill(bonDescs[i].info2)); picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1; break; case CampaignBonusType::RESOURCE: { - int serialResID = 0; + desc.appendLocalString(EMetaText::GENERAL_TXT, 717); + switch(bonDescs[i].info1) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - serialResID = bonDescs[i].info1; - break; - case 0xFD: //wood + ore - serialResID = 7; - break; - case 0xFE: //rare resources - serialResID = 8; - break; + case 0xFD: //wood + ore + { + desc.replaceLocalString(EMetaText::GENERAL_TXT, 721); + picNumber = 7; + break; + } + case 0xFE: //wood + ore + { + desc.replaceLocalString(EMetaText::GENERAL_TXT, 722); + picNumber = 8; + break; + } + default: + { + desc.replaceName(GameResID(bonDescs[i].info1)); + picNumber = bonDescs[i].info1; + } } - picNumber = serialResID; - desc.appendLocalString(EMetaText::GENERAL_TXT, 717); desc.replaceNumber(bonDescs[i].info2); - - if(serialResID <= 6) - { - desc.replaceLocalString(EMetaText::RES_NAMES, serialResID); - } - else - { - desc.replaceLocalString(EMetaText::GENERAL_TXT, 714 + serialResID); - } break; } case CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO: @@ -281,30 +285,27 @@ void CBonusSelection::createBonusesIcons() } case CampaignBonusType::HERO: - - desc.appendLocalString(EMetaText::GENERAL_TXT, 718); - desc.replaceTextID(TextIdentifier("core", "genrltxt", "capColors", bonDescs[i].info1).get()); if(bonDescs[i].info2 == 0xFFFF) { - desc.replaceLocalString(EMetaText::GENERAL_TXT, 101); + desc.appendLocalString(EMetaText::GENERAL_TXT, 720); // Start with random hero picNumber = -1; picName = "CBONN1A3.BMP"; } else { + desc.appendLocalString(EMetaText::GENERAL_TXT, 715); // Start with %s desc.replaceTextID(CGI->heroh->objects[bonDescs[i].info2]->getNameTextID()); } break; } - std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath(), CButton::tooltip(desc.toString(), desc.toString())); + std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath::builtin("campaignBonusSelection"), CButton::tooltip(desc.toString(), desc.toString())); if(picNumber != -1) - picName += ":" + std::to_string(picNumber); + bonusButton->setOverlay(std::make_shared(AnimationPath::builtin(picName), picNumber)); + else + bonusButton->setOverlay(std::make_shared(ImagePath::builtin(picName))); - auto anim = GH.renderHandler().createAnimation(); - anim->setCustom(picName, 0); - bonusButton->setImage(anim); if(CSH->campaignBonus == i) bonusButton->setBorderColor(Colors::BRIGHT_YELLOW); groupBonuses->addToggle(i, bonusButton); @@ -316,19 +317,18 @@ void CBonusSelection::createBonusesIcons() void CBonusSelection::updateAfterStateChange() { - if(CSH->state != EClientState::GAMEPLAY) + if(CSH->getState() != EClientState::GAMEPLAY) { buttonRestart->disable(); + buttonVideo->disable(); buttonStart->enable(); - if(!getCampaign()->conqueredScenarios().empty()) - buttonBack->block(true); - else - buttonBack->block(false); + buttonBack->block(!getCampaign()->conqueredScenarios().empty()); } else { buttonStart->disable(); buttonRestart->enable(); + buttonVideo->enable(); buttonBack->block(false); if(buttonDifficultyLeft) buttonDifficultyLeft->disable(); @@ -365,7 +365,7 @@ void CBonusSelection::updateAfterStateChange() void CBonusSelection::goBack() { - if(CSH->state != EClientState::GAMEPLAY) + if(CSH->getState() != EClientState::GAMEPLAY) { GH.windows().popWindows(2); } @@ -411,6 +411,7 @@ void CBonusSelection::startMap() //block buttons immediately buttonStart->block(true); buttonRestart->block(true); + buttonVideo->block(true); buttonBack->block(true); if(LOCPLINT) // we're currently ingame, so ask for starting new map and end game diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 78289bb02..989e459b3 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -27,6 +27,7 @@ class CAnimImage; class CLabel; class CFlagBox; class ISelectionScreenInfo; +class ExtraOptionsTab; /// Campaign screen where you can choose one out of three starting bonuses class CBonusSelection : public CWindowObject @@ -65,6 +66,7 @@ public: std::shared_ptr buttonStart; std::shared_ptr buttonRestart; std::shared_ptr buttonBack; + std::shared_ptr buttonVideo; std::shared_ptr campaignName; std::shared_ptr labelCampaignDescription; std::shared_ptr campaignDescription; @@ -81,4 +83,7 @@ public: std::shared_ptr buttonDifficultyLeft; std::shared_ptr buttonDifficultyRight; std::shared_ptr iconsMapSizes; + + std::shared_ptr tabExtraOptions; + std::shared_ptr buttonExtraOptions; }; diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index fc0333f37..4cd6c94f6 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -8,14 +8,16 @@ * */ #include "StdInc.h" - #include "CLobbyScreen.h" -#include "CBonusSelection.h" -#include "SelectionTab.h" -#include "RandomMapTab.h" -#include "OptionsTab.h" -#include "../CServerHandler.h" +#include "CBonusSelection.h" +#include "TurnOptionsTab.h" +#include "ExtraOptionsTab.h" +#include "OptionsTab.h" +#include "RandomMapTab.h" +#include "SelectionTab.h" + +#include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../widgets/Buttons.h" @@ -24,12 +26,13 @@ #include "../../CCallback.h" -#include "../CGameInfo.h" -#include "../../lib/networkPacks/PacksForLobby.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/rmg/CMapGenOptions.h" +#include "../CGameInfo.h" CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) : CSelectionBase(screenType), bonusSel(nullptr) @@ -50,16 +53,23 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) }); buttonOptions = std::make_shared(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS); + if(settings["general"]["enableUiEnhancements"].Bool()) + { + buttonTurnOptions = std::make_shared(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE); + buttonExtraOptions = std::make_shared(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::NONE); + } }; buttonChat = std::make_shared(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); + buttonChat->setTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); switch(screenType) { case ESelectionScreen::newGame: { tabOpt = std::make_shared(); + tabTurnOptions = std::make_shared(); + tabExtraOptions = std::make_shared(); tabRand = std::make_shared(); tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2); buttonRMG = std::make_shared(Point(411, 105), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP); @@ -71,14 +81,16 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, CSH, _1)); - buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_BEGIN_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_BEGIN_GAME); initLobby(); break; } case ESelectionScreen::loadGame: { tabOpt = std::make_shared(); - buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME); + tabTurnOptions = std::make_shared(); + tabExtraOptions = std::make_shared(); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_LOAD_GAME); initLobby(); break; } @@ -100,7 +112,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) CLobbyScreen::~CLobbyScreen() { // TODO: For now we always destroy whole lobby when leaving bonus selection screen - if(CSH->state == EClientState::LOBBY_CAMPAIGN) + if(CSH->getState() == EClientState::LOBBY_CAMPAIGN) CSH->sendClientDisconnecting(); } @@ -114,16 +126,32 @@ void CLobbyScreen::toggleTab(std::shared_ptr tab) CSH->sendGuiAction(LobbyGuiAction::OPEN_SCENARIO_LIST); else if(tab == tabRand) CSH->sendGuiAction(LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS); + else if(tab == tabTurnOptions) + CSH->sendGuiAction(LobbyGuiAction::OPEN_TURN_OPTIONS); + else if(tab == tabExtraOptions) + CSH->sendGuiAction(LobbyGuiAction::OPEN_EXTRA_OPTIONS); CSelectionBase::toggleTab(tab); } void CLobbyScreen::startCampaign() { - if(CSH->mi) - { + if(!CSH->mi) + return; + + try { auto ourCampaign = CampaignHandler::getCampaign(CSH->mi->fileURI); CSH->setCampaignState(ourCampaign); } + catch (const std::runtime_error &e) + { + // handle possible exception on map loading. For example campaign that contains map in unsupported format + // for example, wog campaigns or hota campaigns without hota map support mod + MetaString message; + message.appendTextID("vcmi.client.errors.invalidMap"); + message.replaceRawString(e.what()); + + CInfoWindow::showInfoDialog(message.toString(), {}); + } } void CLobbyScreen::startScenario(bool allowOnlyAI) @@ -143,33 +171,57 @@ void CLobbyScreen::toggleMode(bool host) return; auto buttonColor = host ? Colors::WHITE : Colors::ORANGE; - buttonSelect->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); - buttonOptions->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor); + buttonSelect->setTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); + buttonOptions->setTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor); + + if (buttonTurnOptions) + buttonTurnOptions->setTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.turnOptions.hover"), FONT_SMALL, buttonColor); + + if (buttonExtraOptions) + buttonExtraOptions->setTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, buttonColor); + if(buttonRMG) { - buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); + buttonRMG->setTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); buttonRMG->block(!host); } buttonSelect->block(!host); buttonOptions->block(!host); + if (buttonTurnOptions) + buttonTurnOptions->block(!host); + + if (buttonExtraOptions) + buttonExtraOptions->block(!host); + if(CSH->mi) + { tabOpt->recreate(); + tabTurnOptions->recreate(); + tabExtraOptions->recreate(); + } } void CLobbyScreen::toggleChat() { card->toggleChat(); if(card->showChat) - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); + buttonChat->setTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); else - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); + buttonChat->setTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); } void CLobbyScreen::updateAfterStateChange() { - if(CSH->mi && tabOpt) - tabOpt->recreate(); + if(CSH->mi) + { + if (tabOpt) + tabOpt->recreate(); + if (tabTurnOptions) + tabTurnOptions->recreate(); + if (tabExtraOptions) + tabExtraOptions->recreate(); + } buttonStart->block(CSH->mi == nullptr || CSH->isGuest()); diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 4d7f8987e..df251d4f9 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -22,7 +22,6 @@ #include "../CPlayerInterface.h" #include "../CMusicHandler.h" #include "../CVideoHandler.h" -#include "../CPlayerInterface.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" @@ -30,7 +29,7 @@ #include "../mainmenu/CMainMenu.h" #include "../widgets/Buttons.h" #include "../widgets/CComponent.h" -#include "../widgets/MiscWidgets.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/ObjectLists.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" @@ -43,11 +42,12 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" +#include "../../lib/CRandomGenerator.h" #include "../../lib/CThreadHelper.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" -#include "../../lib/serializer/Connection.h" +#include "../../lib/CRandomGenerator.h" ISelectionScreenInfo::ISelectionScreenInfo(ESelectionScreen ScreenType) : screenType(ScreenType) @@ -69,7 +69,7 @@ int ISelectionScreenInfo::getCurrentDifficulty() PlayerInfo ISelectionScreenInfo::getPlayerInfo(PlayerColor color) { - return getMapInfo()->mapHeader->players[color.getNum()]; + return getMapInfo()->mapHeader->players.at(color.getNum()); } CSelectionBase::CSelectionBase(ESelectionScreen type) @@ -135,7 +135,7 @@ InfoCard::InfoCard() Rect descriptionRect(26, 149, 320, 115); mapDescription = std::make_shared("", descriptionRect, 1); playerListBg = std::make_shared(ImagePath::builtin("CHATPLUG.bmp"), 16, 276); - chat = std::make_shared(Rect(26, 132, 340, 132)); + chat = std::make_shared(Rect(18, 126, 335, 143)); if(SEL->screenType == ESelectionScreen::campaignList) { @@ -154,7 +154,7 @@ InfoCard::InfoCard() iconDifficulty = std::make_shared(0); { - static const char * difButns[] = {"GSPBUT3.DEF", "GSPBUT4.DEF", "GSPBUT5.DEF", "GSPBUT6.DEF", "GSPBUT7.DEF"}; + constexpr std::array difButns = {"GSPBUT3.DEF", "GSPBUT4.DEF", "GSPBUT5.DEF", "GSPBUT6.DEF", "GSPBUT7.DEF"}; for(int i = 0; i < 5; i++) { @@ -209,7 +209,6 @@ void InfoCard::changeSelection() return; labelSaveDate->setText(mapInfo->date); - labelMapSize->setText(std::to_string(mapInfo->mapHeader->width) + "x" + std::to_string(mapInfo->mapHeader->height)); mapName->setText(mapInfo->getNameTranslated()); mapDescription->setText(mapInfo->getDescriptionTranslated()); @@ -220,14 +219,17 @@ void InfoCard::changeSelection() if(SEL->screenType == ESelectionScreen::campaignList) return; - iconsMapSizes->setFrame(mapInfo->getMapSizeIconId()); const CMapHeader * header = mapInfo->mapHeader.get(); + + labelMapSize->setText(std::to_string(header->width) + "x" + std::to_string(header->height)); + iconsMapSizes->setFrame(mapInfo->getMapSizeIconId()); + iconsVictoryCondition->setFrame(header->victoryIconIndex); labelVictoryConditionText->setText(header->victoryMessage.toString()); iconsLossCondition->setFrame(header->defeatIconIndex); labelLossConditionText->setText(header->defeatMessage.toString()); flagbox->recreate(); - labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + mapInfo->mapHeader->difficulty]); + labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + vstd::to_underlying(mapInfo->mapHeader->difficulty)]); iconDifficulty->setSelected(SEL->getCurrentDifficulty()); if(SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::saveGame) for(auto & button : iconDifficulty->buttons) @@ -330,9 +332,12 @@ CChatBox::CChatBox(const Rect & rect) setRedrawParent(true); const int height = static_cast(graphics->fonts[FONT_SMALL]->getLineHeight()); - inputBox = std::make_shared(Rect(0, rect.h - height, rect.w, height), EFonts::FONT_SMALL, 0); + Rect textInputArea(1, rect.h - height, rect.w - 1, height); + Rect chatHistoryArea(3, 1, rect.w - 3, rect.h - height - 1); + inputBackground = std::make_shared(textInputArea, ColorRGBA(0,0,0,192)); + inputBox = std::make_shared(textInputArea, EFonts::FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true); inputBox->removeUsedEvents(KEYBOARD); - chatHistory = std::make_shared("", Rect(0, 0, rect.w, rect.h - height), 1); + chatHistory = std::make_shared("", chatHistoryArea, 1); chatHistory->label->color = Colors::GREEN; } diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 92b0c2b4f..981f69df8 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -26,12 +26,15 @@ class CAnimImage; class CToggleGroup; class RandomMapTab; class OptionsTab; +class TurnOptionsTab; +class ExtraOptionsTab; class SelectionTab; class InfoCard; class CChatBox; class CLabel; class CFlagBox; class CLabelGroup; +class TransparentFilledRectangle; class ISelectionScreenInfo { @@ -57,12 +60,16 @@ public: std::shared_ptr buttonSelect; std::shared_ptr buttonRMG; std::shared_ptr buttonOptions; + std::shared_ptr buttonTurnOptions; + std::shared_ptr buttonExtraOptions; std::shared_ptr buttonStart; std::shared_ptr buttonBack; std::shared_ptr buttonSimturns; std::shared_ptr tabSel; std::shared_ptr tabOpt; + std::shared_ptr tabTurnOptions; + std::shared_ptr tabExtraOptions; std::shared_ptr tabRand; std::shared_ptr curTab; @@ -119,6 +126,7 @@ class CChatBox : public CIntObject public: std::shared_ptr chatHistory; std::shared_ptr inputBox; + std::shared_ptr inputBackground; CChatBox(const Rect & rect); diff --git a/AI/BattleAI/common.cpp b/client/lobby/ExtraOptionsTab.cpp similarity index 50% rename from AI/BattleAI/common.cpp rename to client/lobby/ExtraOptionsTab.cpp index 4a467c8cc..0b3b44526 100644 --- a/AI/BattleAI/common.cpp +++ b/client/lobby/ExtraOptionsTab.cpp @@ -1,5 +1,5 @@ /* - * common.cpp, part of VCMI engine + * ExtraOptionsTab.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -7,17 +7,12 @@ * Full text of license available in license.txt file, in main folder * */ + #include "StdInc.h" -#include "common.h" +#include "ExtraOptionsTab.h" -std::shared_ptr cbc; - -void setCbc(std::shared_ptr cb) +ExtraOptionsTab::ExtraOptionsTab() + : OptionsTabBase(JsonPath::builtin("config/widgets/extraOptionsTab.json")) { - cbc = cb; -} -std::shared_ptr getCbc() -{ - return cbc; } diff --git a/client/lobby/ExtraOptionsTab.h b/client/lobby/ExtraOptionsTab.h new file mode 100644 index 000000000..81114724b --- /dev/null +++ b/client/lobby/ExtraOptionsTab.h @@ -0,0 +1,18 @@ +/* + * ExtraOptionsTab.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "OptionsTabBase.h" + +class ExtraOptionsTab : public OptionsTabBase +{ +public: + ExtraOptionsTab(); +}; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 72e3817f3..0d4f1a0d2 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -42,164 +42,18 @@ #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" -OptionsTab::OptionsTab() : humanPlayers(0) +static JsonPath optionsTabConfigLocation() { - recActions = 0; - - addCallback("setTimerPreset", [&](int index){ - if(!variables["timerPresets"].isNull()) - { - auto tpreset = variables["timerPresets"].Vector().at(index).Vector(); - TurnTimerInfo tinfo; - tinfo.baseTimer = tpreset.at(0).Integer() * 1000; - tinfo.turnTimer = tpreset.at(1).Integer() * 1000; - tinfo.battleTimer = tpreset.at(2).Integer() * 1000; - tinfo.creatureTimer = tpreset.at(3).Integer() * 1000; - CSH->setTurnTimerInfo(tinfo); - } - }); + if(settings["general"]["enableUiEnhancements"].Bool()) + return JsonPath::builtin("config/widgets/playerOptionsTab.json"); + else + return JsonPath::builtin("config/widgets/advancedOptionsTab.json"); +} - addCallback("setSimturnDuration", [&](int index){ - SimturnsInfo info; - info.optionalTurns = index; - CSH->setSimturnsInfo(info); - }); - - //helper function to parse string containing time to integer reflecting time in seconds - //assumed that input string can be modified by user, function shall support user's intention - // normal: 2:00, 12:30 - // adding symbol: 2:005 -> 2:05, 2:305 -> 23:05, - // adding symbol (>60 seconds): 12:095 -> 129:05 - // removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02 - auto parseTimerString = [](const std::string & str) -> int - { - auto sc = str.find(":"); - if(sc == std::string::npos) - return str.empty() ? 0 : std::stoi(str); - - auto l = str.substr(0, sc); - auto r = str.substr(sc + 1, std::string::npos); - if(r.length() == 3) //symbol added - { - l.push_back(r.front()); - r.erase(r.begin()); - } - else if(r.length() == 1) //symbol removed - { - r.insert(r.begin(), l.back()); - l.pop_back(); - } - else if(r.empty()) - r = "0"; - - int sec = std::stoi(r); - if(sec >= 60) - { - if(l.empty()) //9:00 -> 0:09 - return sec / 10; - - l.push_back(r.front()); //0:090 -> 9:00 - r.erase(r.begin()); - } - else if(l.empty()) - return sec; - - return std::stoi(l) * 60 + std::stoi(r); - }; - - addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.baseTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.turnTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.battleTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - addCallback("parseAndSetTimer_creature", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.creatureTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - - const JsonNode config(JsonPath::builtin("config/widgets/optionsTab.json")); - build(config); - - //set timers combo box callbacks - if(auto w = widget("timerModeSwitch")) - { - w->onConstructItems = [&](std::vector & curItems){ - if(variables["timers"].isNull()) - return; - - for(auto & p : variables["timers"].Vector()) - { - curItems.push_back(&p); - } - }; - - w->onSetItem = [&](const void * item){ - if(item) - { - if(auto * tObj = reinterpret_cast(item)) - { - for(auto wname : (*tObj)["hideWidgets"].Vector()) - { - if(auto w = widget(wname.String())) - w->setEnabled(false); - } - for(auto wname : (*tObj)["showWidgets"].Vector()) - { - if(auto w = widget(wname.String())) - w->setEnabled(true); - } - if((*tObj)["default"].isVector()) - { - TurnTimerInfo tinfo; - tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000; - tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000; - tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000; - tinfo.creatureTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; - CSH->setTurnTimerInfo(tinfo); - } - } - redraw(); - } - }; - - w->getItemText = [this](int idx, const void * item){ - if(item) - { - if(auto * tObj = reinterpret_cast(item)) - return readText((*tObj)["text"]); - } - return std::string(""); - }; - - w->setItem(0); - } +OptionsTab::OptionsTab() + : OptionsTabBase(optionsTabConfigLocation()) + , humanPlayers(0) +{ } void OptionsTab::recreate() @@ -221,64 +75,7 @@ void OptionsTab::recreate() entries.insert(std::make_pair(pInfo.first, std::make_shared(pInfo.second, * this))); } - //Simultaneous turns - if(auto turnSlider = widget("labelSimturnsDurationValue")) - turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns); - - if(auto w = widget("labelSimturnsDurationValue")) - { - MetaString message; - message.appendRawString("Simturns: up to %d days"); - message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns); - w->setText(message.toString()); - } - - const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; - - //classic timer - if(auto turnSlider = widget("sliderTurnDuration")) - { - if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.creatureTimer && !turnTimerRemote.baseTimer) - { - for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) - { - auto & tpreset = variables["timerPresets"].Vector()[idx]; - if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) - { - turnSlider->scrollTo(idx); - if(auto w = widget("labelTurnDurationValue")) - w->setText(CGI->generaltexth->turnDurations[idx]); - } - } - } - } - - //chess timer - auto timeToString = [](int time) -> std::string - { - std::stringstream ss; - ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60; - return ss.str(); - }; - - if(auto ww = widget("chessFieldBase")) - ww->setText(timeToString(turnTimerRemote.baseTimer), false); - if(auto ww = widget("chessFieldTurn")) - ww->setText(timeToString(turnTimerRemote.turnTimer), false); - if(auto ww = widget("chessFieldBattle")) - ww->setText(timeToString(turnTimerRemote.battleTimer), false); - if(auto ww = widget("chessFieldCreature")) - ww->setText(timeToString(turnTimerRemote.creatureTimer), false); - - if(auto w = widget("timerModeSwitch")) - { - if(turnTimerRemote.battleTimer || turnTimerRemote.creatureTimer || turnTimerRemote.baseTimer) - { - if(auto turnSlider = widget("sliderTurnDuration")) - if(turnSlider->isActive()) - w->setItem(1); - } - } + OptionsTabBase::recreate(); } size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) @@ -401,7 +198,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() return CGI->generaltexth->allTexts[522]; if(!playerSettings.heroNameTextId.empty()) - return playerSettings.heroNameTextId; + return CGI->generaltexth->translate(playerSettings.heroNameTextId); auto index = playerSettings.getHeroValidated(); return (*CGI->heroh)[index]->getNameTranslated(); } @@ -561,27 +358,35 @@ void OptionsTab::CPlayerOptionTooltipBox::genHeader() void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() { + auto factionIndex = playerSettings.getCastleValidated(); + + if (playerSettings.castle == FactionID::RANDOM) + return genBonusWindow(); + pos = Rect(0, 0, 228, 290); genHeader(); labelAssociatedCreatures = std::make_shared(pos.w / 2 + 8, 122, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); - auto factionIndex = playerSettings.getCastleValidated(); std::vector> components; const CTown * town = (*CGI->townh)[factionIndex]->town; for(auto & elem : town->creatures) { if(!elem.empty()) - components.push_back(std::make_shared(CComponent::creature, elem.front(), 0, CComponent::tiny)); + components.push_back(std::make_shared(ComponentType::CREATURE, elem.front(), std::nullopt, CComponent::tiny)); } - boxAssociatedCreatures = std::make_shared(components, Rect(10, 140, pos.w - 20, 140)); + boxAssociatedCreatures = std::make_shared(components, Rect(10, 140, pos.w - 20, 140), 20, 10, 22, 4); } void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() { + auto heroIndex = playerSettings.getHeroValidated(); + + if (playerSettings.hero == HeroTypeID::RANDOM) + return genBonusWindow(); + pos = Rect(0, 0, 292, 226); genHeader(); labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - auto heroIndex = playerSettings.getHeroValidated(); imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); @@ -595,12 +400,11 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) - : CWindowObject(BORDERED) +OptionsTab::SelectionWindow::SelectionWindow(const PlayerColor & color, SelType _type) + : CWindowObject(BORDERED), color(color) { addUsedEvents(LCLICK | SHOW_POPUP); - color = _color; type = _type; initialFaction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; @@ -610,10 +414,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) selectedHero = initialHero; selectedBonus = initialBonus; allowedFactions = SEL->getPlayerInfo(color).allowedFactions; - std::vector allowedHeroesFlag = SEL->getMapInfo()->mapHeader->allowedHeroes; - for(int i = 0; i < allowedHeroesFlag.size(); i++) - if(allowedHeroesFlag[i]) - allowedHeroes.insert(HeroTypeID(i)); + allowedHeroes = SEL->getMapInfo()->mapHeader->allowedHeroes; for(auto & player : SEL->getStartInfo()->playerInfos) { @@ -679,9 +480,10 @@ void OptionsTab::SelectionWindow::setSelection() void OptionsTab::SelectionWindow::reopen() { - std::shared_ptr window = std::shared_ptr(new SelectionWindow(color, type)); + auto window = std::shared_ptr(new SelectionWindow(color, type)); close(); - GH.windows().pushWindow(window); + if(CSH->isMyColor(color) || CSH->isHost()) + GH.windows().pushWindow(window); } void OptionsTab::SelectionWindow::recreate() @@ -701,7 +503,7 @@ void OptionsTab::SelectionWindow::recreate() int count = 0; for(auto & elem : allowedHeroes) { - CHero * type = VLC->heroh->objects[elem]; + const CHero * type = elem.toHeroType(); if(type->heroClass->faction == selectedFaction) { count++; @@ -735,11 +537,11 @@ void OptionsTab::SelectionWindow::recreate() void OptionsTab::SelectionWindow::drawOutlinedText(int x, int y, ColorRGBA color, std::string text) { - components.push_back(std::make_shared(x-1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); - components.push_back(std::make_shared(x+1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); - components.push_back(std::make_shared(x, y-1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); - components.push_back(std::make_shared(x, y+1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); - components.push_back(std::make_shared(x, y, FONT_TINY, ETextAlignment::CENTER, color, text)); + components.push_back(std::make_shared(x-1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x+1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x, y-1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x, y+1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x, y, FONT_TINY, ETextAlignment::CENTER, color, text, 56)); } void OptionsTab::SelectionWindow::genContentGrid(int lines) @@ -830,7 +632,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() void OptionsTab::SelectionWindow::genContentBonus() { - PlayerSettings set = PlayerSettings(); + PlayerSettings set = SEL->getStartInfo()->playerInfos.find(color)->second; int i = 0; for(auto elem : allowedBonus) @@ -972,7 +774,7 @@ OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSett OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; image = std::make_shared(getImageName(), getImageIndex()); - subtitle = std::make_shared(23, 39, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, getName()); + subtitle = std::make_shared(24, 39, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, getName(), 71); pos = image->pos; @@ -1087,10 +889,10 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con background = std::make_shared(ImagePath::builtin(bgs[s->color]), 0, 0); if(s->isControlledByAI() || CSH->isGuest()) - labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name); + labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95); else { - labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, false); + labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, ETextAlignment::CENTER, false); labelPlayerNameEdit->setText(name); } labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); @@ -1115,7 +917,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con CGI->generaltexth->zelp[180], std::bind(&OptionsTab::onSetPlayerClicked, &parentTab, *s) ); - flag->hoverable = true; + flag->setHoverable(true); flag->block(CSH->isGuest()); } else @@ -1165,7 +967,7 @@ void OptionsTab::PlayerOptionsEntry::updateName() { void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const { - if(ps.isControlledByAI() || humanPlayers > 0) + if(ps.isControlledByAI() || humanPlayers > 1) CSH->setPlayer(ps.color); } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 23eff9c96..d25737fe8 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -9,9 +9,9 @@ */ #pragma once +#include "OptionsTabBase.h" #include "../windows/CWindowObject.h" #include "../widgets/Scrollable.h" -#include "../gui/InterfaceObjectConfigurable.h" VCMI_LIB_NAMESPACE_BEGIN struct PlayerSettings; @@ -30,7 +30,7 @@ class CButton; class FilledTexturePlayerColored; /// The options tab which is shown at the map selection phase. -class OptionsTab : public InterfaceObjectConfigurable +class OptionsTab : public OptionsTabBase { struct PlayerOptionsEntry; @@ -148,7 +148,7 @@ private: public: void reopen(); - SelectionWindow(PlayerColor _color, SelType _type); + SelectionWindow(const PlayerColor & color, SelType _type); }; /// Image with current town/hero/bonus diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp new file mode 100644 index 000000000..bb150efe7 --- /dev/null +++ b/client/lobby/OptionsTabBase.cpp @@ -0,0 +1,427 @@ +/* + * OptionsTabBase.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "OptionsTabBase.h" +#include "CSelectionBase.h" + +#include "../widgets/ComboBox.h" +#include "../widgets/Images.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../CServerHandler.h" +#include "../CGameInfo.h" + +#include "../../lib/StartInfo.h" +#include "../../lib/Languages.h" +#include "../../lib/MetaString.h" +#include "../../lib/CGeneralTextHandler.h" + +static std::string timeToString(int time) +{ + std::stringstream ss; + ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60; + return ss.str(); +}; + + +std::vector OptionsTabBase::getTimerPresets() const +{ + std::vector result; + + for (auto const & tpreset : variables["timerPresets"].Vector()) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = tpreset[0].Integer() * 1000; + tinfo.turnTimer = tpreset[1].Integer() * 1000; + tinfo.battleTimer = tpreset[2].Integer() * 1000; + tinfo.unitTimer = tpreset[3].Integer() * 1000; + tinfo.accumulatingTurnTimer = tpreset[4].Bool(); + tinfo.accumulatingUnitTimer = tpreset[5].Bool(); + result.push_back(tinfo); + } + return result; +} + +std::vector OptionsTabBase::getSimturnsPresets() const +{ + std::vector result; + + for (auto const & tpreset : variables["simturnsPresets"].Vector()) + { + SimturnsInfo tinfo; + tinfo.optionalTurns = tpreset[0].Integer(); + tinfo.requiredTurns = tpreset[1].Integer(); + tinfo.allowHumanWithAI = tpreset[2].Bool(); + result.push_back(tinfo); + } + return result; +} + +OptionsTabBase::OptionsTabBase(const JsonPath & configPath) +{ + recActions = 0; + + auto setTimerPresetCallback = [this](int index){ + CSH->setTurnTimerInfo(getTimerPresets().at(index)); + }; + + auto setSimturnsPresetCallback = [this](int index){ + CSH->setSimturnsInfo(getSimturnsPresets().at(index)); + }; + + addCallback("setTimerPreset", setTimerPresetCallback); + addCallback("setSimturnPreset", setSimturnsPresetCallback); + + addCallback("setSimturnDurationMin", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.requiredTurns = index; + info.optionalTurns = std::max(info.optionalTurns, index); + CSH->setSimturnsInfo(info); + }); + + addCallback("setSimturnDurationMax", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.optionalTurns = index; + info.requiredTurns = std::min(info.requiredTurns, index); + CSH->setSimturnsInfo(info); + }); + + addCallback("setSimturnAI", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.allowHumanWithAI = index; + CSH->setSimturnsInfo(info); + }); + + addCallback("setCheatAllowed", [&](int index){ + ExtraOptionsInfo info = SEL->getStartInfo()->extraOptionsInfo; + info.cheatsAllowed = index; + CSH->setExtraOptionsInfo(info); + }); + + addCallback("setUnlimitedReplay", [&](int index){ + ExtraOptionsInfo info = SEL->getStartInfo()->extraOptionsInfo; + info.unlimitedReplay = index; + CSH->setExtraOptionsInfo(info); + }); + + addCallback("setTurnTimerAccumulate", [&](int index){ + TurnTimerInfo info = SEL->getStartInfo()->turnTimerInfo; + info.accumulatingTurnTimer = index; + CSH->setTurnTimerInfo(info); + }); + + addCallback("setUnitTimerAccumulate", [&](int index){ + TurnTimerInfo info = SEL->getStartInfo()->turnTimerInfo; + info.accumulatingUnitTimer = index; + CSH->setTurnTimerInfo(info); + }); + + //helper function to parse string containing time to integer reflecting time in seconds + //assumed that input string can be modified by user, function shall support user's intention + // normal: 2:00, 12:30 + // adding symbol: 2:005 -> 2:05, 2:305 -> 23:05, + // adding symbol (>60 seconds): 12:095 -> 129:05 + // removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02 + auto parseTimerString = [](const std::string & str) -> int + { + auto sc = str.find(":"); + if(sc == std::string::npos) + return str.empty() ? 0 : std::stoi(str); + + auto l = str.substr(0, sc); + auto r = str.substr(sc + 1, std::string::npos); + if(r.length() == 3) //symbol added + { + l.push_back(r.front()); + r.erase(r.begin()); + } + else if(r.length() == 1) //symbol removed + { + r.insert(r.begin(), l.back()); + l.pop_back(); + } + else if(r.empty()) + r = "0"; + + int sec = std::stoi(r); + if(sec >= 60) + { + if(l.empty()) //9:00 -> 0:09 + return sec / 10; + + l.push_back(r.front()); //0:090 -> 9:00 + r.erase(r.begin()); + } + else if(l.empty()) + return sec; + + return std::min(24*60, std::stoi(l)) * 60 + std::stoi(r); + }; + + addCallback("parseAndSetTimer_base", [this, parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.baseTimer = time; + CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldBase")) + ww->setText(timeToString(time), false); + } + }); + addCallback("parseAndSetTimer_turn", [this, parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.turnTimer = time; + CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldTurn")) + ww->setText(timeToString(time), false); + } + }); + addCallback("parseAndSetTimer_battle", [this, parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.battleTimer = time; + CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldBattle")) + ww->setText(timeToString(time), false); + } + }); + addCallback("parseAndSetTimer_unit", [this, parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.unitTimer = time; + CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldUnit")) + ww->setText(timeToString(time), false); + } + }); + + const JsonNode config(configPath); + build(config); + + //set timers combo box callbacks + if(auto w = widget("timerModeSwitch")) + { + w->onConstructItems = [&](std::vector & curItems){ + if(variables["timers"].isNull()) + return; + + for(auto & p : variables["timers"].Vector()) + { + curItems.push_back(&p); + } + }; + + w->onSetItem = [&](const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + { + for(auto wname : (*tObj)["hideWidgets"].Vector()) + { + if(auto w = widget(wname.String())) + w->setEnabled(false); + } + for(auto wname : (*tObj)["showWidgets"].Vector()) + { + if(auto w = widget(wname.String())) + w->setEnabled(true); + } + if((*tObj)["default"].isVector()) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000; + tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000; + tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000; + tinfo.unitTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; + CSH->setTurnTimerInfo(tinfo); + } + } + redraw(); + } + }; + + w->getItemText = [this](int idx, const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + return readText((*tObj)["text"]); + } + return std::string(""); + }; + + w->setItem(0); + } + + if(auto w = widget("simturnsPresetSelector")) + { + w->onConstructItems = [this](std::vector & curItems) + { + for (size_t i = 0; i < variables["simturnsPresets"].Vector().size(); ++i) + curItems.push_back((void*)i); + }; + + w->onSetItem = [setSimturnsPresetCallback](const void * item){ + size_t itemIndex = (size_t)item; + setSimturnsPresetCallback(itemIndex); + }; + } + + if(auto w = widget("timerPresetSelector")) + { + w->onConstructItems = [this](std::vector & curItems) + { + for (size_t i = 0; i < variables["timerPresets"].Vector().size(); ++i) + curItems.push_back((void*)i); + }; + + w->onSetItem = [setTimerPresetCallback](const void * item){ + size_t itemIndex = (size_t)item; + setTimerPresetCallback(itemIndex); + }; + } +} + +void OptionsTabBase::recreate(bool campaign) +{ + auto const & generateSimturnsDurationText = [](int days) -> std::string + { + if (days == 0) + return CGI->generaltexth->translate("core.genrltxt.523"); + + if (days >= 1000000) // Not "unlimited" but close enough + return CGI->generaltexth->translate("core.turndur.10"); + + bool canUseMonth = days % 28 == 0 && days >= 28*2; + bool canUseWeek = days % 7 == 0 && days >= 7*2; + + int value = days; + std::string text = "vcmi.optionsTab.simturns.days"; + + if (canUseWeek && !canUseMonth) + { + value = days / 7; + text = "vcmi.optionsTab.simturns.weeks"; + } + + if (canUseMonth) + { + value = days / 28; + text = "vcmi.optionsTab.simturns.months"; + } + + MetaString message; + message.appendTextID(Languages::getPluralFormTextID( CGI->generaltexth->getPreferredLanguage(), value, text)); + message.replaceNumber(value); + return message.toString(); + }; + + //Simultaneous turns + if(auto turnSlider = widget("simturnsDurationMin")) + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns); + + if(auto turnSlider = widget("simturnsDurationMax")) + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns); + + if(auto w = widget("labelSimturnsDurationValueMin")) + w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns)); + + if(auto w = widget("labelSimturnsDurationValueMax")) + w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.optionalTurns)); + + if(auto buttonSimturnsAI = widget("buttonSimturnsAI")) + buttonSimturnsAI->setSelectedSilent(SEL->getStartInfo()->simturnsInfo.allowHumanWithAI); + + if(auto buttonTurnTimerAccumulate = widget("buttonTurnTimerAccumulate")) + buttonTurnTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingTurnTimer); + + if(auto chessFieldTurnLabel = widget("chessFieldTurnLabel")) + { + if (SEL->getStartInfo()->turnTimerInfo.accumulatingTurnTimer) + chessFieldTurnLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldTurnAccumulate.help")); + else + chessFieldTurnLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldTurnDiscard.help")); + } + + if(auto chessFieldUnitLabel = widget("chessFieldUnitLabel")) + { + if (SEL->getStartInfo()->turnTimerInfo.accumulatingUnitTimer) + chessFieldUnitLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldUnitAccumulate.help")); + else + chessFieldUnitLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldUnitDiscard.help")); + } + + if(auto buttonUnitTimerAccumulate = widget("buttonUnitTimerAccumulate")) + buttonUnitTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingUnitTimer); + + const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; + + //classic timer + if(auto turnSlider = widget("sliderTurnDuration")) + { + if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.unitTimer && !turnTimerRemote.baseTimer) + { + for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) + { + auto & tpreset = variables["timerPresets"].Vector()[idx]; + if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) + { + turnSlider->scrollTo(idx); + if(auto w = widget("labelTurnDurationValue")) + w->setText(CGI->generaltexth->turnDurations[idx]); + } + } + } + } + + if(auto ww = widget("chessFieldBase")) + ww->setText(timeToString(turnTimerRemote.baseTimer), false); + if(auto ww = widget("chessFieldTurn")) + ww->setText(timeToString(turnTimerRemote.turnTimer), false); + if(auto ww = widget("chessFieldBattle")) + ww->setText(timeToString(turnTimerRemote.battleTimer), false); + if(auto ww = widget("chessFieldUnit")) + ww->setText(timeToString(turnTimerRemote.unitTimer), false); + + if(auto w = widget("timerModeSwitch")) + { + if(turnTimerRemote.battleTimer || turnTimerRemote.unitTimer || turnTimerRemote.baseTimer) + { + if(auto turnSlider = widget("sliderTurnDuration")) + if(turnSlider->isActive()) + w->setItem(1); + } + } + + if(auto buttonCheatAllowed = widget("buttonCheatAllowed")) + { + buttonCheatAllowed->setSelectedSilent(SEL->getStartInfo()->extraOptionsInfo.cheatsAllowed); + buttonCheatAllowed->block(SEL->screenType == ESelectionScreen::loadGame); + } + + if(auto buttonUnlimitedReplay = widget("buttonUnlimitedReplay")) + { + buttonUnlimitedReplay->setSelectedSilent(SEL->getStartInfo()->extraOptionsInfo.unlimitedReplay); + buttonUnlimitedReplay->block(SEL->screenType == ESelectionScreen::loadGame); + } + + if(auto textureCampaignOverdraw = widget("textureCampaignOverdraw")) + { + if(!campaign) + textureCampaignOverdraw->disable(); + } +} diff --git a/client/lobby/OptionsTabBase.h b/client/lobby/OptionsTabBase.h new file mode 100644 index 000000000..15c6372c9 --- /dev/null +++ b/client/lobby/OptionsTabBase.h @@ -0,0 +1,32 @@ +/* + * OptionsTabBase.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../gui/InterfaceObjectConfigurable.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct TurnTimerInfo; +struct SimturnsInfo; + +VCMI_LIB_NAMESPACE_END + +/// The options tab which is shown at the map selection phase. +class OptionsTabBase : public InterfaceObjectConfigurable +{ + std::vector getTimerPresets() const; + std::vector getSimturnsPresets() const; + +public: + OptionsTabBase(const JsonPath & configPath); + + void recreate(bool campaign = false); +}; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 7928320f9..4ff10a5b6 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -65,7 +65,7 @@ RandomMapTab::RandomMapTab(): addCallback("setPlayersCount", [&](int btnId) { - mapGenOptions->setPlayerCount(btnId); + mapGenOptions->setHumanOrCpuPlayerCount(btnId); setMapGenOptions(mapGenOptions); updateMapInfoByHost(); }); @@ -177,61 +177,56 @@ void RandomMapTab::updateMapInfoByHost() mapInfo->mapHeader->version = EMapFormat::VCMI; mapInfo->mapHeader->name.appendLocalString(EMetaText::GENERAL_TXT, 740); mapInfo->mapHeader->description.appendLocalString(EMetaText::GENERAL_TXT, 741); - mapInfo->mapHeader->difficulty = 1; // Normal + + const auto * temp = mapGenOptions->getMapTemplate(); + if (temp) + { + auto randomTemplateDescription = temp->getDescription(); + if (!randomTemplateDescription.empty()) + { + auto description = std::string("\n\n") + randomTemplateDescription; + mapInfo->mapHeader->description.appendRawString(description); + } + } + + mapInfo->mapHeader->difficulty = EMapDifficulty::NORMAL; mapInfo->mapHeader->height = mapGenOptions->getHeight(); mapInfo->mapHeader->width = mapGenOptions->getWidth(); mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels(); // Generate player information - int playersToGen = PlayerColor::PLAYER_LIMIT_I; - if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) - { - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE) - playersToGen = mapGenOptions->getPlayerCount() + mapGenOptions->getCompOnlyPlayerCount(); - else - playersToGen = mapGenOptions->getPlayerCount(); - } + int playersToGen = mapGenOptions->getMaxPlayersCount(); mapInfo->mapHeader->howManyTeams = playersToGen; - std::set occupiedTeams; + //TODO: Assign all human-controlled colors in first place + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { mapInfo->mapHeader->players[i].canComputerPlay = false; mapInfo->mapHeader->players[i].canHumanPlay = false; } - for(int i = 0; i < playersToGen; ++i) + std::vector availableColors; + for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; color++) { - PlayerInfo player; - player.isFactionRandom = true; - player.canComputerPlay = true; - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE && i >= mapGenOptions->getPlayerCount()) - { - player.canHumanPlay = false; - } - else - { - player.canHumanPlay = true; - } - auto team = mapGenOptions->getPlayersSettings().at(PlayerColor(i)).getTeam(); - player.team = team; - occupiedTeams.insert(team); - player.hasMainTown = true; - player.generateHeroAtMainTown = true; - mapInfo->mapHeader->players[i] = player; + availableColors.push_back(PlayerColor(color)); } - for(auto & player : mapInfo->mapHeader->players) + + //First restore known players + for (auto& player : mapGenOptions->getPlayersSettings()) { - for(int i = 0; player.team == TeamID::NO_TEAM; ++i) - { - TeamID team(i); - if(!occupiedTeams.count(team)) - { - player.team = team; - occupiedTeams.insert(team); - } - } + PlayerInfo playerInfo; + playerInfo.isFactionRandom = (player.second.getStartingTown() == FactionID::RANDOM); + playerInfo.canComputerPlay = (player.second.getPlayerType() != EPlayerType::HUMAN); + playerInfo.canHumanPlay = (player.second.getPlayerType() != EPlayerType::COMP_ONLY); + + auto team = player.second.getTeam(); + playerInfo.team = team; + playerInfo.hasMainTown = true; + playerInfo.generateHeroAtMainTown = true; + mapInfo->mapHeader->players[player.first] = playerInfo; + vstd::erase(availableColors, player.first); } mapInfoChanged(mapInfo, mapGenOptions); @@ -241,47 +236,72 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) { mapGenOptions = opts; - //prepare allowed options + //Prepare allowed options - add all, then erase the ones above the limit for(int i = 0; i <= PlayerColor::PLAYER_LIMIT_I; ++i) { playerCountAllowed.insert(i); compCountAllowed.insert(i); - playerTeamsAllowed.insert(i); - compTeamsAllowed.insert(i); + if (i >= 2) + { + playerTeamsAllowed.insert(i); + } + if (i >= 1) + { + compTeamsAllowed.insert(i); + } } + std::set humanCountAllowed; + auto * tmpl = mapGenOptions->getMapTemplate(); if(tmpl) { playerCountAllowed = tmpl->getPlayers().getNumbers(); - compCountAllowed = tmpl->getCpuPlayers().getNumbers(); + humanCountAllowed = tmpl->getHumanPlayers().getNumbers(); // Unused now? } - if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) + + si8 playerLimit = opts->getMaxPlayersCount(); + si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount(); + si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); + + if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE) { - vstd::erase_if(compCountAllowed, - [opts](int el){ - return PlayerColor::PLAYER_LIMIT_I - opts->getPlayerCount() < el; + vstd::erase_if(compCountAllowed, [playerLimit, humanOrCpuPlayerCount](int el) + { + return (playerLimit - humanOrCpuPlayerCount) < el; }); - vstd::erase_if(playerTeamsAllowed, - [opts](int el){ - return opts->getPlayerCount() <= el; + vstd::erase_if(playerTeamsAllowed, [humanOrCpuPlayerCount](int el) + { + return humanOrCpuPlayerCount <= el; }); - - if(!playerTeamsAllowed.count(opts->getTeamCount())) - opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); } - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE) + else // Random { - vstd::erase_if(playerCountAllowed, - [opts](int el){ - return PlayerColor::PLAYER_LIMIT_I - opts->getCompOnlyPlayerCount() < el; + vstd::erase_if(compCountAllowed, [playerLimit](int el) + { + return (playerLimit - 1) < el; // Must leave at least 1 human player }); - vstd::erase_if(compTeamsAllowed, - [opts](int el){ - return opts->getCompOnlyPlayerCount() <= el; + vstd::erase_if(playerTeamsAllowed, [playerLimit](int el) + { + return playerLimit <= el; + }); + } + if(!playerTeamsAllowed.count(opts->getTeamCount())) + { + opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); + } + + if(compOnlyPlayersCount != CMapGenOptions::RANDOM_SIZE) + { + // This setting doesn't impact total number of players + vstd::erase_if(compTeamsAllowed, [compOnlyPlayersCount](int el) + { + return compOnlyPlayersCount<= el; }); if(!compTeamsAllowed.count(opts->getCompOnlyTeamCount())) + { opts->setCompOnlyTeamCount(CMapGenOptions::RANDOM_SIZE); + } } if(auto w = widget("groupMapSize")) @@ -310,7 +330,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) } if(auto w = widget("groupMaxPlayers")) { - w->setSelected(opts->getPlayerCount()); + w->setSelected(opts->getHumanOrCpuPlayerCount()); deactivateButtonsFrom(*w, playerCountAllowed); } if(auto w = widget("groupMaxTeams")) @@ -344,13 +364,17 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) if(auto w = widget("templateButton")) { if(tmpl) - w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); + w->setTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); else - w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); + w->setTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); } for(auto r : VLC->roadTypeHandler->objects) { - if(auto w = widget(r->getJsonKey())) + // Workaround for vcmi-extras bug + std::string jsonKey = r->getJsonKey(); + std::string identifier = jsonKey.substr(jsonKey.find(':')+1); + + if(auto w = widget(identifier)) { w->setSelected(opts->isRoadEnabled(r->getId())); } @@ -364,9 +388,9 @@ void RandomMapTab::setTemplate(const CRmgTemplate * tmpl) if(auto w = widget("templateButton")) { if(tmpl) - w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); + w->setTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); else - w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); + w->setTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); } updateMapInfoByHost(); } @@ -397,6 +421,25 @@ std::vector RandomMapTab::getPossibleMapSizes() return {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, CMapHeader::MAP_SIZE_HUGE, CMapHeader::MAP_SIZE_XHUGE, CMapHeader::MAP_SIZE_GIANT}; } +void TeamAlignmentsWidget::checkTeamCount() +{ + //Do not allow to select one team only + std::set teams; + for (int plId = 0; plId < players.size(); ++plId) + { + teams.insert(TeamID(players[plId]->getSelected())); + } + if (teams.size() < 2) + { + //Do not let player close the window + buttonOk->block(true); + } + else + { + buttonOk->block(false); + } +} + TeamAlignments::TeamAlignments(RandomMapTab & randomMapTab) : CWindowObject(BORDERED) { @@ -415,10 +458,8 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): const JsonNode config(JsonPath::builtin("config/widgets/randomMapTeamsWidget.json")); variables = config["variables"]; - int humanPlayers = randomMapTab.obtainMapGenOptions().getPlayerCount(); - int cpuPlayers = randomMapTab.obtainMapGenOptions().getCompOnlyPlayerCount(); - int totalPlayers = humanPlayers == CMapGenOptions::RANDOM_SIZE || cpuPlayers == CMapGenOptions::RANDOM_SIZE - ? PlayerColor::PLAYER_LIMIT_I : humanPlayers + cpuPlayers; + //int totalPlayers = randomMapTab.obtainMapGenOptions().getPlayerLimit(); + int totalPlayers = randomMapTab.obtainMapGenOptions().getMaxPlayersCount(); assert(totalPlayers <= PlayerColor::PLAYER_LIMIT_I); auto settings = randomMapTab.obtainMapGenOptions().getPlayersSettings(); variables["totalPlayers"].Integer() = totalPlayers; @@ -458,6 +499,27 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): OBJ_CONSTRUCTION; + // Window should have X * X columns, where X is max players allowed for current settings + // For random player count, X is 8 + + if (totalPlayers > settings.size()) + { + auto savedPlayers = randomMapTab.obtainMapGenOptions().getSavedPlayersMap(); + for (const auto & player : savedPlayers) + { + if (!vstd::contains(settings, player.first)) + { + settings[player.first] = player.second; + } + } + } + + std::vector settingsVec; + for (const auto & player : settings) + { + settingsVec.push_back(player.second); + } + for(int plId = 0; plId < totalPlayers; ++plId) { players.push_back(std::make_shared([&, totalPlayers, plId](int sel) @@ -470,16 +532,21 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): assert(button); if(sel == teamId) { - button->addOverlay(buildWidget(variables["flagsAnimation"])); + button->setOverlay(buildWidget(variables["flagsAnimation"])); } else { - button->addOverlay(nullptr); + button->setOverlay(nullptr); } + button->addCallback([this](bool) + { + checkTeamCount(); + }); } })); OBJ_CONSTRUCTION_TARGETED(players.back().get()); + for(int teamId = 0; teamId < totalPlayers; ++teamId) { variables["point"]["x"].Integer() = variables["cellOffset"]["x"].Integer() + plId * variables["cellMargin"]["x"].Integer(); @@ -488,10 +555,17 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): players.back()->addToggle(teamId, std::dynamic_pointer_cast(button)); } - auto team = settings.at(PlayerColor(plId)).getTeam(); + // plId is not neccessarily player color, just an index + auto team = settingsVec.at(plId).getTeam(); if(team == TeamID::NO_TEAM) + { + logGlobal->warn("Player %d (id %d) has uninitialized team", settingsVec.at(plId).getColor(), plId); players.back()->setSelected(plId); + } else players.back()->setSelected(team.getNum()); } + + buttonOk = widget("buttonOK"); + buttonCancel = widget("buttonCancel"); } diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index 37b72f4ac..2ad559e80 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -48,7 +48,10 @@ private: std::shared_ptr mapInfo; //options allowed - need to store as impact each other - std::set playerCountAllowed, playerTeamsAllowed, compCountAllowed, compTeamsAllowed; + std::set playerCountAllowed; + std::set playerTeamsAllowed; + std::set compCountAllowed; + std::set compTeamsAllowed; }; class TeamAlignmentsWidget: public InterfaceObjectConfigurable @@ -57,9 +60,12 @@ public: TeamAlignmentsWidget(RandomMapTab & randomMapTab); private: + void checkTeamCount(); + std::shared_ptr background; std::shared_ptr labels; - std::shared_ptr buttonOk, buttonCancel; + std::shared_ptr buttonOk; + std::shared_ptr buttonCancel; std::vector> players; std::vector> placeholders; }; diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 8098715f2..73e7aca83 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -43,7 +43,6 @@ #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" #include "../../lib/TerrainHandler.h" -#include "../../lib/serializer/Connection.h" bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) { @@ -72,7 +71,10 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::sh return (a->defeatIconIndex < b->defeatIconIndex); break; case _playerAm: //by player amount - int playerAmntB, humenPlayersB, playerAmntA, humenPlayersA; + int playerAmntB; + int humenPlayersB; + int playerAmntA; + int humenPlayersA; playerAmntB = humenPlayersB = playerAmntA = humenPlayersA = 0; for(int i = 0; i < 8; i++) { @@ -110,6 +112,8 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::sh return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); case _fileName: //by filename return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI); + case _changeDate: //by changedate + return aaa->lastWrite < bbb->lastWrite; default: return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); } @@ -149,9 +153,11 @@ SelectionTab::SelectionTab(ESelectionScreen Type) : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false) { OBJ_CONSTRUCTION; - + generalSortingBy = getSortBySelectionScreen(tabType); + bool enableUiEnhancements = settings["general"]["enableUiEnhancements"].Bool(); + if(tabType != ESelectionScreen::campaignList) { sortingBy = _format; @@ -208,6 +214,13 @@ SelectionTab::SelectionTab(ESelectionScreen Type) break; } + if(enableUiEnhancements) + { + auto sortByDate = std::make_shared(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate)); + sortByDate->setOverlay(std::make_shared(ImagePath::builtin("lobby/selectionTabSortDate"))); + buttonsSortBy.push_back(sortByDate); + } + iconsMapFormats = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCSELC.DEF")); iconsVictoryCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRVICT.DEF")); iconsLossCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRLOSS.DEF")); @@ -215,7 +228,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type) listItems.push_back(std::make_shared(Point(30, 129 + i * 25), iconsMapFormats, iconsVictoryCondition, iconsLossCondition)); labelTabTitle = std::make_shared(205, 28, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, tabTitle); - slider = std::make_shared(Point(372, 86), tabType != ESelectionScreen::saveGame ? 480 : 430, std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, (int)curItems.size(), 0, Orientation::VERTICAL, CSlider::BLUE); + slider = std::make_shared(Point(372, 86 + (enableUiEnhancements ? 30 : 0)), (tabType != ESelectionScreen::saveGame ? 480 : 430) - (enableUiEnhancements ? 30 : 0), std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, (int)curItems.size(), 0, Orientation::VERTICAL, CSlider::BLUE); slider->setPanningStep(24); // create scroll bounds that encompass all area in this UI element to the left of slider (including area of slider itself) @@ -445,7 +458,7 @@ void SelectionTab::filter(int size, bool selectFirst) } } - std::shared_ptr folder = std::make_shared(); + auto folder = std::make_shared(); folder->isFolder = true; folder->folderName = folderName; auto itemIt = boost::range::find_if(curItems, [folder](std::shared_ptr e) { return e->folderName == folder->folderName; }); @@ -756,7 +769,7 @@ void SelectionTab::parseSaves(const std::unordered_set & files) mapInfo->saveInit(file); // Filter out other game modes - bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == StartInfo::CAMPAIGN; + bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == EStartMode::CAMPAIGN; bool isMultiplayer = mapInfo->amountOfHumanPlayersInSave > 1; bool isTutorial = boost::to_upper_copy(mapInfo->scenarioOptionsOfSave->mapname) == "MAPS/TUTORIAL"; switch(CSH->getLoadMode()) @@ -823,7 +836,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pictureEmptyLine = std::make_shared(GH.renderHandler().loadImage(ImagePath::builtin("camcust")), Rect(25, 121, 349, 26), -8, -14); - labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "", 185); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelAmountOfPlayers->setAutoRedraw(false); @@ -866,11 +879,13 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool iconLossCondition->disable(); labelNumberOfCampaignMaps->disable(); labelName->enable(); + labelName->setMaxWidth(316); labelName->setText(info->folderName); labelName->setColor(color); return; } + labelName->enable(); if(info->campaign) { labelAmountOfPlayers->disable(); @@ -885,6 +900,7 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool ostr << info->campaign->scenariosCount(); labelNumberOfCampaignMaps->setText(ostr.str()); labelNumberOfCampaignMaps->setColor(color); + labelName->setMaxWidth(316); } else { @@ -905,8 +921,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool iconVictoryCondition->setFrame(info->mapHeader->victoryIconIndex, 0); iconLossCondition->enable(); iconLossCondition->setFrame(info->mapHeader->defeatIconIndex, 0); + labelName->setMaxWidth(185); } - labelName->enable(); labelName->setText(info->getNameForList()); labelName->setColor(color); } diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index b457bcef8..529c2487f 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -23,7 +23,7 @@ class IImage; enum ESortBy { - _playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName + _playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName, _changeDate }; //_numOfMaps is for campaigns class ElementInfo : public CMapInfo diff --git a/client/lobby/TurnOptionsTab.cpp b/client/lobby/TurnOptionsTab.cpp new file mode 100644 index 000000000..6bf3523a8 --- /dev/null +++ b/client/lobby/TurnOptionsTab.cpp @@ -0,0 +1,18 @@ +/* + * TurnOptionsTab.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "TurnOptionsTab.h" + +TurnOptionsTab::TurnOptionsTab() + : OptionsTabBase(JsonPath::builtin("config/widgets/turnOptionsTab.json")) +{ + +} diff --git a/client/lobby/TurnOptionsTab.h b/client/lobby/TurnOptionsTab.h new file mode 100644 index 000000000..ad9ad1450 --- /dev/null +++ b/client/lobby/TurnOptionsTab.h @@ -0,0 +1,18 @@ +/* + * TurnOptionsTab.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "OptionsTabBase.h" + +class TurnOptionsTab : public OptionsTabBase +{ +public: + TurnOptionsTab(); +}; diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 4078908e1..9db6cf79c 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -66,7 +66,7 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name) if(!config[name]["exitbutton"].isNull()) { buttonBack = createExitButton(config[name]["exitbutton"]); - buttonBack->hoverable = true; + buttonBack->setHoverable(true); } for(const JsonNode & node : config[name]["items"].Vector()) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index de76d62b5..4d18d4c04 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -17,7 +17,7 @@ #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" -#include "../widgets/MiscWidgets.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../windows/InfoWindows.h" #include "../render/Canvas.h" @@ -29,8 +29,7 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/constants/EntityIdentifiers.h" #include "../../lib/TextOperations.h" - -#include "vstd/DateUtils.h" +#include "../../lib/Languages.h" auto HighScoreCalculation::calculate() { @@ -42,7 +41,8 @@ auto HighScoreCalculation::calculate() bool cheater = false; }; - Result firstResult, summary; + Result firstResult; + Result summary; const std::array difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; for(auto & param : parameters) { @@ -252,7 +252,7 @@ int CHighScoreInputScreen::addEntry(std::string text) { auto sortFunctor = [](const JsonNode & left, const JsonNode & right) { if(left["points"].Integer() == right["points"].Integer()) - return left["posFlag"].Integer() > right["posFlag"].Integer(); + return left["posFlag"].Bool() > right["posFlag"].Bool(); return left["points"].Integer() > right["points"].Integer(); }; @@ -264,7 +264,7 @@ int CHighScoreInputScreen::addEntry(std::string text) { newNode["scenarioName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].scenarioName; newNode["days"].Integer() = calc.calculate().sumDays; newNode["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; - newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(nullptr)); + newNode["datetime"].String() = TextOperations::getFormattedDateTimeLocal(std::time(nullptr)); newNode["posFlag"].Bool() = true; baseNode.push_back(newNode); @@ -325,7 +325,6 @@ void CHighScoreInputScreen::deactivate() { CCS->videoh->close(); CCS->soundh->stopSound(videoSoundHandle); - CIntObject::deactivate(); } void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) @@ -361,7 +360,7 @@ void CHighScoreInputScreen::keyPressed(EShortcut key) } CHighScoreInput::CHighScoreInput(std::string playerName, std::function readyCB) - : CWindowObject(0, ImagePath::builtin("HIGHNAME")), ready(readyCB) + : CWindowObject(NEEDS_ANIMATED_BACKGROUND, ImagePath::builtin("HIGHNAME")), ready(readyCB) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -373,7 +372,7 @@ CHighScoreInput::CHighScoreInput(std::string playerName, std::function(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); - textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); + textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, nullptr, ETextAlignment::CENTER, true); textInput->setText(playerName); } @@ -385,4 +384,4 @@ void CHighScoreInput::okay() void CHighScoreInput::abort() { ready(""); -} \ No newline at end of file +} diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 68e270866..f4e91daca 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -24,6 +24,9 @@ #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Canvas.h" +#include "../globalLobby/GlobalLobbyLoginWindow.h" +#include "../globalLobby/GlobalLobbyClient.h" +#include "../globalLobby/GlobalLobbyWindow.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" @@ -42,13 +45,12 @@ #include "../../CCallback.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/JsonNode.h" #include "../../lib/campaign/CampaignHandler.h" -#include "../../lib/serializer/Connection.h" #include "../../lib/serializer/CTypeList.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CCompressedStream.h" #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/modding/CModHandler.h" #include "../../lib/VCMIDirs.h" #include "../../lib/CStopWatch.h" #include "../../lib/CThreadHelper.h" @@ -57,11 +59,6 @@ #include "../../lib/CRandomGenerator.h" #include "../../lib/CondSh.h" -#if defined(SINGLE_PROCESS_APP) && defined(VCMI_ANDROID) -#include "../../server/CVCMIServer.h" -#include -#endif - std::shared_ptr CMM; ISelectionScreenInfo * SEL; @@ -184,11 +181,11 @@ static std::function genCommand(CMenuScreen * menu, std::vector(ESelectionScreen::newGame); }; case 2: - return std::bind(CMainMenu::openLobby, ESelectionScreen::campaignList, true, nullptr, ELoadMode::NONE); + return []() { CMainMenu::openLobby(ESelectionScreen::campaignList, true, {}, ELoadMode::NONE);}; case 3: return std::bind(CMainMenu::startTutorial); } @@ -199,13 +196,14 @@ static std::function genCommand(CMenuScreen * menu, std::vector(ESelectionScreen::loadGame); }; case 2: - return std::bind(CMainMenu::openLobby, ESelectionScreen::loadGame, true, nullptr, ELoadMode::CAMPAIGN); + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::CAMPAIGN);}; case 3: - return std::bind(CMainMenu::openLobby, ESelectionScreen::loadGame, true, nullptr, ELoadMode::TUTORIAL); + return []() { CMainMenu::openLobby(ESelectionScreen::loadGame, true, {}, ELoadMode::TUTORIAL);}; + } } break; @@ -262,7 +260,7 @@ CMenuEntry::CMenuEntry(CMenuScreen * parent, const JsonNode & config) for(const JsonNode & node : config["buttons"].Vector()) { buttons.push_back(createButton(parent, node)); - buttons.back()->hoverable = true; + buttons.back()->setHoverable(true); buttons.back()->setRedrawParent(true); } } @@ -274,9 +272,9 @@ CMainMenuConfig::CMainMenuConfig() } -CMainMenuConfig & CMainMenuConfig::get() +const CMainMenuConfig & CMainMenuConfig::get() { - static CMainMenuConfig config; + static const CMainMenuConfig config; return config; } @@ -339,15 +337,25 @@ void CMainMenu::update() menu->switchToTab(menu->getActiveTab()); } + static bool warnedAboutModDependencies = false; + + if (!warnedAboutModDependencies) + { + warnedAboutModDependencies = true; + auto errorMessages = CGI->modh->getModLoadErrors(); + + if (!errorMessages.empty()) + CInfoWindow::showInfoDialog(errorMessages, std::vector>(), PlayerColor(1)); + } + // Handles mouse and key input GH.handleEvents(); GH.windows().simpleRedraw(); } -void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode) +void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode) { - CSH->resetStateForLobby(screenType == ESelectionScreen::newGame ? StartInfo::NEW_GAME : StartInfo::LOAD_GAME, names); - CSH->screenType = screenType; + CSH->resetStateForLobby(screenType == ESelectionScreen::newGame ? EStartMode::NEW_GAME : EStartMode::LOAD_GAME, screenType, EServerMode::LOCAL, names); CSH->loadMode = loadMode; GH.windows().createAndPushWindow(host); @@ -362,20 +370,36 @@ void CMainMenu::openCampaignLobby(const std::string & campaignFileName, std::str void CMainMenu::openCampaignLobby(std::shared_ptr campaign) { - CSH->resetStateForLobby(StartInfo::CAMPAIGN); - CSH->screenType = ESelectionScreen::campaignList; + CSH->resetStateForLobby(EStartMode::CAMPAIGN, ESelectionScreen::campaignList, EServerMode::LOCAL, {}); CSH->campaignStateToSend = campaign; GH.windows().createAndPushWindow(); } void CMainMenu::openCampaignScreen(std::string name) { - if(vstd::contains(CMainMenuConfig::get().getCampaigns().Struct(), name)) + auto const & config = CMainMenuConfig::get().getCampaigns(); + + if(!vstd::contains(config.Struct(), name)) { - GH.windows().createAndPushWindow(CMainMenuConfig::get().getCampaigns(), name); + logGlobal->error("Unknown campaign set: %s", name); return; } - logGlobal->error("Unknown campaign set: %s", name); + + bool campaignsFound = true; + for (auto const & entry : config[name]["items"].Vector()) + { + ResourcePath resourceID(entry["file"].String(), EResType::CAMPAIGN); + if (!CResourceHandler::get()->existsResource(resourceID)) + campaignsFound = false; + } + + if (!campaignsFound) + { + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.client.errors.missingCampaigns"), std::vector>(), PlayerColor(1)); + return; + } + + GH.windows().createAndPushWindow(config, name); } void CMainMenu::startTutorial() @@ -389,7 +413,7 @@ void CMainMenu::startTutorial() auto mapInfo = std::make_shared(); mapInfo->mapInit(tutorialMap.getName()); - CMainMenu::openLobby(ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); + CMainMenu::openLobby(ESelectionScreen::newGame, true, {}, ELoadMode::NONE); CSH->startMapAfterConnection(mapInfo); } @@ -427,12 +451,21 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType) playerName->setText(getPlayerName()); playerName->cb += std::bind(&CMultiMode::onNameChange, this, _1); - buttonHotseat = std::make_shared(Point(373, 78), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); - buttonHost = std::make_shared(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); - buttonJoin = std::make_shared(Point(373, 78 + 57 * 2), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); + buttonHotseat = std::make_shared(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); + buttonLobby = std::make_shared(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this)); + + buttonHost = std::make_shared(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); + buttonJoin = std::make_shared(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); + buttonCancel = std::make_shared(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } +void CMultiMode::openLobby() +{ + close(); + CSH->getGlobalLobby().activateInterface(); +} + void CMultiMode::hostTCP() { auto savedScreenType = screenType; @@ -451,7 +484,7 @@ std::string CMultiMode::getPlayerName() { std::string name = settings["general"]["playerName"].String(); if(name == "Player") - name = CGI->generaltexth->translate("vcmi.mainMenu.playerName"); + name = CGI->generaltexth->translate("core.genrltxt.434"); return name; } @@ -504,7 +537,7 @@ void CMultiPlayers::enterSelectionScreen() Settings name = settings.write["general"]["playerName"]; name->String() = names[0]; - CMainMenu::openLobby(screenType, host, &names, loadMode); + CMainMenu::openLobby(screenType, host, names, loadMode); } CSimpleJoinScreen::CSimpleJoinScreen(bool host) @@ -516,10 +549,12 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) textTitle = std::make_shared("", Rect(20, 20, 205, 50), 0, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE); inputAddress = std::make_shared(Rect(25, 68, 175, 16), background->getSurface()); inputPort = std::make_shared(Rect(25, 115, 175, 16), background->getSurface()); + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); if(host && !settings["session"]["donotstartserver"].Bool()) { textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverConnecting")); - startConnectThread(); + buttonOk->block(true); + startConnection(); } else { @@ -527,12 +562,10 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) inputAddress->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); - buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); - inputAddress->giveFocus(); } - inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true); - inputPort->setText(std::to_string(CSH->getHostPort()), true); + inputAddress->setText(host ? CSH->getLocalHostname() : CSH->getRemoteHostname(), true); + inputPort->setText(std::to_string(host ? CSH->getLocalPort() : CSH->getRemotePort()), true); buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); @@ -544,20 +577,13 @@ void CSimpleJoinScreen::connectToServer() buttonOk->block(true); GH.stopTextInput(); - startConnectThread(inputAddress->getText(), boost::lexical_cast(inputPort->getText())); + startConnection(inputAddress->getText(), boost::lexical_cast(inputPort->getText())); } void CSimpleJoinScreen::leaveScreen() { - if(CSH->state == EClientState::CONNECTING) - { - textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverClosing")); - CSH->state = EClientState::CONNECTION_CANCELLED; - } - else if(GH.windows().isTopWindow(this)) - { - close(); - } + textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverClosing")); + CSH->setState(EClientState::CONNECTION_CANCELLED); } void CSimpleJoinScreen::onChange(const std::string & newText) @@ -565,43 +591,12 @@ void CSimpleJoinScreen::onChange(const std::string & newText) buttonOk->block(inputAddress->getText().empty() || inputPort->getText().empty()); } -void CSimpleJoinScreen::startConnectThread(const std::string & addr, ui16 port) +void CSimpleJoinScreen::startConnection(const std::string & addr, ui16 port) { -#if defined(SINGLE_PROCESS_APP) && defined(VCMI_ANDROID) - // in single process build server must use same JNIEnv as client - // as server runs in a separate thread, it must not attempt to search for Java classes (and they're already cached anyway) - // https://github.com/libsdl-org/SDL/blob/main/docs/README-android.md#threads-and-the-java-vm - CVCMIServer::reuseClientJNIEnv(SDL_AndroidGetJNIEnv()); -#endif - boost::thread connector(&CSimpleJoinScreen::connectThread, this, addr, port); - - connector.detach(); -} - -void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) -{ - setThreadName("connectThread"); - if(!addr.length()) - CSH->startLocalServerAndConnect(); + if(addr.empty()) + CSH->startLocalServerAndConnect(false); else - CSH->justConnectToServer(addr, port); - - // async call to prevent thread race - GH.dispatchMainThread([this](){ - if(CSH->state == EClientState::CONNECTION_FAILED) - { - CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.serverConnectionFailed"), {}); - - textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverAddressEnter")); - GH.startTextInput(inputAddress->pos); - buttonOk->block(false); - } - - if(GH.windows().isTopWindow(this)) - { - close(); - } - }); + CSH->connectToServer(addr, port); } CLoadingScreen::CLoadingScreen() @@ -616,7 +611,8 @@ CLoadingScreen::CLoadingScreen() const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; if(conf.isStruct()) { - const int posx = conf["x"].Integer(), posy = conf["y"].Integer(); + const int posx = conf["x"].Integer(); + const int posy = conf["y"].Integer(); const int blockSize = conf["size"].Integer(); const int blocksAmount = conf["amount"].Integer(); diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index ea9010797..e4d62aef1 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -10,7 +10,7 @@ #pragma once #include "../windows/CWindowObject.h" -#include "../../lib/JsonNode.h" +#include "../../lib/json/JsonNode.h" #include "../../lib/LoadProgress.h" VCMI_LIB_NAMESPACE_BEGIN @@ -31,11 +31,11 @@ class CLabel; // TODO: Find new location for these enums -enum ESelectionScreen : ui8 { +enum class ESelectionScreen : ui8 { unknown = 0, newGame, loadGame, saveGame, scenarioInfo, campaignList }; -enum ELoadMode : ui8 +enum class ELoadMode : ui8 { NONE = 0, SINGLE, MULTI, CAMPAIGN, TUTORIAL }; @@ -86,12 +86,14 @@ public: std::shared_ptr picture; std::shared_ptr playerName; std::shared_ptr buttonHotseat; + std::shared_ptr buttonLobby; std::shared_ptr buttonHost; std::shared_ptr buttonJoin; std::shared_ptr buttonCancel; std::shared_ptr statusBar; CMultiMode(ESelectionScreen ScreenType); + void openLobby(); void hostTCP(); void joinTCP(); std::string getPlayerName(); @@ -123,7 +125,7 @@ public: class CMainMenuConfig { public: - static CMainMenuConfig & get(); + static const CMainMenuConfig & get(); const JsonNode & getConfig() const; const JsonNode & getCampaigns() const; @@ -148,7 +150,7 @@ public: void activate() override; void onScreenResize() override; void update() override; - static void openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode); + static void openLobby(ESelectionScreen screenType, bool host, const std::vector & names, ELoadMode loadMode); static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); static void openCampaignLobby(std::shared_ptr campaign); static void startTutorial(); @@ -175,8 +177,7 @@ class CSimpleJoinScreen : public WindowBase void connectToServer(); void leaveScreen(); void onChange(const std::string & newText); - void startConnectThread(const std::string & addr = {}, ui16 port = 0); - void connectThread(const std::string & addr, ui16 port); + void startConnection(const std::string & addr = {}, ui16 port = 0); public: CSimpleJoinScreen(bool host = true); diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 6446d6d1b..a6257a4e6 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -14,16 +14,18 @@ #include "../CGameInfo.h" #include "../CMusicHandler.h" #include "../CVideoHandler.h" +#include "../gui/WindowHandler.h" #include "../gui/CGuiHandler.h" +#include "../gui/FramerateManager.h" #include "../widgets/TextControls.h" #include "../render/Canvas.h" CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback) - : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback) + : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback), elapsedTimeMilliseconds(0) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - addUsedEvents(LCLICK); + addUsedEvents(LCLICK | TIME); pos = center(Rect(0, 0, 800, 600)); updateShadow(); @@ -31,34 +33,42 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f videoSoundHandle = CCS->soundh->playSound(audioData); CCS->videoh->open(spe.prologVideo); CCS->musich->playMusic(spe.prologMusic, true, true); + voiceDurationMilliseconds = CCS->soundh->getSoundDurationMilliseconds(spe.prologVoice); voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); auto onVoiceStop = [this]() { voiceStopped = true; + elapsedTimeMilliseconds = 0; }; CCS->soundh->setCallback(voiceSoundHandle, onVoiceStop); text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText.toString()); - text->scrollTextTo(-100); + text->scrollTextTo(-50); // beginning of text in the vertical middle of black area +} + +void CPrologEpilogVideo::tick(uint32_t msPassed) +{ + elapsedTimeMilliseconds += msPassed; + + const uint32_t speed = (voiceDurationMilliseconds == 0) ? 150 : (voiceDurationMilliseconds / (text->textSize.y)); + + if(elapsedTimeMilliseconds > speed && text->textSize.y - 50 > positionCounter) + { + text->scrollTextBy(1); + elapsedTimeMilliseconds -= speed; + ++positionCounter; + } + else if(elapsedTimeMilliseconds > (voiceDurationMilliseconds == 0 ? 8000 : 3000) && voiceStopped) // pause after completed scrolling (longer for intros missing voice) + clickPressed(GH.getCursorPosition()); } void CPrologEpilogVideo::show(Canvas & to) { to.drawColor(pos, Colors::BLACK); - //BUG: some videos are 800x600 in size while some are 800x400 - //VCMI should center them in the middle of the screen. Possible but needs modification - //of video player API which I'd like to avoid until we'll get rid of Windows-specific player - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); + //some videos are 800x600 in size while some are 800x400 + CCS->videoh->update(pos.x, pos.y + (CCS->videoh->size().y == 400 ? 100 : 0), to.getInternalSurface(), true, false); - //move text every 5 calls/frames; seems to be good enough - ++positionCounter; - if(positionCounter % 5 == 0) - text->scrollTextBy(1); - else - text->showAll(to); // blit text over video, if needed - - if(text->textSize.y + 100 < positionCounter / 5 && voiceStopped) - clickPressed(GH.getCursorPosition()); + text->showAll(to); // blit text over video, if needed } void CPrologEpilogVideo::clickPressed(const Point & cursorPosition) @@ -66,5 +76,6 @@ void CPrologEpilogVideo::clickPressed(const Point & cursorPosition) close(); CCS->soundh->stopSound(voiceSoundHandle); CCS->soundh->stopSound(videoSoundHandle); - exitCb(); + if(exitCb) + exitCb(); } diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index 1666a87c1..5923791d5 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -19,6 +19,8 @@ class CPrologEpilogVideo : public CWindowObject CampaignScenarioPrologEpilog spe; int positionCounter; int voiceSoundHandle; + uint32_t voiceDurationMilliseconds; + uint32_t elapsedTimeMilliseconds; int videoSoundHandle; std::function exitCb; @@ -29,6 +31,7 @@ class CPrologEpilogVideo : public CWindowObject public: CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback); + void tick(uint32_t msPassed) override; void clickPressed(const Point & cursorPosition) override; void show(Canvas & to) override; }; diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index 6a3947ce9..ebe2dbf14 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -134,8 +134,10 @@ std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIn MapRendererTerrain::MapRendererTerrain() : storage(VLC->terrainTypeHandler->objects.size()) { + logGlobal->debug("Loading map terrains"); for(const auto & terrain : VLC->terrainTypeHandler->objects) storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); + logGlobal->debug("Done loading map terrains"); } void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) @@ -173,8 +175,10 @@ uint8_t MapRendererTerrain::checksum(IMapRendererContext & context, const int3 & MapRendererRiver::MapRendererRiver() : storage(VLC->riverTypeHandler->objects.size()) { + logGlobal->debug("Loading map rivers"); for(const auto & river : VLC->riverTypeHandler->objects) storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); + logGlobal->debug("Done loading map rivers"); } void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) @@ -208,8 +212,10 @@ uint8_t MapRendererRiver::checksum(IMapRendererContext & context, const int3 & c MapRendererRoad::MapRendererRoad() : storage(VLC->roadTypeHandler->objects.size()) { + logGlobal->debug("Loading map roads"); for(const auto & road : VLC->roadTypeHandler->objects) storage.load(road->getIndex(), road->tilesFilename, EImageBlitMode::COLORKEY); + logGlobal->debug("Done loading map roads"); } void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index d3c896029..88348af09 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -21,6 +21,7 @@ #include "../../lib/Point.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/mapping/CMap.h" #include "../../lib/pathfinder/CGPathNode.h" @@ -427,11 +428,7 @@ size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates) if(!object->visitableAt(coordinates.x, coordinates.y)) continue; - ObjectPosInfo info; - info.pos = coordinates; - info.id = object->ID; - info.subId = object->subID; - info.owner = object->tempOwner; + ObjectPosInfo info(object); size_t iconIndex = selectOverlayImageForObject(info); diff --git a/client/mapView/MapRendererContextState.cpp b/client/mapView/MapRendererContextState.cpp index 196ff9671..aa1a6ab0a 100644 --- a/client/mapView/MapRendererContextState.cpp +++ b/client/mapView/MapRendererContextState.cpp @@ -34,8 +34,10 @@ MapRendererContextState::MapRendererContextState() objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + logGlobal->debug("Loading map objects"); for(const auto & obj : CGI->mh->getMap()->objects) addObject(obj); + logGlobal->debug("Done loading map objects"); } void MapRendererContextState::addObject(const CGObjectInstance * obj) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index b39c8a6f5..d1375f1df 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -122,7 +122,10 @@ void MapView::onMapLevelSwitched() void MapView::onMapScrolled(const Point & distance) { if(!isGesturing()) + { + postSwipeSpeed = 0.0; controller->setViewCenter(model->getMapViewCenter() + distance, model->getLevel()); + } } void MapView::onMapSwiped(const Point & viewPosition) diff --git a/client/mapView/MapViewCache.cpp b/client/mapView/MapViewCache.cpp index ad851ef11..2bd21ce8d 100644 --- a/client/mapView/MapViewCache.cpp +++ b/client/mapView/MapViewCache.cpp @@ -141,7 +141,7 @@ void MapViewCache::update(const std::shared_ptr & context) void MapViewCache::render(const std::shared_ptr & context, Canvas & target, bool fullRedraw) { bool mapMoved = (cachedPosition != model->getMapViewCenter()); - bool lazyUpdate = !mapMoved && !fullRedraw && context->viewTransitionProgress() == 0; + bool lazyUpdate = !mapMoved && !fullRedraw && vstd::isAlmostZero(context->viewTransitionProgress()); Rect dimensions = model->getTilesTotalRect(); @@ -184,7 +184,7 @@ void MapViewCache::render(const std::shared_ptr & context, } } - if(context->viewTransitionProgress() != 0) + if(!vstd::isAlmostZero(context->viewTransitionProgress())) target.drawTransparent(*terrainTransition, Point(0, 0), 1.0 - context->viewTransitionProgress()); cachedPosition = model->getMapViewCenter(); diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index d710dc6d2..5a0a4d873 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -269,6 +269,9 @@ void MapViewController::afterRender() bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator) { + if(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool()) + return true; + if (!isEventVisible(obj, initiator)) return true; diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index fdd4fae86..aac3232e1 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -74,6 +74,10 @@ std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) cons bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) { + //FIXME: Optimize + // this method is called A LOT on game start and some parts, e.g. for loops are too slow for that + + assert(a && b); if(!a) return true; if(!b) diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 8077c3438..93549f333 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -14,7 +14,7 @@ #include "Graphics.h" #include "../../lib/filesystem/Filesystem.h" -#include "../../lib/JsonNode.h" +#include "../../lib/json/JsonUtils.h" #include "../renderSDL/SDLImage.h" std::shared_ptr CAnimation::getFromExtraDef(std::string filename) @@ -102,7 +102,7 @@ void CAnimation::initFromJson(const JsonNode & config) std::string basepath; basepath = config["basepath"].String(); - JsonNode base(JsonNode::JsonType::DATA_STRUCT); + JsonNode base; base["margins"] = config["margins"]; base["width"] = config["width"]; base["height"] = config["height"]; @@ -114,7 +114,7 @@ void CAnimation::initFromJson(const JsonNode & config) for(const JsonNode & frame : group["frames"].Vector()) { - JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); + JsonNode toAdd; JsonUtils::inherit(toAdd, base); toAdd["file"].String() = basepath + frame.String(); source[groupID].push_back(toAdd); @@ -129,7 +129,7 @@ void CAnimation::initFromJson(const JsonNode & config) if (source[group].size() <= frame) source[group].resize(frame+1); - JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); + JsonNode toAdd; JsonUtils::inherit(toAdd, base); toAdd["file"].String() = basepath + node["file"].String(); source[group][frame] = toAdd; @@ -191,7 +191,7 @@ void CAnimation::init() std::unique_ptr textData(new ui8[stream->getSize()]); stream->read(textData.get(), stream->getSize()); - const JsonNode config((char*)textData.get(), stream->getSize()); + const JsonNode config(reinterpret_cast(textData.get()), stream->getSize()); initFromJson(config); } @@ -204,11 +204,19 @@ void CAnimation::printError(size_t frame, size_t group, std::string type) const CAnimation::CAnimation(const AnimationPath & Name): name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")), - preloaded(false), - defFile() + preloaded(false) { if(CResourceHandler::get()->existsResource(name)) - defFile = std::make_shared(name); + { + try + { + defFile = std::make_shared(name); + } + catch ( const std::runtime_error & e) + { + logAnim->error("Def file %s failed to load! Reason: %s", Name.getOriginalName(), e.what()); + } + } init(); diff --git a/client/render/CBitmapHandler.cpp b/client/render/CBitmapHandler.cpp index 79b597480..a6e0836f0 100644 --- a/client/render/CBitmapHandler.cpp +++ b/client/render/CBitmapHandler.cpp @@ -157,11 +157,11 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path) ret->format->palette->colors[0].g == 0 && ret->format->palette->colors[0].b == 255 ) { - static SDL_Color shadow[3] = + constexpr std::array shadow = { - { 0, 0, 0, 0},// 100% - transparency - { 0, 0, 0, 32},// 75% - shadow border, - { 0, 0, 0, 128},// 50% - shadow body + SDL_Color{ 0, 0, 0, 0},// 100% - transparency + SDL_Color{ 0, 0, 0, 32},// 75% - shadow border, + SDL_Color{ 0, 0, 0, 128},// 50% - shadow body }; CSDL_Ext::setColorKey(ret, ret->format->palette->colors[0]); diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index 261c7b556..1e6bd290d 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -182,6 +182,20 @@ void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color) CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color)); } +void Canvas::fillTexture(const std::shared_ptr& image) +{ + assert(image); + if (!image) + return; + + Rect imageArea(Point(0, 0), image->dimensions()); + for (int y=0; y < surface->h; y+= imageArea.h) + { + for (int x=0; x < surface->w; x+= imageArea.w) + image->draw(surface, renderArea.x + x, renderArea.y + y); + } +} + SDL_Surface * Canvas::getInternalSurface() { return surface; diff --git a/client/render/Canvas.h b/client/render/Canvas.h index 8a4abcf67..647c1ddde 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -99,6 +99,9 @@ public: /// fills selected area with blended color void drawColorBlended(const Rect & target, const ColorRGBA & color); + /// fills canvas with texture + void fillTexture(const std::shared_ptr& image); + /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. SDL_Surface * getInternalSurface(); diff --git a/client/render/ColorFilter.cpp b/client/render/ColorFilter.cpp index d6234116f..8b2288b4e 100644 --- a/client/render/ColorFilter.cpp +++ b/client/render/ColorFilter.cpp @@ -10,8 +10,8 @@ #include "StdInc.h" #include "ColorFilter.h" -#include "../../lib/JsonNode.h" #include "../../lib/Color.h" +#include "../../lib/json/JsonNode.h" ColorRGBA ColorFilter::shiftColor(const ColorRGBA & in) const { @@ -41,10 +41,10 @@ bool ColorFilter::operator != (const ColorFilter & other) const bool ColorFilter::operator == (const ColorFilter & other) const { return - r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a && - g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a && - b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a && - a == other.a; + vstd::isAlmostEqual(r.r, other.r.r) && vstd::isAlmostEqual(r.g, other.r.g) && vstd::isAlmostEqual(r.b, other.r.b) && vstd::isAlmostEqual(r.a, other.r.a) && + vstd::isAlmostEqual(g.r, other.g.r) && vstd::isAlmostEqual(g.g, other.g.g) && vstd::isAlmostEqual(g.b, other.g.b) && vstd::isAlmostEqual(g.a, other.g.a) && + vstd::isAlmostEqual(b.r, other.b.r) && vstd::isAlmostEqual(b.g, other.b.g) && vstd::isAlmostEqual(b.b, other.b.b) && vstd::isAlmostEqual(b.a, other.b.a) && + vstd::isAlmostEqual(a, other.a); } ColorFilter ColorFilter::genEmptyShifter( ) diff --git a/client/render/Colors.cpp b/client/render/Colors.cpp index 0c239e879..d0bd8e23f 100644 --- a/client/render/Colors.cpp +++ b/client/render/Colors.cpp @@ -10,7 +10,8 @@ #include "StdInc.h" #include "Colors.h" -#include "../../lib/JsonNode.h" + +#include "../../lib/json/JsonNode.h" const ColorRGBA Colors::YELLOW = { 229, 215, 123, ColorRGBA::ALPHA_OPAQUE }; const ColorRGBA Colors::WHITE = { 255, 243, 222, ColorRGBA::ALPHA_OPAQUE }; @@ -29,11 +30,11 @@ std::optional Colors::parseColor(std::string text) { std::smatch match; std::regex expr("^#([0-9a-fA-F]{6})$"); - ui8 rgb[3] = {0, 0, 0}; + std::vector rgb; + rgb.reserve(3); if(std::regex_search(text, match, expr)) { - std::string tmp = boost::algorithm::unhex(match[1].str()); - std::copy(tmp.begin(), tmp.end(), rgb); + boost::algorithm::unhex(match[1].str(), std::back_inserter(rgb)); return ColorRGBA(rgb[0], rgb[1], rgb[2]); } @@ -42,11 +43,10 @@ std::optional Colors::parseColor(std::string text) for(auto & color : colors) { if(boost::algorithm::to_lower_copy(color.first) == boost::algorithm::to_lower_copy(text)) { - std::string tmp = boost::algorithm::unhex(color.second.String()); - std::copy(tmp.begin(), tmp.end(), rgb); + boost::algorithm::unhex(color.second.String(), std::back_inserter(rgb)); return ColorRGBA(rgb[0], rgb[1], rgb[2]); } } return std::nullopt; -} \ No newline at end of file +} diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index 603972feb..ad8f90d92 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -29,13 +29,13 @@ #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CBinaryReader.h" +#include "../../lib/json/JsonNode.h" #include "../lib/modding/CModHandler.h" #include "../lib/modding/ModScope.h" #include "CGameInfo.h" #include "../lib/VCMI_Lib.h" #include "../CCallback.h" #include "../lib/CGeneralTextHandler.h" -#include "../lib/JsonNode.h" #include "../lib/vcmi_endian.h" #include "../lib/CStopWatch.h" #include "../lib/CHeroHandler.h" @@ -107,7 +107,7 @@ void Graphics::initializeBattleGraphics() if(!CResourceHandler::get(mod)->existsResource(ResourcePath("config/battles_graphics.json"))) continue; - const JsonNode config(mod, JsonPath::builtin("config/battles_graphics.json")); + const JsonNode config(JsonPath::builtin("config/battles_graphics.json"), mod); //initialization of AC->def name mapping if(!config["ac_mapping"].isNull()) diff --git a/client/renderSDL/CBitmapHanFont.cpp b/client/renderSDL/CBitmapHanFont.cpp index cb527d054..ac1684185 100644 --- a/client/renderSDL/CBitmapHanFont.cpp +++ b/client/renderSDL/CBitmapHanFont.cpp @@ -13,8 +13,8 @@ #include "CBitmapFont.h" #include "SDL_Extensions.h" -#include "../../lib/JsonNode.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/json/JsonNode.h" #include "../../lib/TextOperations.h" #include "../../lib/Rect.h" diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index b9d19aa12..e977a6c4c 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -15,8 +15,8 @@ #include "../render/Colors.h" #include "../renderSDL/SDL_Extensions.h" -#include "../../lib/JsonNode.h" #include "../../lib/TextOperations.h" +#include "../../lib/json/JsonNode.h" #include "../../lib/filesystem/Filesystem.h" #include diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index e8930abd3..465597b63 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -18,7 +18,7 @@ #include "../render/CDefFile.h" #include "../render/Graphics.h" -#include "../../lib/JsonNode.h" +#include "../../lib/json/JsonNode.h" #include @@ -184,7 +184,7 @@ std::shared_ptr SDLImage::scaleFast(const Point & size) const else CSDL_Ext::setDefaultColorKey(scaled);//just in case - SDLImage * ret = new SDLImage(scaled, EImageBlitMode::ALPHA); + auto * ret = new SDLImage(scaled, EImageBlitMode::ALPHA); ret->fullSize.x = (int) round((float)fullSize.x * scaleX); ret->fullSize.y = (int) round((float)fullSize.y * scaleY); @@ -205,6 +205,8 @@ void SDLImage::exportBitmap(const boost::filesystem::path& path) const void SDLImage::playerColored(PlayerColor player) { + if (!surf) + return; graphics->blueToPlayersAdv(surf, player); } diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 300611d70..9000473c4 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -246,7 +246,10 @@ int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRec if (src->format->BytesPerPixel==1 && (bpp==3 || bpp==4 || bpp==2)) //everything's ok { SDL_Rect fulldst; - int srcx, srcy, w, h; + int srcx; + int srcy; + int w; + int h; /* If the destination rectangle is nullptr, use the entire dest surface */ if ( dstRect == nullptr ) @@ -258,7 +261,8 @@ int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRec /* clip the source rectangle to the source surface */ if(srcRect) { - int maxw, maxh; + int maxw; + int maxh; srcx = srcRect->x; w = srcRect->w; @@ -295,7 +299,8 @@ int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRec /* clip the destination rectangle against the clip rectangle */ { SDL_Rect *clip = &dst->clip_rect; - int dx, dy; + int dx; + int dy; dx = clip->x - dstRect->x; if(dx > 0) @@ -659,16 +664,16 @@ void CSDL_Ext::convertToGrayscale( SDL_Surface * surf, const Rect & rect ) template void scaleSurfaceFastInternal(SDL_Surface *surf, SDL_Surface *ret) { - const float factorX = float(surf->w) / float(ret->w), - factorY = float(surf->h) / float(ret->h); + const float factorX = static_cast(surf->w) / static_cast(ret->w); + const float factorY = static_cast(surf->h) / static_cast(ret->h); for(int y = 0; y < ret->h; y++) { for(int x = 0; x < ret->w; x++) { //coordinates we want to calculate - int origX = static_cast(floor(factorX * x)), - origY = static_cast(floor(factorY * y)); + auto origX = static_cast(floor(factorX * x)); + auto origY = static_cast(floor(factorY * y)); // Get pointers to source pixels uint8_t *srcPtr = (uint8_t*)surf->pixels + origY * surf->pitch + origX * bpp; diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 1371c51c7..f8ceb26f1 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -12,6 +12,7 @@ #include "ScreenHandler.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/constants/StringConstants.h" #include "../gui/CGuiHandler.h" #include "../eventsSDL/NotificationHandler.h" #include "../gui/WindowHandler.h" @@ -284,7 +285,12 @@ void ScreenHandler::initializeWindow() mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), rendererFlags); if(mainRenderer == nullptr) - throw std::runtime_error("Unable to create renderer\n"); + { + const char * error = SDL_GetError(); + std::string messagePattern = "Failed to create SDL renderer. Reason: %s"; + std::string message = boost::str(boost::format(messagePattern) % error); + handleFatalError(message, true); + } SDL_RendererInfo info; SDL_GetRendererInfo(mainRenderer, &info); diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index b4706fa3a..1666d9d72 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -22,6 +22,7 @@ #include "../gui/CGuiHandler.h" #include "../gui/MouseButton.h" #include "../gui/Shortcut.h" +#include "../gui/InterfaceObjectConfigurable.h" #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" @@ -29,33 +30,28 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" +#include "../../lib/filesystem/Filesystem.h" -void CButton::update() +void ButtonBase::update() { if (overlay) { Point targetPos = Rect::createCentered( pos, overlay->pos.dimensions()).topLeft(); - if (state == PRESSED) + if (state == EButtonState::PRESSED) overlay->moveTo(targetPos + Point(1,1)); else overlay->moveTo(targetPos); } - int newPos = stateToIndex[int(state)]; - if(animateLonelyFrame) + if (image) { - if(state == PRESSED) - image->moveBy(Point(1,1)); - else - image->moveBy(Point(-1,-1)); + // checkbox - has only have two frames: normal and pressed/highlighted + // hero movement speed buttons: only three frames: normal, pressed and blocked/highlighted + if (state == EButtonState::HIGHLIGHTED && image->size() < 4) + image->setFrame(image->size()-1); + image->setFrame(stateToIndex[vstd::to_underlying(state)]); } - if (newPos < 0) - newPos = 0; - - if (state == HIGHLIGHTED && image->size() < 4) - newPos = (int)image->size()-1; - image->setFrame(newPos); if (isActive()) redraw(); @@ -71,14 +67,14 @@ void CButton::addCallback(const std::function & callback) this->callback += callback; } -void CButton::addTextOverlay(const std::string & Text, EFonts font, ColorRGBA color) +void ButtonBase::setTextOverlay(const std::string & Text, EFonts font, ColorRGBA color) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - addOverlay(std::make_shared(pos.w/2, pos.h/2, font, ETextAlignment::CENTER, color, Text)); + setOverlay(std::make_shared(pos.w/2, pos.h/2, font, ETextAlignment::CENTER, color, Text)); update(); } -void CButton::addOverlay(std::shared_ptr newOverlay) +void ButtonBase::setOverlay(const std::shared_ptr& newOverlay) { overlay = newOverlay; if(overlay) @@ -90,17 +86,59 @@ void CButton::addOverlay(std::shared_ptr newOverlay) update(); } -void CButton::addImage(const AnimationPath & filename) +void ButtonBase::setImage(const AnimationPath & defName, bool playerColoredButton) { - imageNames.push_back(filename); + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + configurable.reset(); + image = std::make_shared(defName, vstd::to_underlying(getState())); + pos = image->pos; + + if (playerColoredButton) + image->playerColored(LOCPLINT->playerID); } -void CButton::addHoverText(ButtonState state, std::string text) +const JsonNode & ButtonBase::getCurrentConfig() const { - hoverTexts[state] = text; + if (!config) + throw std::runtime_error("No config found in button!"); + + static constexpr std::array stateToConfig = { + "normal", + "pressed", + "blocked", + "highlighted" + }; + + std::string key = stateToConfig[vstd::to_underlying(getState())]; + const JsonNode & value = (*config)[key]; + + if (value.isNull()) + throw std::runtime_error("No config found in button for state " + key + "!"); + + return value; } -void CButton::setImageOrder(int state1, int state2, int state3, int state4) +void ButtonBase::setConfigurable(const JsonPath & jsonName, bool playerColoredButton) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + config = std::make_unique(jsonName); + + image.reset(); + configurable = std::make_shared(getCurrentConfig()); + pos = configurable->pos; + + if (playerColoredButton) + image->playerColored(LOCPLINT->playerID); +} + +void CButton::addHoverText(EButtonState state, const std::string & text) +{ + hoverTexts[vstd::to_underlying(state)] = text; +} + +void ButtonBase::setImageOrder(int state1, int state2, int state3, int state4) { stateToIndex[0] = state1; stateToIndex[1] = state2; @@ -109,44 +147,79 @@ void CButton::setImageOrder(int state1, int state2, int state3, int state4) update(); } -void CButton::setAnimateLonelyFrame(bool agreement) +std::shared_ptr ButtonBase::getOverlay() { - animateLonelyFrame = agreement; + return overlay; } -void CButton::setState(ButtonState newState) + +void ButtonBase::setStateImpl(EButtonState newState) { - if (state == newState) + state = newState; + + if (configurable) + { + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + configurable = std::make_shared(getCurrentConfig()); + pos = configurable->pos; + + if (overlay) + { + // Force overlay on top + removeChild(overlay.get()); + addChild(overlay.get()); + } + } + + update(); +} + +void CButton::setState(EButtonState newState) +{ + if (getState() == newState) return; - if (newState == BLOCKED) + if (newState == EButtonState::BLOCKED) removeUsedEvents(LCLICK | SHOW_POPUP | HOVER | KEYBOARD); else addUsedEvents(LCLICK | SHOW_POPUP | HOVER | KEYBOARD); - - state = newState; - update(); + setStateImpl(newState); } -CButton::ButtonState CButton::getState() +EButtonState ButtonBase::getState() const { return state; } bool CButton::isBlocked() { - return state == BLOCKED; + return getState() == EButtonState::BLOCKED; } bool CButton::isHighlighted() { - return state == HIGHLIGHTED; + return getState() == EButtonState::HIGHLIGHTED; +} + +void CButton::setHoverable(bool on) +{ + hoverable = on; +} + +void CButton::setSoundDisabled(bool on) +{ + soundDisabled = on; +} + +void CButton::setActOnDown(bool on) +{ + actOnDown = on; } void CButton::block(bool on) { - if(on || state == BLOCKED) //dont change button state if unblock requested, but it's not blocked - setState(on ? BLOCKED : NORMAL); + if(on || getState() == EButtonState::BLOCKED) //dont change button state if unblock requested, but it's not blocked + setState(on ? EButtonState::BLOCKED : EButtonState::NORMAL); } void CButton::onButtonClicked() @@ -169,14 +242,14 @@ void CButton::clickPressed(const Point & cursorPosition) if(isBlocked()) return; - if (getState() != PRESSED) + if (getState() != EButtonState::PRESSED) { if (!soundDisabled) { CCS->soundh->playSound(soundBase::button); GH.input().hapticFeedback(); } - setState(PRESSED); + setState(EButtonState::PRESSED); if (actOnDown) onButtonClicked(); @@ -185,12 +258,12 @@ void CButton::clickPressed(const Point & cursorPosition) void CButton::clickReleased(const Point & cursorPosition) { - if (getState() == PRESSED) + if (getState() == EButtonState::PRESSED) { if(hoverable && isHovered()) - setState(HIGHLIGHTED); + setState(EButtonState::HIGHLIGHTED); else - setState(NORMAL); + setState(EButtonState::NORMAL); if (!actOnDown) onButtonClicked(); @@ -199,18 +272,18 @@ void CButton::clickReleased(const Point & cursorPosition) void CButton::clickCancel(const Point & cursorPosition) { - if (getState() == PRESSED) + if (getState() == EButtonState::PRESSED) { if(hoverable && isHovered()) - setState(HIGHLIGHTED); + setState(EButtonState::HIGHLIGHTED); else - setState(NORMAL); + setState(EButtonState::NORMAL); } } void CButton::showPopupWindow(const Point & cursorPosition) { - if(helpBox.size()) //there is no point to show window with nothing inside... + if(!helpBox.empty()) //there is no point to show window with nothing inside... CRClickPopup::createAndPush(helpBox); } @@ -219,17 +292,17 @@ void CButton::hover (bool on) if(hoverable && !isBlocked()) { if(on) - setState(HIGHLIGHTED); + setState(EButtonState::HIGHLIGHTED); else - setState(NORMAL); + setState(EButtonState::NORMAL); } /*if(pressedL && on) // WTF is this? When this is used? setState(PRESSED);*/ - std::string name = hoverTexts[getState()].empty() + std::string name = hoverTexts[vstd::to_underlying(getState())].empty() ? hoverTexts[0] - : hoverTexts[getState()]; + : hoverTexts[vstd::to_underlying(getState())]; if(!name.empty() && !isBlocked()) //if there is no name, there is nothing to display also { @@ -240,55 +313,43 @@ void CButton::hover (bool on) } } -CButton::CButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList Callback, EShortcut key, bool playerColoredButton): - CKeyShortcut(key), - callback(Callback) +ButtonBase::ButtonBase(Point position, const AnimationPath & defName, EShortcut key, bool playerColoredButton) + : CKeyShortcut(key) + , stateToIndex({0, 1, 2, 3}) + , state(EButtonState::NORMAL) { - defActions = 255-DISPOSE; - addUsedEvents(LCLICK | SHOW_POPUP | HOVER | KEYBOARD); - - stateToIndex[0] = 0; - stateToIndex[1] = 1; - stateToIndex[2] = 2; - stateToIndex[3] = 3; - - state=NORMAL; - - currentImage = -1; - hoverable = actOnDown = soundDisabled = false; - hoverTexts[0] = help.first; - helpBox=help.second; - pos.x += position.x; pos.y += position.y; - if (!defName.empty()) + JsonPath jsonConfig = defName.toType().addPrefix("CONFIG/WIDGETS/BUTTONS/"); + if (CResourceHandler::get()->existsResource(jsonConfig)) { - imageNames.push_back(defName); - setIndex(0); - if (playerColoredButton) - image->playerColored(LOCPLINT->playerID); + setConfigurable(jsonConfig, playerColoredButton); + return; + } + else + { + setImage(defName, playerColoredButton); + return; } } -void CButton::setIndex(size_t index) +ButtonBase::~ButtonBase() = default; + +CButton::CButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList Callback, EShortcut key, bool playerColoredButton): + ButtonBase(position, defName, key, playerColoredButton), + callback(Callback), + helpBox(help.second), + actOnDown(false), + hoverable(false), + soundDisabled(false) { - if (index == currentImage || index>=imageNames.size()) - return; - currentImage = index; - auto anim = GH.renderHandler().loadAnimation(imageNames[index]); - setImage(anim); + defActions = 255-DISPOSE; + addUsedEvents(LCLICK | SHOW_POPUP | HOVER | KEYBOARD); + hoverTexts[0] = help.first; } -void CButton::setImage(std::shared_ptr anim, int animFlags) -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - image = std::make_shared(anim, getState(), 0, 0, 0, animFlags); - pos = image->pos; -} - -void CButton::setPlayerColor(PlayerColor player) +void ButtonBase::setPlayerColor(PlayerColor player) { if (image && image->isPlayerColored()) image->playerColored(player); @@ -353,41 +414,48 @@ void CToggleBase::setSelected(bool on) callback(on); } -bool CToggleBase::canActivate() +bool CToggleBase::isSelected() const { - if (selected && !allowDeselection) - return false; - return true; + return selected; } -void CToggleBase::addCallback(std::function function) +bool CToggleBase::canActivate() const +{ + return !selected || allowDeselection; +} + +void CToggleBase::addCallback(const std::function & function) { callback += function; } +void CToggleBase::setAllowDeselection(bool on) +{ + allowDeselection = on; +} + CToggleButton::CToggleButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList callback, EShortcut key, bool playerColoredButton): CButton(position, defName, help, 0, key, playerColoredButton), CToggleBase(callback) { - allowDeselection = true; } void CToggleButton::doSelect(bool on) { if (on) { - setState(HIGHLIGHTED); + setState(EButtonState::HIGHLIGHTED); } else { - setState(NORMAL); + setState(EButtonState::NORMAL); } } void CToggleButton::setEnabled(bool enabled) { - setState(enabled ? NORMAL : BLOCKED); + setState(enabled ? EButtonState::NORMAL : EButtonState::BLOCKED); } void CToggleButton::clickPressed(const Point & cursorPosition) @@ -403,7 +471,7 @@ void CToggleButton::clickPressed(const Point & cursorPosition) { CCS->soundh->playSound(soundBase::button); GH.input().hapticFeedback(); - setState(PRESSED); + setState(EButtonState::PRESSED); } } @@ -416,13 +484,13 @@ void CToggleButton::clickReleased(const Point & cursorPosition) if(isBlocked()) return; - if (getState() == PRESSED && canActivate()) + if (getState() == EButtonState::PRESSED && canActivate()) { onButtonClicked(); - setSelected(!selected); + setSelected(!isSelected()); } else - doSelect(selected); // restore + doSelect(isSelected()); // restore } void CToggleButton::clickCancel(const Point & cursorPosition) @@ -434,10 +502,10 @@ void CToggleButton::clickCancel(const Point & cursorPosition) if(isBlocked()) return; - doSelect(selected); + doSelect(isSelected()); } -void CToggleGroup::addCallback(std::function callback) +void CToggleGroup::addCallback(const std::function & callback) { onChange += callback; } @@ -447,7 +515,7 @@ void CToggleGroup::resetCallback() onChange.clear(); } -void CToggleGroup::addToggle(int identifier, std::shared_ptr button) +void CToggleGroup::addToggle(int identifier, const std::shared_ptr & button) { if(auto intObj = std::dynamic_pointer_cast(button)) // hack-ish workagound to avoid diamond problem with inheritance { @@ -455,7 +523,7 @@ void CToggleGroup::addToggle(int identifier, std::shared_ptr button } button->addCallback([=] (bool on) { if (on) selectionChanged(identifier);}); - button->allowDeselection = false; + button->setAllowDeselection(false); if(buttons.count(identifier)>0) logAnim->error("Duplicated toggle button id %d", identifier); @@ -474,11 +542,8 @@ void CToggleGroup::setSelected(int id) void CToggleGroup::setSelectedOnly(int id) { - for(auto it = buttons.begin(); it != buttons.end(); it++) - { - int buttonId = it->first; - buttons[buttonId]->setEnabled(buttonId == id); - } + for(const auto & button : buttons) + button.second->setEnabled(button.first == id); selectionChanged(id); } diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index 9eea639fd..5a629c4c8 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -19,67 +19,86 @@ class Rect; VCMI_LIB_NAMESPACE_END class CAnimImage; -class CLabel; -class CAnimation; +class InterfaceObjectConfigurable; + +enum class EButtonState +{ + NORMAL=0, + PRESSED=1, + BLOCKED=2, + HIGHLIGHTED=3 // used for: highlighted state for selectable buttons, hovered state for hoverable buttons (e.g. main menu) +}; + +class ButtonBase : public CKeyShortcut +{ + std::shared_ptr image; //image for this button + std::shared_ptr configurable; //image for this button + std::shared_ptr overlay;//object-overlay, can be null + std::unique_ptr config; + + std::array stateToIndex; // mapping of button state to index of frame in animation + + EButtonState state;//current state of button from enum + + void update();//to refresh button after image or text change + + const JsonNode & getCurrentConfig() const; + +protected: + ButtonBase(Point position, const AnimationPath & defName, EShortcut key, bool playerColoredButton); + ~ButtonBase(); + + std::shared_ptr getOverlay(); + void setStateImpl(EButtonState state); + EButtonState getState() const; + +public: + /// Appearance modifiers + void setPlayerColor(PlayerColor player); + void setImage(const AnimationPath & defName, bool playerColoredButton = false); + void setConfigurable(const JsonPath & jsonName, bool playerColoredButton = false); + void setImageOrder(int state1, int state2, int state3, int state4); + + /// adds overlay on top of button image. Only one overlay can be active at once + void setOverlay(const std::shared_ptr& newOverlay); + void setTextOverlay(const std::string & Text, EFonts font, ColorRGBA color); +}; /// Typical Heroes 3 button which can be inactive or active and can /// hold further information if you right-click it -class CButton : public CKeyShortcut +class CButton : public ButtonBase { CFunctionList callback; -public: - enum ButtonState - { - NORMAL=0, - PRESSED=1, - BLOCKED=2, - HIGHLIGHTED=3 - }; -protected: - std::vector imageNames;//store list of images that can be used by this button - size_t currentImage; - - ButtonState state;//current state of button from enum - - std::array stateToIndex; // mapping of button state to index of frame in animation std::array hoverTexts; //texts for statusbar, if empty - first entry will be used std::optional borderColor; // mapping of button state to border color std::string helpBox; //for right-click help - std::shared_ptr image; //image for this button - std::shared_ptr overlay;//object-overlay, can be null - bool animateLonelyFrame = false; + bool actOnDown; //runs when mouse is pressed down over it, not when up + bool hoverable; //if true, button will be highlighted when hovered (e.g. main menu) + bool soundDisabled; + protected: void onButtonClicked(); // calls callback - void update();//to refresh button after image or text change // internal method to change state. Public change can be done only via block() - void setState(ButtonState newState); - ButtonState getState(); + void setState(EButtonState newState); public: - bool actOnDown,//runs when mouse is pressed down over it, not when up - hoverable,//if true, button will be highlighted when hovered (e.g. main menu) - soundDisabled; - // sets the same border color for all button states. void setBorderColor(std::optional borderColor); /// adds one more callback to on-click actions void addCallback(const std::function & callback); - /// adds overlay on top of button image. Only one overlay can be active at once - void addOverlay(std::shared_ptr newOverlay); - void addTextOverlay(const std::string & Text, EFonts font, ColorRGBA color); + void addHoverText(EButtonState state, const std::string & text); - void addImage(const AnimationPath & filename); - void addHoverText(ButtonState state, std::string text); - - void setImageOrder(int state1, int state2, int state3, int state4); - void setAnimateLonelyFrame(bool agreement); void block(bool on); + void setHoverable(bool on); + void setSoundDisabled(bool on); + void setActOnDown(bool on); + /// State modifiers bool isBlocked(); bool isHighlighted(); @@ -88,11 +107,6 @@ public: CButton(Point position, const AnimationPath & defName, const std::pair & help, CFunctionList Callback = 0, EShortcut key = {}, bool playerColoredButton = false ); - /// Appearance modifiers - void setIndex(size_t index); - void setImage(std::shared_ptr anim, int animFlags=0); - void setPlayerColor(PlayerColor player); - /// CIntObject overrides void showPopupWindow(const Point & cursorPosition) override; void clickPressed(const Point & cursorPosition) override; @@ -110,20 +124,20 @@ public: class CToggleBase { CFunctionList callback; -protected: bool selected; + /// if set to false - button can not be deselected normally + bool allowDeselection; + +protected: // internal method for overrides virtual void doSelect(bool on); // returns true if toggle can change its state - bool canActivate(); + bool canActivate() const; public: - /// if set to false - button can not be deselected normally - bool allowDeselection; - CToggleBase(CFunctionList callback); virtual ~CToggleBase(); @@ -133,7 +147,11 @@ public: /// Changes selection to "on" without calling callback void setSelectedSilent(bool on); - void addCallback(std::function callback); + bool isSelected() const; + + void setAllowDeselection(bool on); + + void addCallback(const std::function & callback); /// Set whether the toggle is currently enabled for user to use, this is only inplemented in ToggleButton, not for other toggles yet. virtual void setEnabled(bool enabled); @@ -169,11 +187,11 @@ public: CToggleGroup(const CFunctionList & OnChange); - void addCallback(std::function callback); + void addCallback(const std::function & callback); void resetCallback(); /// add one toggle/button into group - void addToggle(int index, std::shared_ptr button); + void addToggle(int index, const std::shared_ptr & button); /// Changes selection to specific value. Will select toggle with this ID, if present void setSelected(int id); /// in some cases, e.g. LoadGame difficulty selection, after refreshing the UI, the ToggleGroup should diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index a3b72d320..14a90796d 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -49,7 +49,7 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) if(settings["general"]["enableUiEnhancements"].Bool()) { imageIndex = spellID.num; - if(baseType != CComponent::spell) + if(component.type != ComponentType::SPELL_SCROLL) { image->setScale(Point(pos.w, 34)); image->setAnimationPath(AnimationPath::builtin("spellscr"), imageIndex); @@ -57,31 +57,39 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) } } // Add spell component info (used to provide a pic in r-click popup) - baseType = CComponent::spell; - type = spellID; + component.type = ComponentType::SPELL_SCROLL; + component.subType = spellID; } else { - if(settings["general"]["enableUiEnhancements"].Bool() && baseType != CComponent::artifact) + if(settings["general"]["enableUiEnhancements"].Bool() && component.type != ComponentType::ARTIFACT) { image->setScale(Point()); image->setAnimationPath(AnimationPath::builtin("artifact"), imageIndex); image->moveTo(Point(pos.x, pos.y)); } - baseType = CComponent::artifact; - type = artInst->getTypeId(); + component.type = ComponentType::ARTIFACT; + component.subType = artInst->getTypeId(); } - bonusValue = 0; image->enable(); text = artInst->getDescription(); } -CArtPlace::CArtPlace(Point position, const CArtifactInstance * Art) - : ourArt(Art) +CArtPlace::CArtPlace(Point position, const CArtifactInstance * art) + : SelectableSlot(Rect(position, Point(44, 44)), Point(1, 1)) + , ourArt(art) + , locked(false) { - image = nullptr; - pos += position; - pos.w = pos.h = 44; + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + imageIndex = 0; + if(locked) + imageIndex = ArtifactID::ART_LOCK; + else if(ourArt) + imageIndex = ourArt->artType->getIconIndex(); + + image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); + image->disable(); } const CArtifactInstance * CArtPlace::getArt() @@ -89,26 +97,12 @@ const CArtifactInstance * CArtPlace::getArt() return ourArt; } -CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art) - : CArtPlace(position, Art), +CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art) + : CArtPlace(position, art), commanderOwner(commanderOwner), commanderSlotID(artSlot.num) { - createImage(); - setArtifact(Art); -} - -void CCommanderArtPlace::createImage() -{ - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - - imageIndex = 0; - if(ourArt) - imageIndex = ourArt->artType->getIconIndex(); - - image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); - if(!ourArt) - image->disable(); + setArtifact(art); } void CCommanderArtPlace::returnArtToHeroCallback() @@ -121,10 +115,11 @@ void CCommanderArtPlace::returnArtToHeroCallback() } else { - ArtifactLocation src(commanderOwner->commander.get(), artifactPos); - ArtifactLocation dst(commanderOwner, freeSlot); + ArtifactLocation src(commanderOwner->id, artifactPos); + src.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; + ArtifactLocation dst(commanderOwner->id, freeSlot); - if(ourArt->canBePutAt(dst, true)) + if(ourArt->canBePutAt(commanderOwner, freeSlot, true)) { LOCPLINT->cb->swapArtifacts(src, dst); setArtifact(nullptr); @@ -139,26 +134,18 @@ void CCommanderArtPlace::clickPressed(const Point & cursorPosition) LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this]() { returnArtToHeroCallback(); }, []() {}); } -void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition) +void CCommanderArtPlace::showPopupWindow(const Point& cursorPosition) { if(ourArt && text.size()) CArtPlace::showPopupWindow(cursorPosition); } -void CCommanderArtPlace::setArtifact(const CArtifactInstance * art) +CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * art) + : CArtPlace(position, art) { - setInternals(art); } -CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * Art) - : CArtPlace(position, Art), - locked(false), - marked(false) -{ - createImage(); -} - -void CHeroArtPlace::lockSlot(bool on) +void CArtPlace::lockSlot(bool on) { if(locked == on) return; @@ -173,52 +160,33 @@ void CHeroArtPlace::lockSlot(bool on) image->setFrame(0); } -bool CHeroArtPlace::isLocked() +bool CArtPlace::isLocked() const { return locked; } -void CHeroArtPlace::selectSlot(bool on) +void CArtPlace::clickPressed(const Point & cursorPosition) { - if(marked == on) - return; - - marked = on; - if(on) - selection->enable(); - else - selection->disable(); + if(clickPressedCallback) + clickPressedCallback(*this, cursorPosition); } -bool CHeroArtPlace::isMarked() const -{ - return marked; -} - -void CHeroArtPlace::clickPressed(const Point & cursorPosition) -{ - if(leftClickCallback) - leftClickCallback(*this); -} - -void CHeroArtPlace::showPopupWindow(const Point & cursorPosition) +void CArtPlace::showPopupWindow(const Point & cursorPosition) { if(showPopupCallback) - showPopupCallback(*this); + showPopupCallback(*this, cursorPosition); } -void CHeroArtPlace::showAll(Canvas & to) +void CArtPlace::gesture(bool on, const Point & initialPosition, const Point & finalPosition) { - if(ourArt) - { - CIntObject::showAll(to); - } + if(!on) + return; - if(marked && isActive()) - to.drawBorder(pos, Colors::BRIGHT_YELLOW); + if(gestureCallback) + gestureCallback(*this, initialPosition); } -void CHeroArtPlace::setArtifact(const CArtifactInstance * art) +void CArtPlace::setArtifact(const CArtifactInstance * art) { setInternals(art); if(art) @@ -236,6 +204,21 @@ void CHeroArtPlace::setArtifact(const CArtifactInstance * art) } } +void CArtPlace::setClickPressedCallback(ClickFunctor callback) +{ + clickPressedCallback = callback; +} + +void CArtPlace::setShowPopupCallback(ClickFunctor callback) +{ + showPopupCallback = callback; +} + +void CArtPlace::setGestureCallback(ClickFunctor callback) +{ + gestureCallback = callback; +} + void CHeroArtPlace::addCombinedArtInfo(std::map & arts) { for(const auto & combinedArt : arts) @@ -253,24 +236,6 @@ void CHeroArtPlace::addCombinedArtInfo(std::map & arts) } } -void CHeroArtPlace::createImage() -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - si32 imageIndex = 0; - - if(locked) - imageIndex = ArtifactID::ART_LOCK; - else if(ourArt) - imageIndex = ourArt->artType->getIconIndex(); - - image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); - image->disable(); - - selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION); - selection->disable(); -} - bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot) { assert(hero); diff --git a/client/widgets/CArtifactHolder.h b/client/widgets/CArtifactHolder.h index 5f8648a3f..c39cbdca7 100644 --- a/client/widgets/CArtifactHolder.h +++ b/client/widgets/CArtifactHolder.h @@ -19,7 +19,6 @@ class CArtifactSet; VCMI_LIB_NAMESPACE_END class CAnimImage; -class CButton; class CArtifactHolder { @@ -30,21 +29,35 @@ public: virtual void artifactAssembled(const ArtifactLocation & artLoc)=0; }; -class CArtPlace : public LRClickableAreaWTextComp +class CArtPlace : public SelectableSlot { +public: + using ClickFunctor = std::function; + + ArtifactPosition slot; + + CArtPlace(Point position, const CArtifactInstance * art = nullptr); + const CArtifactInstance * getArt(); + void lockSlot(bool on); + bool isLocked() const; + void setArtifact(const CArtifactInstance * art); + void setClickPressedCallback(ClickFunctor callback); + void setShowPopupCallback(ClickFunctor callback); + void setGestureCallback(ClickFunctor callback); + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + protected: std::shared_ptr image; const CArtifactInstance * ourArt; int imageIndex; + bool locked; + ClickFunctor clickPressedCallback; + ClickFunctor showPopupCallback; + ClickFunctor gestureCallback; void setInternals(const CArtifactInstance * artInst); - virtual void createImage()=0; - -public: - CArtPlace(Point position, const CArtifactInstance * Art = nullptr); - const CArtifactInstance * getArt(); - - virtual void setArtifact(const CArtifactInstance * art)=0; }; class CCommanderArtPlace : public CArtPlace @@ -53,42 +66,19 @@ protected: const CGHeroInstance * commanderOwner; ArtifactPosition commanderSlotID; - void createImage() override; void returnArtToHeroCallback(); public: - CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art = nullptr); + CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art = nullptr); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - void setArtifact(const CArtifactInstance * art) override; }; class CHeroArtPlace: public CArtPlace { public: - using ClickFunctor = std::function; - - ArtifactPosition slot; - ClickFunctor leftClickCallback; - ClickFunctor showPopupCallback; - - CHeroArtPlace(Point position, const CArtifactInstance * Art = nullptr); - void lockSlot(bool on); - bool isLocked(); - void selectSlot(bool on); - bool isMarked() const; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void showAll(Canvas & to) override; - void setArtifact(const CArtifactInstance * art) override; + CHeroArtPlace(Point position, const CArtifactInstance * art = nullptr); void addCombinedArtInfo(std::map & arts); - -protected: - std::shared_ptr selection; - bool locked; - bool marked; - - void createImage() override; }; namespace ArtifactUtilsClient diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 737b05f9c..8011d8bf1 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "CArtifactsOfHeroAltar.h" +#include "Buttons.h" #include "../CPlayerInterface.h" #include "../../CCallback.h" @@ -19,95 +20,21 @@ #include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) - : visibleArtSet(ArtBearer::ArtBearer::HERO) { init( - std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1), - std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1), + std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2), + std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2), position, std::bind(&CArtifactsOfHeroAltar::scrollBackpack, this, _1)); - pickedArtFromSlot = ArtifactPosition::PRE_FIRST; + + // The backpack is in the altar window above and to the right + for(auto & slot : backpack) + slot->moveBy(Point(2, -1)); + leftBackpackRoll->moveBy(Point(2, -1)); + rightBackpackRoll->moveBy(Point(2, -1)); }; CArtifactsOfHeroAltar::~CArtifactsOfHeroAltar() { putBackPickedArtifact(); } - -void CArtifactsOfHeroAltar::setHero(const CGHeroInstance * hero) -{ - if(hero) - { - visibleArtSet.artifactsWorn = hero->artifactsWorn; - visibleArtSet.artifactsInBackpack = hero->artifactsInBackpack; - CArtifactsOfHeroBase::setHero(hero); - } -} - -void CArtifactsOfHeroAltar::updateWornSlots() -{ - for(auto place : artWorn) - setSlotData(getArtPlace(place.first), place.first, visibleArtSet); -} - -void CArtifactsOfHeroAltar::updateBackpackSlots() -{ - for(auto artPlace : backpack) - setSlotData(getArtPlace(artPlace->slot), artPlace->slot, visibleArtSet); -} - -void CArtifactsOfHeroAltar::scrollBackpack(int offset) -{ - CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, visibleArtSet); - redraw(); -} - -void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace) -{ - if(const auto art = artPlace.getArt()) - { - pickedArtFromSlot = artPlace.slot; - artPlace.setArtifact(nullptr); - deleteFromVisible(art); - if(ArtifactUtils::isSlotBackpack(pickedArtFromSlot)) - pickedArtFromSlot = curHero->getSlotByInstance(art); - assert(pickedArtFromSlot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, pickedArtFromSlot), ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); - } -} - -void CArtifactsOfHeroAltar::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc) -{ - LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); - const auto pickedArtInst = curHero->getArt(ArtifactPosition::TRANSITION_POS); - assert(pickedArtInst); - visibleArtSet.putArtifact(dstLoc.slot, const_cast(pickedArtInst)); -} - -void CArtifactsOfHeroAltar::pickedArtMoveToAltar(const ArtifactPosition & slot) -{ - if(ArtifactUtils::isSlotBackpack(slot) || ArtifactUtils::isSlotEquipment(slot) || slot == ArtifactPosition::TRANSITION_POS) - { - assert(curHero->getSlot(slot)->getArt()); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, slot), ArtifactLocation(curHero, pickedArtFromSlot)); - pickedArtFromSlot = ArtifactPosition::PRE_FIRST; - } -} - -void CArtifactsOfHeroAltar::deleteFromVisible(const CArtifactInstance * artInst) -{ - const auto slot = visibleArtSet.getSlotByInstance(artInst); - visibleArtSet.removeArtifact(slot); - if(ArtifactUtils::isSlotBackpack(slot)) - { - scrollBackpackForArtSet(0, visibleArtSet); - } - else - { - for(const auto & part : artInst->getPartsInfo()) - { - if(part.slot != ArtifactPosition::PRE_FIRST) - getArtPlace(part.slot)->setArtifact(nullptr); - } - } -} diff --git a/client/widgets/CArtifactsOfHeroAltar.h b/client/widgets/CArtifactsOfHeroAltar.h index 83fd2b6f4..244b09f9f 100644 --- a/client/widgets/CArtifactsOfHeroAltar.h +++ b/client/widgets/CArtifactsOfHeroAltar.h @@ -16,18 +16,6 @@ class CArtifactsOfHeroAltar : public CArtifactsOfHeroBase { public: - std::set artifactsOnAltar; - ArtifactPosition pickedArtFromSlot; - CArtifactFittingSet visibleArtSet; - CArtifactsOfHeroAltar(const Point & position); ~CArtifactsOfHeroAltar(); - void setHero(const CGHeroInstance * hero) override; - void updateWornSlots() override; - void updateBackpackSlots() override; - void scrollBackpack(int offset) override; - void pickUpArtifact(CHeroArtPlace & artPlace); - void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc); - void pickedArtMoveToAltar(const ArtifactPosition & slot); - void deleteFromVisible(const CArtifactInstance * artInst); }; diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index f95d5cf65..ddc75c544 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -17,71 +17,28 @@ #include "ObjectLists.h" #include "../CPlayerInterface.h" +#include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/networkPacks/ArtifactLocation.h" #include "../../CCallback.h" -CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position) +CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(size_t slotsColumnsMax, size_t slotsRowsMax) + : slotsColumnsMax(slotsColumnsMax) + , slotsRowsMax(slotsRowsMax) { - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - pos += position; setRedrawParent(true); +} +CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack() + : CArtifactsOfHeroBackpack(8, 8) +{ const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP); - auto visibleCapacityMax = HERO_BACKPACK_WINDOW_SLOT_ROWS * HERO_BACKPACK_WINDOW_SLOT_COLUMNS; + auto visibleCapacityMax = slotsRowsMax * slotsColumnsMax; if(backpackCap >= 0) visibleCapacityMax = visibleCapacityMax > backpackCap ? backpackCap : visibleCapacityMax; - backpack.resize(visibleCapacityMax); - size_t artPlaceIdx = 0; - for(auto & artPlace : backpack) - { - const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % HERO_BACKPACK_WINDOW_SLOT_COLUMNS), - slotSizeWithMargin * (artPlaceIdx / HERO_BACKPACK_WINDOW_SLOT_COLUMNS)); - backpackSlotsBackgrounds.emplace_back(std::make_shared(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos)); - artPlace = std::make_shared(pos); - artPlace->setArtifact(nullptr); - artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); - artPlaceIdx++; - } - - if(backpackCap < 0 || visibleCapacityMax < backpackCap) - { - auto onCreate = [](size_t index) -> std::shared_ptr - { - return std::make_shared(); - }; - CListBoxWithCallback::MovedPosCallback posMoved = [this](size_t pos) -> void - { - scrollBackpack(static_cast(pos) * HERO_BACKPACK_WINDOW_SLOT_COLUMNS - backpackPos); - }; - backpackListBox = std::make_shared( - posMoved, onCreate, Point(0, 0), Point(0, 0), HERO_BACKPACK_WINDOW_SLOT_ROWS, 0, 0, 1, - Rect(HERO_BACKPACK_WINDOW_SLOT_COLUMNS * slotSizeWithMargin + sliderPosOffsetX, 0, HERO_BACKPACK_WINDOW_SLOT_ROWS * slotSizeWithMargin - 2, 0)); - } - - pos.w = visibleCapacityMax > HERO_BACKPACK_WINDOW_SLOT_COLUMNS ? HERO_BACKPACK_WINDOW_SLOT_COLUMNS : visibleCapacityMax; - pos.w *= slotSizeWithMargin; - if(backpackListBox) - pos.w += sliderPosOffsetX + 16; // 16 is slider width. TODO: get it from CListBox directly; - - pos.h = (visibleCapacityMax / HERO_BACKPACK_WINDOW_SLOT_COLUMNS); - if(visibleCapacityMax % HERO_BACKPACK_WINDOW_SLOT_COLUMNS != 0) - pos.h += 1; - pos.h *= slotSizeWithMargin; -} - -void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc) -{ - LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); -} - -void CArtifactsOfHeroBackpack::pickUpArtifact(CHeroArtPlace & artPlace) -{ - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + initAOHbackpack(visibleCapacityMax, backpackCap < 0 || visibleCapacityMax < backpackCap); } void CArtifactsOfHeroBackpack::scrollBackpack(int offset) @@ -92,7 +49,7 @@ void CArtifactsOfHeroBackpack::scrollBackpack(int offset) auto slot = ArtifactPosition::BACKPACK_START + backpackPos; for(auto artPlace : backpack) { - setSlotData(artPlace, slot, *curHero); + setSlotData(artPlace, slot); slot = slot + 1; } redraw(); @@ -107,5 +64,145 @@ void CArtifactsOfHeroBackpack::updateBackpackSlots() size_t CArtifactsOfHeroBackpack::getActiveSlotRowsNum() { - return (curHero->artifactsInBackpack.size() + HERO_BACKPACK_WINDOW_SLOT_COLUMNS - 1) / HERO_BACKPACK_WINDOW_SLOT_COLUMNS; + return (curHero->artifactsInBackpack.size() + slotsColumnsMax - 1) / slotsColumnsMax; } + +size_t CArtifactsOfHeroBackpack::getSlotsNum() +{ + return backpack.size(); +} + +void CArtifactsOfHeroBackpack::initAOHbackpack(size_t slots, bool slider) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + backpack.resize(slots); + size_t artPlaceIdx = 0; + for(auto & artPlace : backpack) + { + const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % slotsColumnsMax), + slotSizeWithMargin * (artPlaceIdx / slotsColumnsMax)); + backpackSlotsBackgrounds.emplace_back(std::make_shared(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos)); + artPlace = std::make_shared(pos); + artPlace->setArtifact(nullptr); + artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); + artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); + artPlaceIdx++; + } + + if(slider) + { + auto onCreate = [](size_t index) -> std::shared_ptr + { + return std::make_shared(); + }; + CListBoxWithCallback::MovedPosCallback posMoved = [this](size_t pos) -> void + { + scrollBackpack(static_cast(pos) * slotsColumnsMax - backpackPos); + }; + backpackListBox = std::make_shared( + posMoved, onCreate, Point(0, 0), Point(0, 0), slotsRowsMax, 0, 0, 1, + Rect(slotsColumnsMax * slotSizeWithMargin + sliderPosOffsetX, 0, slotsRowsMax * slotSizeWithMargin - 2, 0)); + } + + pos.w = slots > slotsColumnsMax ? slotsColumnsMax : slots; + pos.w *= slotSizeWithMargin; + if(slider) + pos.w += sliderPosOffsetX + 16; // 16 is slider width. TODO: get it from CListBox directly; + pos.h = calcRows(slots) * slotSizeWithMargin; +} + +size_t CArtifactsOfHeroBackpack::calcRows(size_t slots) +{ + size_t rows = 0; + if(slotsColumnsMax != 0) + { + rows = slots / slotsColumnsMax; + if(slots % slotsColumnsMax != 0) + rows += 1; + } + return rows; +} + +CArtifactsOfHeroQuickBackpack::CArtifactsOfHeroQuickBackpack(const ArtifactPosition filterBySlot) + : CArtifactsOfHeroBackpack(0, 0) +{ + assert(filterBySlot != ArtifactPosition::FIRST_AVAILABLE); + + if(!ArtifactUtils::isSlotEquipment(filterBySlot)) + return; + + this->filterBySlot = filterBySlot; +} + +void CArtifactsOfHeroQuickBackpack::setHero(const CGHeroInstance * hero) +{ + if(curHero == hero) + return; + + curHero = hero; + if(curHero) + { + ArtifactID artInSlotId = ArtifactID::NONE; + SpellID scrollInSlotSpellId = SpellID::NONE; + if(auto artInSlot = curHero->getArt(filterBySlot)) + { + artInSlotId = artInSlot->getTypeId(); + scrollInSlotSpellId = artInSlot->getScrollSpellID(); + } + + std::map filteredArts; + for(auto & slotInfo : curHero->artifactsInBackpack) + if(slotInfo.artifact->getTypeId() != artInSlotId && !slotInfo.artifact->isScroll() && + slotInfo.artifact->artType->canBePutAt(curHero, filterBySlot, true)) + { + filteredArts.insert(std::pair(slotInfo.artifact->getTypeId(), slotInfo.artifact)); + } + + std::map filteredScrolls; + if(filterBySlot == ArtifactPosition::MISC1 || filterBySlot == ArtifactPosition::MISC2 || filterBySlot == ArtifactPosition::MISC3 || + filterBySlot == ArtifactPosition::MISC4 || filterBySlot == ArtifactPosition::MISC5) + { + for(auto & slotInfo : curHero->artifactsInBackpack) + { + if(slotInfo.artifact->isScroll() && slotInfo.artifact->getScrollSpellID() != scrollInSlotSpellId) + filteredScrolls.insert(std::pair(slotInfo.artifact->getScrollSpellID(), slotInfo.artifact)); + } + } + + backpack.clear(); + auto requiredSlots = filteredArts.size() + filteredScrolls.size(); + slotsColumnsMax = ceilf(sqrtf(requiredSlots)); + slotsRowsMax = calcRows(requiredSlots); + initAOHbackpack(requiredSlots, false); + auto artPlace = backpack.begin(); + for(auto & art : filteredArts) + setSlotData(*artPlace++, curHero->getSlotByInstance(art.second)); + for(auto & art : filteredScrolls) + setSlotData(*artPlace++, curHero->getSlotByInstance(art.second)); + } +} + +ArtifactPosition CArtifactsOfHeroQuickBackpack::getFilterSlot() +{ + return filterBySlot; +} + +void CArtifactsOfHeroQuickBackpack::selectSlotAt(const Point & position) +{ + for(auto & artPlace : backpack) + artPlace->selectSlot(artPlace->pos.isInside(position)); +} + +void CArtifactsOfHeroQuickBackpack::swapSelected() +{ + ArtifactLocation backpackLoc(curHero->id, ArtifactPosition::PRE_FIRST); + for(auto & artPlace : backpack) + if(artPlace->isSelected()) + { + backpackLoc.slot = artPlace->slot; + break; + } + if(backpackLoc.slot != ArtifactPosition::PRE_FIRST && filterBySlot != ArtifactPosition::PRE_FIRST && curHero) + LOCPLINT->cb->swapArtifacts(backpackLoc, ArtifactLocation(curHero->id, filterBySlot)); +} \ No newline at end of file diff --git a/client/widgets/CArtifactsOfHeroBackpack.h b/client/widgets/CArtifactsOfHeroBackpack.h index ce51531ef..d79776109 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.h +++ b/client/widgets/CArtifactsOfHeroBackpack.h @@ -22,18 +22,34 @@ class CListBoxWithCallback; class CArtifactsOfHeroBackpack : public CArtifactsOfHeroBase { public: - CArtifactsOfHeroBackpack(const Point & position); - void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc); - void pickUpArtifact(CHeroArtPlace & artPlace); + CArtifactsOfHeroBackpack(size_t slotsColumnsMax, size_t slotsRowsMax); + CArtifactsOfHeroBackpack(); void scrollBackpack(int offset) override; void updateBackpackSlots() override; size_t getActiveSlotRowsNum(); + size_t getSlotsNum(); -private: +protected: std::shared_ptr backpackListBox; std::vector> backpackSlotsBackgrounds; - const size_t HERO_BACKPACK_WINDOW_SLOT_COLUMNS = 8; - const size_t HERO_BACKPACK_WINDOW_SLOT_ROWS = 8; + size_t slotsColumnsMax; + size_t slotsRowsMax; const int slotSizeWithMargin = 46; - const int sliderPosOffsetX = 10; + const int sliderPosOffsetX = 5; + + void initAOHbackpack(size_t slots, bool slider); + size_t calcRows(size_t slots); +}; + +class CArtifactsOfHeroQuickBackpack : public CArtifactsOfHeroBackpack +{ +public: + CArtifactsOfHeroQuickBackpack(const ArtifactPosition filterBySlot); + void setHero(const CGHeroInstance * hero); + ArtifactPosition getFilterSlot(); + void selectSlotAt(const Point & position); + void swapSelected(); + +private: + ArtifactPosition filterBySlot; }; diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index 769da2258..924830ce4 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -39,11 +39,11 @@ void CArtifactsOfHeroBase::putBackPickedArtifact() auto slot = ArtifactUtils::getArtAnyPosition(curHero, curHero->artifactsTransitionPos.begin()->artifact->getTypeId()); if(slot == ArtifactPosition::PRE_FIRST) { - LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } else { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero, slot)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero->id, slot)); } } if(putBackPickedArtCallback) @@ -56,8 +56,8 @@ void CArtifactsOfHeroBase::setPutBackPickedArtifactCallback(PutBackPickedArtCall } void CArtifactsOfHeroBase::init( - CHeroArtPlace::ClickFunctor lClickCallback, - CHeroArtPlace::ClickFunctor showPopupCallback, + CArtPlace::ClickFunctor lClickCallback, + CArtPlace::ClickFunctor showPopupCallback, const Point & position, BpackScrollFunctor scrollCallback) { @@ -78,14 +78,14 @@ void CArtifactsOfHeroBase::init( { artPlace.second->slot = artPlace.first; artPlace.second->setArtifact(nullptr); - artPlace.second->leftClickCallback = lClickCallback; - artPlace.second->showPopupCallback = showPopupCallback; + artPlace.second->setClickPressedCallback(lClickCallback); + artPlace.second->setShowPopupCallback(showPopupCallback); } for(auto artPlace : backpack) { artPlace->setArtifact(nullptr); - artPlace->leftClickCallback = lClickCallback; - artPlace->showPopupCallback = showPopupCallback; + artPlace->setClickPressedCallback(lClickCallback); + artPlace->setShowPopupCallback(showPopupCallback); } leftBackpackRoll = std::make_shared(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(-1);}, EShortcut::MOVE_LEFT); rightBackpackRoll = std::make_shared(Point(632, 364), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(+1);}, EShortcut::MOVE_RIGHT); @@ -95,16 +95,22 @@ void CArtifactsOfHeroBase::init( setRedrawParent(true); } -void CArtifactsOfHeroBase::leftClickArtPlace(CHeroArtPlace & artPlace) +void CArtifactsOfHeroBase::clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition) { - if(leftClickCallback) - leftClickCallback(*this, artPlace); + if(clickPressedCallback) + clickPressedCallback(*this, artPlace, cursorPosition); } -void CArtifactsOfHeroBase::rightClickArtPlace(CHeroArtPlace & artPlace) +void CArtifactsOfHeroBase::showPopupArtPlace(CArtPlace & artPlace, const Point & cursorPosition) { if(showPopupCallback) - showPopupCallback(*this, artPlace); + showPopupCallback(*this, artPlace, cursorPosition); +} + +void CArtifactsOfHeroBase::gestureArtPlace(CArtPlace & artPlace, const Point & cursorPosition) +{ + if(gestureCallback) + gestureCallback(*this, artPlace, cursorPosition); } void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero) @@ -117,7 +123,7 @@ void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero) for(auto slot : artWorn) { - setSlotData(slot.second, slot.first, *curHero); + setSlotData(slot.second, slot.first); } scrollBackpack(0); } @@ -128,16 +134,10 @@ const CGHeroInstance * CArtifactsOfHeroBase::getHero() const } void CArtifactsOfHeroBase::scrollBackpack(int offset) -{ - scrollBackpackForArtSet(offset, *curHero); - redraw(); -} - -void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSet & artSet) { // offset==-1 => to left; offset==1 => to right using slotInc = std::function; - auto artsInBackpack = static_cast(artSet.artifactsInBackpack.size()); + auto artsInBackpack = static_cast(curHero->artifactsInBackpack.size()); auto scrollingPossible = artsInBackpack > backpack.size(); slotInc inc_straight = [](ArtifactPosition & slot) -> ArtifactPosition @@ -164,7 +164,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe auto slot = ArtifactPosition(ArtifactPosition::BACKPACK_START + backpackPos); for(auto artPlace : backpack) { - setSlotData(artPlace, slot, artSet); + setSlotData(artPlace, slot); slot = inc(slot); } @@ -173,12 +173,14 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe leftBackpackRoll->block(!scrollingPossible); if(rightBackpackRoll) rightBackpackRoll->block(!scrollingPossible); + + redraw(); } void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved) { for(auto artPlace : artWorn) - artPlace.second->selectSlot(art->artType->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved)); + artPlace.second->selectSlot(art->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved)); } void CArtifactsOfHeroBase::unmarkSlots() @@ -229,7 +231,7 @@ void CArtifactsOfHeroBase::updateBackpackSlots() void CArtifactsOfHeroBase::updateSlot(const ArtifactPosition & slot) { - setSlotData(getArtPlace(slot), slot, *curHero); + setSlotData(getArtPlace(slot), slot); } const CArtifactInstance * CArtifactsOfHeroBase::getPickedArtifact() @@ -241,7 +243,16 @@ const CArtifactInstance * CArtifactsOfHeroBase::getPickedArtifact() return curHero->getArt(ArtifactPosition::TRANSITION_POS); } -void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet) +void CArtifactsOfHeroBase::addGestureCallback(CArtPlace::ClickFunctor callback) +{ + for(auto & artPlace : artWorn) + { + artPlace.second->setGestureCallback(callback); + artPlace.second->addUsedEvents(GESTURE); + } +} + +void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot) { // Spurious call from artifactMoved in attempt to update hidden backpack slot if(!artPlace && ArtifactUtils::isSlotBackpack(slot)) @@ -250,7 +261,7 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit } artPlace->slot = slot; - if(auto slotInfo = artSet.getSlot(slot)) + if(auto slotInfo = curHero->getSlot(slot)) { artPlace->lockSlot(slotInfo->locked); artPlace->setArtifact(slotInfo->artifact); @@ -263,7 +274,7 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit arts.insert(std::pair(combinedArt, 0)); for(const auto part : combinedArt->getConstituents()) { - if(artSet.hasArt(part->getId(), false)) + if(curHero->hasArt(part->getId(), false)) arts.at(combinedArt)++; } } diff --git a/client/widgets/CArtifactsOfHeroBase.h b/client/widgets/CArtifactsOfHeroBase.h index aeacc10fa..83dbc8389 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -11,6 +11,8 @@ #include "CArtifactHolder.h" +class CButton; + class CArtifactsOfHeroBase : public CIntObject { protected: @@ -19,17 +21,19 @@ protected: public: using ArtPlaceMap = std::map; - using ClickFunctor = std::function; + using ClickFunctor = std::function; using PutBackPickedArtCallback = std::function; - ClickFunctor leftClickCallback; + ClickFunctor clickPressedCallback; ClickFunctor showPopupCallback; + ClickFunctor gestureCallback; CArtifactsOfHeroBase(); virtual void putBackPickedArtifact(); virtual void setPutBackPickedArtifactCallback(PutBackPickedArtCallback callback); - virtual void leftClickArtPlace(CHeroArtPlace & artPlace); - virtual void rightClickArtPlace(CHeroArtPlace & artPlace); + virtual void clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition); + virtual void showPopupArtPlace(CArtPlace & artPlace, const Point & cursorPosition); + virtual void gestureArtPlace(CArtPlace & artPlace, const Point & cursorPosition); virtual void setHero(const CGHeroInstance * hero); virtual const CGHeroInstance * getHero() const; virtual void scrollBackpack(int offset); @@ -40,6 +44,7 @@ public: virtual void updateBackpackSlots(); virtual void updateSlot(const ArtifactPosition & slot); virtual const CArtifactInstance * getPickedArtifact(); + void addGestureCallback(CArtPlace::ClickFunctor callback); protected: const CGHeroInstance * curHero; @@ -52,18 +57,17 @@ protected: const std::vector slotPos = { - Point(509,30), Point(567,240), Point(509,80), //0-2 - Point(383,68), Point(564,183), Point(509,130), //3-5 - Point(431,68), Point(610,183), Point(515,295), //6-8 - Point(383,143), Point(399,194), Point(415,245), //9-11 - Point(431,296), Point(564,30), Point(610,30), //12-14 + Point(509,30), Point(568,242), Point(509,80), //0-2 + Point(383,69), Point(562,184), Point(509,131), //3-5 + Point(431,69), Point(610,184), Point(515,295), //6-8 + Point(383,143), Point(399,193), Point(415,244), //9-11 + Point(431,295), Point(564,30), Point(610,30), //12-14 Point(610,76), Point(610,122), Point(610,310), //15-17 - Point(381,296) //18 + Point(381,295) //18 }; virtual void init(CHeroArtPlace::ClickFunctor lClickCallback, CHeroArtPlace::ClickFunctor showPopupCallback, const Point & position, BpackScrollFunctor scrollCallback); // Assigns an artifacts to an artifact place depending on it's new slot ID - virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet); - virtual void scrollBackpackForArtSet(int offset, const CArtifactSet & artSet); + virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot); }; diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index 67e702b5f..20a9cf855 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -13,6 +13,7 @@ #include "Buttons.h" #include "../CPlayerInterface.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" #include "../../lib/networkPacks/ArtifactLocation.h" @@ -29,14 +30,15 @@ CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vecto { artPlace.second->slot = artPlace.first; artPlace.second->setArtifact(nullptr); - artPlace.second->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace.second->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace.second->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); + artPlace.second->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); } + addGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, this, _1, _2)); for(auto artPlace : backpack) { artPlace->setArtifact(nullptr); - artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2)); + artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2)); } leftBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, -1)); rightBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, +1)); @@ -48,15 +50,3 @@ CArtifactsOfHeroKingdom::~CArtifactsOfHeroKingdom() { putBackPickedArtifact(); } - -void CArtifactsOfHeroKingdom::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc) -{ - LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); -} - -void CArtifactsOfHeroKingdom::pickUpArtifact(CHeroArtPlace & artPlace) -{ - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); -} - diff --git a/client/widgets/CArtifactsOfHeroKingdom.h b/client/widgets/CArtifactsOfHeroKingdom.h index 84f6ad185..4218fa54f 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.h +++ b/client/widgets/CArtifactsOfHeroKingdom.h @@ -20,9 +20,8 @@ VCMI_LIB_NAMESPACE_END class CArtifactsOfHeroKingdom : public CArtifactsOfHeroBase { public: + CArtifactsOfHeroKingdom() = delete; CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector Backpack, std::shared_ptr leftScroll, std::shared_ptr rightScroll); ~CArtifactsOfHeroKingdom(); - void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc); - void pickUpArtifact(CHeroArtPlace & artPlace); -}; \ No newline at end of file +}; diff --git a/client/widgets/CArtifactsOfHeroMain.cpp b/client/widgets/CArtifactsOfHeroMain.cpp index 46b5329ca..fbeb7db8e 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -11,6 +11,7 @@ #include "CArtifactsOfHeroMain.h" #include "../CPlayerInterface.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" #include "../../lib/networkPacks/ArtifactLocation.h" @@ -18,24 +19,14 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position) { init( - std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1), - std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1), + std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2), + std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2), position, std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1)); + addGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, this, _1, _2)); } CArtifactsOfHeroMain::~CArtifactsOfHeroMain() { putBackPickedArtifact(); } - -void CArtifactsOfHeroMain::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc) -{ - LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); -} - -void CArtifactsOfHeroMain::pickUpArtifact(CHeroArtPlace & artPlace) -{ - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); -} diff --git a/client/widgets/CArtifactsOfHeroMain.h b/client/widgets/CArtifactsOfHeroMain.h index 6e7253bbe..6caf0d636 100644 --- a/client/widgets/CArtifactsOfHeroMain.h +++ b/client/widgets/CArtifactsOfHeroMain.h @@ -22,6 +22,4 @@ class CArtifactsOfHeroMain : public CArtifactsOfHeroBase public: CArtifactsOfHeroMain(const Point & position); ~CArtifactsOfHeroMain(); - void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc); - void pickUpArtifact(CHeroArtPlace & artPlace); }; diff --git a/client/widgets/CArtifactsOfHeroMarket.cpp b/client/widgets/CArtifactsOfHeroMarket.cpp index 1f01775b3..257c212b9 100644 --- a/client/widgets/CArtifactsOfHeroMarket.cpp +++ b/client/widgets/CArtifactsOfHeroMarket.cpp @@ -15,27 +15,31 @@ CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position) { init( - std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1), - std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1), + std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2), + std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2), position, std::bind(&CArtifactsOfHeroMarket::scrollBackpack, this, _1)); + + for(const auto & [slot, artPlace] : artWorn) + artPlace->setSelectionWidth(2); + for(auto artPlace : backpack) + artPlace->setSelectionWidth(2); }; void CArtifactsOfHeroMarket::scrollBackpack(int offset) { - CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, *curHero); + CArtifactsOfHeroBase::scrollBackpack(offset); // We may have highlight on one of backpack artifacts if(selectArtCallback) { - for(auto & artPlace : backpack) + for(const auto & artPlace : backpack) { - if(artPlace->isMarked()) + if(artPlace->isSelected()) { selectArtCallback(artPlace.get()); break; } } } - redraw(); } \ No newline at end of file diff --git a/client/widgets/CArtifactsOfHeroMarket.h b/client/widgets/CArtifactsOfHeroMarket.h index c88ff0494..c8d34c399 100644 --- a/client/widgets/CArtifactsOfHeroMarket.h +++ b/client/widgets/CArtifactsOfHeroMarket.h @@ -14,7 +14,7 @@ class CArtifactsOfHeroMarket : public CArtifactsOfHeroBase { public: - std::function selectArtCallback; + std::function selectArtCallback; CArtifactsOfHeroMarket(const Point & position); void scrollBackpack(int offset) override; diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 12844ceb3..d8e7a7d97 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -39,41 +39,35 @@ #include "../../lib/CArtHandler.h" #include "../../lib/CArtifactInstance.h" -CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font): - perDay(false) +CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font) { init(Type, Subtype, Val, imageSize, font, ""); } -CComponent::CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize, EFonts font): - perDay(false) +CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize, EFonts font) { - init(Type, Subtype, 0, imageSize, font, Val); + init(Type, Subtype, std::nullopt, imageSize, font, Val); } CComponent::CComponent(const Component & c, ESize imageSize, EFonts font) - : perDay(false) { - if(c.id == Component::EComponentType::RESOURCE && c.when==-1) - perDay = true; - - init((Etype)c.id, c.subtype, c.val, imageSize, font); + init(c.type, c.subType, c.value, imageSize, font, ""); } -void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts fnt, std::string ValText) +void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts fnt, const std::string & ValText) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); addUsedEvents(SHOW_POPUP); - compType = Type; - subtype = Subtype; - val = Val; - valText = ValText; + data.type = Type; + data.subType = Subtype; + data.value = Val; + + customSubtitle = ValText; size = imageSize; font = fnt; - assert(compType < typeInvalid); assert(size < sizeInvalid); setSurface(getFileName()[size], (int)getIndex()); @@ -86,15 +80,16 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts pos.h += 4; //distance between text and image - auto max = 80; + // WARNING: too low values will lead to bad line-breaks in CPlayerOptionTooltipBox - check right-click on starting town in pregame + int max = 80; if (size < large) max = 72; if (size < medium) - max = 40; + max = 60; if (size < small) - max = 30; + max = 55; - if(Type == Etype::resource && !valText.empty()) + if(Type == ComponentType::RESOURCE && !ValText.empty()) max = 80; std::vector textLines = CMessage::breakText(getSubtitle(), std::max(max, pos.w), font); @@ -113,153 +108,209 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts } } -std::vector CComponent::getFileName() +std::vector CComponent::getFileName() const { - static const std::string primSkillsArr [] = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; - static const std::string secSkillsArr [] = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; - static const std::string resourceArr [] = {"SMALRES", "RESOURCE", "RESOURCE", "RESOUR82"}; - static const std::string creatureArr [] = {"CPRSMALL", "CPRSMALL", "CPRSMALL", "TWCRPORT"}; - static const std::string artifactArr[] = {"Artifact", "Artifact", "Artifact", "Artifact"}; - static const std::string spellsArr [] = {"SpellInt", "SpellInt", "SpellInt", "SPELLSCR"}; - static const std::string moraleArr [] = {"IMRL22", "IMRL30", "IMRL42", "imrl82"}; - static const std::string luckArr [] = {"ILCK22", "ILCK30", "ILCK42", "ilck82"}; - static const std::string heroArr [] = {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"}; - static const std::string flagArr [] = {"CREST58", "CREST58", "CREST58", "CREST58"}; + static const std::array primSkillsArr = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; + static const std::array secSkillsArr = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; + static const std::array resourceArr = {"SMALRES", "RESOURCE", "RESOURCE", "RESOUR82"}; + static const std::array creatureArr = {"CPRSMALL", "CPRSMALL", "CPRSMALL", "TWCRPORT"}; + static const std::array artifactArr = {"Artifact", "Artifact", "Artifact", "Artifact"}; + static const std::array spellsArr = {"SpellInt", "SpellInt", "SpellInt", "SPELLSCR"}; + static const std::array moraleArr = {"IMRL22", "IMRL30", "IMRL42", "imrl82"}; + static const std::array luckArr = {"ILCK22", "ILCK30", "ILCK42", "ilck82"}; + static const std::array heroArr = {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"}; + static const std::array flagArr = {"CREST58", "CREST58", "CREST58", "CREST58"}; - auto gen = [](const std::string * arr) -> std::vector + auto gen = [](const std::array & arr) -> std::vector { return { AnimationPath::builtin(arr[0]), AnimationPath::builtin(arr[1]), AnimationPath::builtin(arr[2]), AnimationPath::builtin(arr[3]) }; }; - switch(compType) + switch(data.type) { - case primskill: return gen(primSkillsArr); - case secskill: return gen(secSkillsArr); - case resource: return gen(resourceArr); - case creature: return gen(creatureArr); - case artifact: return gen(artifactArr); - case experience: return gen(primSkillsArr); - case spell: return gen(spellsArr); - case morale: return gen(moraleArr); - case luck: return gen(luckArr); - case building: return std::vector(4, (*CGI->townh)[subtype]->town->clientInfo.buildingsIcons); - case hero: return gen(heroArr); - case flag: return gen(flagArr); + case ComponentType::PRIM_SKILL: + case ComponentType::EXPERIENCE: + case ComponentType::MANA: + case ComponentType::LEVEL: + return gen(primSkillsArr); + case ComponentType::SEC_SKILL: + return gen(secSkillsArr); + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return gen(resourceArr); + case ComponentType::CREATURE: + return gen(creatureArr); + case ComponentType::ARTIFACT: + return gen(artifactArr); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return gen(spellsArr); + case ComponentType::MORALE: + return gen(moraleArr); + case ComponentType::LUCK: + return gen(luckArr); + case ComponentType::BUILDING: + return std::vector(4, (*CGI->townh)[data.subType.as().getFaction()]->town->clientInfo.buildingsIcons); + case ComponentType::HERO_PORTRAIT: + return gen(heroArr); + case ComponentType::FLAG: + return gen(flagArr); + default: + assert(0); + return {}; } - assert(0); - return {}; } -size_t CComponent::getIndex() +size_t CComponent::getIndex() const { - switch(compType) + switch(data.type) { - case primskill: return subtype; - case secskill: return subtype*3 + 3 + val - 1; - case resource: return subtype; - case creature: return CGI->creatures()->getByIndex(subtype)->getIconIndex(); - case artifact: return CGI->artifacts()->getByIndex(subtype)->getIconIndex(); - case experience: return 4; - case spell: return (size < large) ? subtype + 1 : subtype; - case morale: return val+3; - case luck: return val+3; - case building: return val; - case hero: return CGI->heroTypes()->getByIndex(subtype)->getIconIndex(); - case flag: return subtype; + case ComponentType::PRIM_SKILL: + return data.subType.getNum(); + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + return 4; // for whatever reason, in H3 experience icon is located in primary skills icons + case ComponentType::MANA: + return 5; // for whatever reason, in H3 mana points icon is located in primary skills icons + case ComponentType::SEC_SKILL: + return data.subType.getNum() * 3 + 3 + data.value.value_or(0) - 1; + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return data.subType.getNum(); + case ComponentType::CREATURE: + return CGI->creatures()->getById(data.subType.as())->getIconIndex(); + case ComponentType::ARTIFACT: + return CGI->artifacts()->getById(data.subType.as())->getIconIndex(); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return (size < large) ? data.subType.getNum() + 1 : data.subType.getNum(); + case ComponentType::MORALE: + return data.value.value_or(0) + 3; + case ComponentType::LUCK: + return data.value.value_or(0) + 3; + case ComponentType::BUILDING: + return data.subType.as().getBuilding(); + case ComponentType::HERO_PORTRAIT: + return CGI->heroTypes()->getById(data.subType.as())->getIconIndex(); + case ComponentType::FLAG: + return data.subType.getNum(); + default: + assert(0); + return 0; } - assert(0); - return 0; } -std::string CComponent::getDescription() +std::string CComponent::getDescription() const { - switch(compType) + switch(data.type) { - case primskill: return (subtype < 4)? CGI->generaltexth->arraytxt[2+subtype] //Primary skill - : CGI->generaltexth->allTexts[149]; //mana - case secskill: return CGI->skillh->getByIndex(subtype)->getDescriptionTranslated(val); - case resource: return CGI->generaltexth->allTexts[242]; - case creature: return ""; - case artifact: - { - auto artID = ArtifactID(subtype); - auto description = VLC->arth->objects[artID]->getDescriptionTranslated(); - if(artID == ArtifactID::SPELL_SCROLL) + case ComponentType::PRIM_SKILL: + return CGI->generaltexth->arraytxt[2+data.subType.getNum()]; + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + return CGI->generaltexth->allTexts[241]; + case ComponentType::MANA: + return CGI->generaltexth->allTexts[149]; + case ComponentType::SEC_SKILL: + return CGI->skillh->getByIndex(data.subType.getNum())->getDescriptionTranslated(data.value.value_or(0)); + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return CGI->generaltexth->allTexts[242]; + case ComponentType::CREATURE: + return ""; + case ComponentType::ARTIFACT: + return VLC->artifacts()->getById(data.subType.as())->getDescriptionTranslated(); + case ComponentType::SPELL_SCROLL: { - ArtifactUtils::insertScrrollSpellName(description, SpellID(val)); + auto description = VLC->arth->objects[ArtifactID::SPELL_SCROLL]->getDescriptionTranslated(); + ArtifactUtils::insertScrrollSpellName(description, data.subType.as()); + return description; } - return description; - } - case experience: return CGI->generaltexth->allTexts[241]; - case spell: return (*CGI->spellh)[subtype]->getDescriptionTranslated(val); - case morale: return CGI->generaltexth->heroscrn[ 4 - (val>0) + (val<0)]; - case luck: return CGI->generaltexth->heroscrn[ 7 - (val>0) + (val<0)]; - case building: return (*CGI->townh)[subtype]->town->buildings[BuildingID(val)]->getDescriptionTranslated(); - case hero: return ""; - case flag: return ""; - } - assert(0); - return ""; -} - -std::string CComponent::getSubtitle() -{ - if(!perDay) - return getSubtitleInternal(); - - std::string ret = CGI->generaltexth->allTexts[3]; - boost::replace_first(ret, "%d", getSubtitleInternal()); - return ret; -} - -std::string CComponent::getSubtitleInternal() -{ - //FIXME: some of these are horrible (e.g creature) - switch(compType) - { - case primskill: return boost::str(boost::format("%+d %s") % val % (subtype < 4 ? CGI->generaltexth->primarySkillNames[subtype] : CGI->generaltexth->allTexts[387])); - case secskill: return CGI->generaltexth->levels[val-1] + "\n" + CGI->skillh->getByIndex(subtype)->getNameTranslated(); - case resource: return valText.empty() ? std::to_string(val) : valText; - case creature: + case ComponentType::SPELL: + return VLC->spells()->getById(data.subType.as())->getDescriptionTranslated(data.value.value_or(0)); + case ComponentType::MORALE: + return CGI->generaltexth->heroscrn[ 4 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)]; + case ComponentType::LUCK: + return CGI->generaltexth->heroscrn[ 7 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)]; + case ComponentType::BUILDING: { - auto creature = CGI->creh->getByIndex(subtype); - if ( val ) - return std::to_string(val) + " " + (val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); + auto index = data.subType.as(); + return (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()]->getDescriptionTranslated(); + } + case ComponentType::HERO_PORTRAIT: + return ""; + case ComponentType::FLAG: + return ""; + default: + assert(0); + return ""; + } +} + +std::string CComponent::getSubtitle() const +{ + if (!customSubtitle.empty()) + return customSubtitle; + + switch(data.type) + { + case ComponentType::PRIM_SKILL: + if (data.value) + return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->primarySkillNames[data.subType.getNum()]); else - return val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated(); - } - case artifact: return CGI->artifacts()->getByIndex(subtype)->getNameTranslated(); - case experience: + return CGI->generaltexth->primarySkillNames[data.subType.getNum()]; + case ComponentType::EXPERIENCE: + return std::to_string(data.value.value_or(0)); + case ComponentType::LEVEL: { - if(subtype == 1) //+1 level - tree of knowledge - { - std::string level = CGI->generaltexth->allTexts[442]; - boost::replace_first(level, "1", std::to_string(val)); - return level; - } + std::string level = CGI->generaltexth->allTexts[442]; + boost::replace_first(level, "1", std::to_string(data.value.value_or(0))); + return level; + } + case ComponentType::MANA: + return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->allTexts[387]); + case ComponentType::SEC_SKILL: + return CGI->generaltexth->levels[data.value.value_or(0)-1] + "\n" + CGI->skillh->getById(data.subType.as())->getNameTranslated(); + case ComponentType::RESOURCE: + return std::to_string(data.value.value_or(0)); + case ComponentType::RESOURCE_PER_DAY: + return boost::str(boost::format(CGI->generaltexth->allTexts[3]) % data.value.value_or(0)); + case ComponentType::CREATURE: + { + auto creature = CGI->creh->getById(data.subType.as()); + if(data.value) + return std::to_string(*data.value) + " " + (*data.value > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); else - { - return std::to_string(val); //amount of experience OR level required for seer hut; - } + return creature->getNamePluralTranslated(); } - case spell: return CGI->spells()->getByIndex(subtype)->getNameTranslated(); - case morale: return ""; - case luck: return ""; - case building: - { - auto building = (*CGI->townh)[subtype]->town->buildings[BuildingID(val)]; - if(!building) + case ComponentType::ARTIFACT: + return CGI->artifacts()->getById(data.subType.as())->getNameTranslated(); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return CGI->spells()->getById(data.subType.as())->getNameTranslated(); + case ComponentType::MORALE: + return ""; + case ComponentType::LUCK: + return ""; + case ComponentType::BUILDING: { - logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[subtype]->town->faction->getNameTranslated(), val); - return (boost::format("Missing building #%d") % val).str(); + auto index = data.subType.as(); + auto building = (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()]; + if(!building) + { + logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[index.getFaction()]->town->faction->getNameTranslated(), index.getBuilding().getNum()); + return (boost::format("Missing building #%d") % index.getBuilding().getNum()).str(); + } + return building->getNameTranslated(); } - return building->getNameTranslated(); - } - case hero: return ""; - case flag: return CGI->generaltexth->capColors[subtype]; + case ComponentType::HERO_PORTRAIT: + return ""; + case ComponentType::FLAG: + return CGI->generaltexth->capColors[data.subType.as().getNum()]; + default: + assert(0); + return ""; } - logGlobal->error("Invalid CComponent type: %d", (int)compType); - return ""; } void CComponent::setSurface(const AnimationPath & defName, int imgPos) @@ -299,7 +350,7 @@ CSelectableComponent::CSelectableComponent(const Component &c, std::function OnSelect): +CSelectableComponent::CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize, std::function OnSelect): CComponent(Type,Sub,Val, imageSize),onSelect(OnSelect) { setRedrawParent(true); @@ -376,6 +427,7 @@ void CComponentBox::placeComponents(bool selectable) for(auto & comp : components) { addChild(comp.get()); + comp->recActions = defActions; //FIXME: for some reason, received component might have recActions set to 0 comp->moveTo(Point(pos.x, pos.y)); } diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index 3f94137ff..f4d360460 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -12,6 +12,7 @@ #include "../gui/CIntObject.h" #include "../render/EFont.h" #include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,11 +27,6 @@ class CLabel; class CComponent : public virtual CIntObject { public: - enum Etype - { - primskill, secskill, resource, creature, artifact, experience, spell, morale, luck, building, hero, flag, typeInvalid - }; - //NOTE: not all types have exact these sizes or have less than 4 of them. In such cases closest one will be used enum ESize { @@ -44,29 +40,24 @@ public: private: std::vector> lines; - size_t getIndex(); - std::vector getFileName(); + size_t getIndex() const; + std::vector getFileName() const; void setSurface(const AnimationPath & defName, int imgPos); - std::string getSubtitleInternal(); - void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL, std::string ValText=""); + void init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font, const std::string & ValText); public: std::shared_ptr image; - - Etype compType; //component type + Component data; + std::string customSubtitle; ESize size; //component size. EFonts font; //Font size of label - int subtype; //type-dependant subtype. See getSomething methods for details - int val; // value \ strength \ amount of component. See getSomething methods for details - std::string valText; // value instead of amount; currently only for resource - bool perDay; // add "per day" text to subtitle - std::string getDescription(); - std::string getSubtitle(); + std::string getDescription() const; + std::string getSubtitle() const; - CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large, EFonts font = FONT_SMALL); - CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val = std::nullopt, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize=large, EFonts font = FONT_SMALL); CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL); void showPopupWindow(const Point & cursorPosition) override; //call-in @@ -86,7 +77,7 @@ public: void clickPressed(const Point & cursorPosition) override; //call-in void clickDouble(const Point & cursorPosition) override; //call-in - CSelectableComponent(Etype Type, int Sub, int Val, ESize imageSize=large, std::function OnSelect = nullptr); + CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize=large, std::function OnSelect = nullptr); CSelectableComponent(const Component & c, std::function OnSelect = nullptr); }; @@ -101,7 +92,7 @@ class CComponentBox : public CIntObject std::shared_ptr selected; std::function onSelect; - static constexpr int defaultBetweenImagesMin = 20; + static constexpr int defaultBetweenImagesMin = 42; static constexpr int defaultBetweenSubtitlesMin = 10; static constexpr int defaultBetweenRows = 22; static constexpr int defaultComponentsInRow = 4; diff --git a/client/widgets/CExchangeController.cpp b/client/widgets/CExchangeController.cpp index 23c9019d8..0b2c25683 100644 --- a/client/widgets/CExchangeController.cpp +++ b/client/widgets/CExchangeController.cpp @@ -35,7 +35,8 @@ void CExchangeController::swapArmy() auto leftSlots = getStacks(left); auto rightSlots = getStacks(right); - auto i = leftSlots.begin(), j = rightSlots.begin(); + auto i = leftSlots.begin(); + auto j = rightSlots.begin(); for(; i != leftSlots.end() && j != rightSlots.end(); i++, j++) { diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 3942da8a0..a63121f04 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -196,16 +196,18 @@ bool CGarrisonSlot::highlightOrDropArtifact() artSelected = true; if (myStack) // try dropping the artifact only if the slot isn't empty { - ArtifactLocation src(srcHero, ArtifactPosition::TRANSITION_POS); - ArtifactLocation dst(myStack, ArtifactPosition::CREATURE_SLOT); - if(pickedArtInst->canBePutAt(dst, true)) + ArtifactLocation src(srcHero->id, ArtifactPosition::TRANSITION_POS); + ArtifactLocation dst(getObj()->id, ArtifactPosition::CREATURE_SLOT); + dst.creature = getSlot(); + + if(pickedArtInst->canBePutAt(myStack, ArtifactPosition::CREATURE_SLOT, true)) { //equip clicked stack - if(dst.getArt()) + if(auto dstArt = myStack->getArt(ArtifactPosition::CREATURE_SLOT)) { //creature can wear only one active artifact //if we are placing a new one, the old one will be returned to the hero's backpack - LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero, - ArtifactUtils::getArtBackpackPosition(srcHero, dst.getArt()->getTypeId()))); + LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero->id, + ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId()))); } LOCPLINT->cb->swapArtifacts(src, dst); } @@ -232,7 +234,8 @@ bool CGarrisonSlot::split() const CGarrisonSlot * selection = owner->getSelection(); owner->setSplittingMode(false); - int minLeft=0, minRight=0; + int minLeft=0; + int minRight=0; if(upg != selection->upg) // not splitting within same army { @@ -430,6 +433,7 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, EGa selectionImage = std::make_shared(graphics->getAnimation(imgName), 1); selectionImage->disable(); + selectionImage->center(creatureImage->pos.center()); if(Owner->smallIcons) { diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 8c6d2b7c9..af3894604 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -23,6 +23,7 @@ #include "../windows/CHeroWindow.h" #include "../windows/CSpellWindow.h" #include "../windows/GUIClasses.h" +#include "../windows/CHeroBackpackWindow.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" @@ -32,6 +33,8 @@ #include "../../lib/networkPacks/ArtifactLocation.h" #include "../../lib/CConfigHandler.h" +#include "../../CCallback.h" + void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) { artSets.emplace_back(artSet); @@ -39,18 +42,19 @@ void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) void CWindowWithArtifacts::addSetAndCallbacks(CArtifactsOfHeroPtr artSet) { - CArtifactsOfHeroBase::PutBackPickedArtCallback artPutBackHandler = []() -> void + CArtifactsOfHeroBase::PutBackPickedArtCallback artPutBackFunctor = []() -> void { CCS->curh->dragAndDropCursor(nullptr); }; addSet(artSet); - std::visit([this, artPutBackHandler](auto artSetWeak) + std::visit([this, artPutBackFunctor](auto artSetWeak) { auto artSet = artSetWeak.lock(); - artSet->leftClickCallback = std::bind(&CWindowWithArtifacts::leftClickArtPlaceHero, this, _1, _2); - artSet->showPopupCallback = std::bind(&CWindowWithArtifacts::rightClickArtPlaceHero, this, _1, _2); - artSet->setPutBackPickedArtifactCallback(artPutBackHandler); + artSet->clickPressedCallback = std::bind(&CWindowWithArtifacts::clickPressedArtPlaceHero, this, _1, _2, _3); + artSet->showPopupCallback = std::bind(&CWindowWithArtifacts::showPopupArtPlaceHero, this, _1, _2, _3); + artSet->gestureCallback = std::bind(&CWindowWithArtifacts::gestureArtPlaceHero, this, _1, _2, _3); + artSet->setPutBackPickedArtifactCallback(artPutBackFunctor); }, artSet); } @@ -77,33 +81,16 @@ const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact() return nullptr; } -void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace) +void CWindowWithArtifacts::clickPressedArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) { - const auto artSetWeak = findAOHbyRef(artsInst); - assert(artSetWeak.has_value()); + const auto artSet = findAOHbyRef(artsInst); + assert(artSet.has_value()); if(artPlace.isLocked()) return; - const auto checkSpecialArts = [](const CGHeroInstance * hero, CHeroArtPlace & artPlace) -> bool - { - if(artPlace.getArt()->getTypeId() == ArtifactID::SPELLBOOK) - { - GH.windows().createAndPushWindow(hero, LOCPLINT, LOCPLINT->battleInt.get()); - return false; - } - if(artPlace.getArt()->getTypeId() == ArtifactID::CATAPULT) - { - // The Catapult must be equipped - std::vector> catapult(1, std::make_shared(CComponent::artifact, 3, 0)); - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], catapult); - return false; - } - return true; - }; - std::visit( - [checkSpecialArts, this, &artPlace](auto artSetWeak) -> void + [this, &artPlace](auto artSetWeak) -> void { const auto artSetPtr = artSetWeak.lock(); @@ -122,8 +109,8 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst if(pickedArtInst) { - auto srcLoc = ArtifactLocation(heroPickedArt, ArtifactPosition::TRANSITION_POS); - auto dstLoc = ArtifactLocation(hero, artPlace.slot); + auto srcLoc = ArtifactLocation(heroPickedArt->id, ArtifactPosition::TRANSITION_POS); + auto dstLoc = ArtifactLocation(hero->id, artPlace.slot); if(ArtifactUtils::isSlotBackpack(artPlace.slot)) { @@ -141,7 +128,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst } } // Check if artifact transfer is possible - else if(pickedArtInst->canBePutAt(dstLoc, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID)) + else if(pickedArtInst->canBePutAt(hero, artPlace.slot, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID)) { isTransferAllowed = true; } @@ -151,26 +138,23 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst isTransferAllowed = false; } if(isTransferAllowed) - artSetPtr->swapArtifacts(srcLoc, dstLoc); + LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc); } - else + else if(auto art = artPlace.getArt()) { - if(artPlace.getArt()) + if(artSetPtr->getHero()->getOwner() == LOCPLINT->playerID) { - if(artSetPtr->getHero()->tempOwner == LOCPLINT->playerID) - { - if(checkSpecialArts(hero, artPlace)) - artSetPtr->pickUpArtifact(artPlace); - } - else - { - for(const auto artSlot : ArtifactUtils::unmovableSlots()) - if(artPlace.slot == artSlot) - { - msg = CGI->generaltexth->allTexts[21]; - break; - } - } + if(checkSpecialArts(*art, hero, std::is_same_v> ? true : false)) + LOCPLINT->cb->swapArtifacts(ArtifactLocation(artSetPtr->getHero()->id, artPlace.slot), ArtifactLocation(artSetPtr->getHero()->id, ArtifactPosition::TRANSITION_POS)); + } + else + { + for(const auto artSlot : ArtifactUtils::unmovableSlots()) + if(artPlace.slot == artSlot) + { + msg = CGI->generaltexth->allTexts[21]; + break; + } } } @@ -206,10 +190,17 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst } } } - }, artSetWeak.value()); + else if constexpr(std::is_same_v>) + { + const auto hero = artSetPtr->getHero(); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, artPlace.slot), ArtifactLocation(hero->id, artSetPtr->getFilterSlot())); + if(closeCallback) + closeCallback(); + } + }, artSet.value()); } -void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace) +void CWindowWithArtifacts::showPopupArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) { const auto artSetWeak = findAOHbyRef(artsInst); assert(artSetWeak.has_value()); @@ -218,12 +209,13 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns return; std::visit( - [&artPlace](auto artSetWeak) -> void + [&artPlace, &cursorPosition](auto artSetWeak) -> void { const auto artSetPtr = artSetWeak.lock(); // Hero (Main, Exchange) window, Kingdom window, Backpack window right click handler if constexpr( + std::is_same_v> || std::is_same_v> || std::is_same_v> || std::is_same_v>) @@ -239,16 +231,42 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns return; } if(artPlace.text.size()) - artPlace.LRClickableAreaWTextComp::showPopupWindow(GH.getCursorPosition()); + artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition); } } // Altar window, Market window right click handler else if constexpr( - std::is_same_v> || - std::is_same_v>) + std::is_same_v> || + std::is_same_v>) { if(artPlace.getArt() && artPlace.text.size()) - artPlace.LRClickableAreaWTextComp::showPopupWindow(GH.getCursorPosition()); + artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition); + } + }, artSetWeak.value()); +} + +void CWindowWithArtifacts::gestureArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition) +{ + const auto artSetWeak = findAOHbyRef(artsInst); + assert(artSetWeak.has_value()); + if(artPlace.isLocked()) + return; + + std::visit( + [&artPlace, cursorPosition](auto artSetWeak) -> void + { + const auto artSetPtr = artSetWeak.lock(); + if constexpr( + std::is_same_v> || + std::is_same_v>) + { + if(!settings["general"]["enableUiEnhancements"].Bool()) + return; + + GH.windows().createAndPushWindow(artSetPtr->getHero(), artPlace.slot); + auto backpackWindow = GH.windows().topWindow(); + backpackWindow->moveTo(cursorPosition - Point(1, 1)); + backpackWindow->fitToScreen(15); } }, artSetWeak.value()); } @@ -270,7 +288,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const // we have a different artifact may look surprising... but it's valid. auto pickedArtInst = std::get(curState.value()); - assert(!pickedArtInst || destLoc.isHolder(std::get(curState.value()))); + assert(!pickedArtInst || destLoc.artHolder == std::get(curState.value())->id); auto artifactMovedBody = [this, withRedraw, &destLoc, &pickedArtInst](auto artSetWeak) -> void { @@ -316,7 +334,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const } // Make sure the status bar is updated so it does not display old text - if(destLoc.getHolderArtSet() == hero) + if(destLoc.artHolder == hero->id) { if(auto artPlace = artSetPtr->getArtPlace(destLoc.slot)) artPlace->hover(true); @@ -432,3 +450,31 @@ void CWindowWithArtifacts::markPossibleSlots() std::visit(artifactAssembledBody, artSetWeak); } } + +bool CWindowWithArtifacts::checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance * hero, bool isTrade) +{ + const auto artId = artInst.getTypeId(); + + if(artId == ArtifactID::SPELLBOOK) + { + GH.windows().createAndPushWindow(hero, LOCPLINT, LOCPLINT->battleInt.get()); + return false; + } + if(artId == ArtifactID::CATAPULT) + { + // The Catapult must be equipped + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], + std::vector>(1, std::make_shared(ComponentType::ARTIFACT, ArtifactID(ArtifactID::CATAPULT)))); + return false; + } + if(isTrade) + { + if(!artInst.artType->isTradable()) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[21], + std::vector>(1, std::make_shared(ComponentType::ARTIFACT, artId))); + return false; + } + } + return true; +} diff --git a/client/widgets/CWindowWithArtifacts.h b/client/widgets/CWindowWithArtifacts.h index 777e4537d..6a1ca1841 100644 --- a/client/widgets/CWindowWithArtifacts.h +++ b/client/widgets/CWindowWithArtifacts.h @@ -24,7 +24,8 @@ public: std::weak_ptr, std::weak_ptr, std::weak_ptr, - std::weak_ptr>; + std::weak_ptr, + std::weak_ptr>; using CloseCallback = std::function; void addSet(CArtifactsOfHeroPtr artSet); @@ -32,8 +33,9 @@ public: void addCloseCallback(CloseCallback callback); const CGHeroInstance * getHeroPickedArtifact(); const CArtifactInstance * getPickedArtifact(); - void leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace); - void rightClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace); + void clickPressedArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition); + void showPopupArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition); + void gestureArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition); void artifactRemoved(const ArtifactLocation & artLoc) override; void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override; @@ -48,4 +50,5 @@ protected: std::optional> getState(); std::optional findAOHbyRef(CArtifactsOfHeroBase & artsInst); void markPossibleSlots(); + bool checkSpecialArts(const CArtifactInstance & artInst, const CGHeroInstance * hero, bool isTrade); }; diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index 7b6ee4920..8bf18bd69 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -22,19 +22,20 @@ ComboBox::DropDown::Item::Item(const JsonNode & config, ComboBox::DropDown & _dr { build(config); - if(auto w = widget("hoverImage")) + if(auto w = widget("hoverImage")) { pos.w = w->pos.w; pos.h = w->pos.h; + w->disable(); } setRedrawParent(true); } void ComboBox::DropDown::Item::updateItem(int idx, const void * _item) { + item = _item; if(auto w = widget("labelName")) { - item = _item; if(dropDown.comboBox.getItemText) w->setText(dropDown.comboBox.getItemText(idx, item)); } @@ -42,14 +43,14 @@ void ComboBox::DropDown::Item::updateItem(int idx, const void * _item) void ComboBox::DropDown::Item::hover(bool on) { - auto h = widget("hoverImage"); + auto h = widget("hoverImage"); auto w = widget("labelName"); if(h && w) { - if(w->getText().empty()) - h->visible = false; + if(w->getText().empty() || on == false) + h->disable(); else - h->visible = on; + h->enable(); } redraw(); } @@ -66,7 +67,7 @@ void ComboBox::DropDown::Item::clickReleased(const Point & cursorPosition) dropDown.clickReleased(cursorPosition); } -ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox): +ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox, Point dropDownPosition): InterfaceObjectConfigurable(LCLICK | HOVER), comboBox(_comboBox) { @@ -77,7 +78,7 @@ ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox): addCallback("sliderMove", std::bind(&ComboBox::DropDown::sliderMove, this, std::placeholders::_1)); - pos = comboBox.pos; + pos = comboBox.pos + dropDownPosition; build(config); @@ -129,20 +130,21 @@ void ComboBox::DropDown::clickPressed(const Point & cursorPosition) void ComboBox::DropDown::updateListItems() { + int elemIdx = 0; + if(auto w = widget("slider")) + elemIdx = w->getValue(); + + for(auto item : items) { - int elemIdx = w->getValue(); - for(auto item : items) + if(elemIdx < curItems.size()) { - if(elemIdx < curItems.size()) - { - item->updateItem(elemIdx, curItems[elemIdx]); - elemIdx++; - } - else - { - item->updateItem(elemIdx); - } + item->updateItem(elemIdx, curItems[elemIdx]); + elemIdx++; + } + else + { + item->updateItem(elemIdx); } } } @@ -155,19 +157,20 @@ void ComboBox::DropDown::setItem(const void * item) GH.windows().popWindows(1); } -ComboBox::ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key, bool playerColoredButton): +ComboBox::ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, Point dropDownPosition, EShortcut key, bool playerColoredButton): CButton(position, defName, help, 0, key, playerColoredButton) { - addCallback([&, dropDownDescriptor]() + addCallback([this, dropDownDescriptor, dropDownPosition]() { - GH.windows().createAndPushWindow(dropDownDescriptor, *this); + GH.windows().createAndPushWindow(dropDownDescriptor, *this, dropDownPosition); }); } void ComboBox::setItem(const void * item) { - if(auto w = std::dynamic_pointer_cast(overlay); getItemText) - addTextOverlay(getItemText(0, item), w->font, w->color); + auto w = std::dynamic_pointer_cast(getOverlay()); + if( w && getItemText) + setTextOverlay(getItemText(0, item), w->font, w->color); if(onSetItem) onSetItem(item); diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h index dafd496bf..cd3c1e883 100644 --- a/client/widgets/ComboBox.h +++ b/client/widgets/ComboBox.h @@ -32,17 +32,18 @@ class ComboBox : public CButton friend struct Item; public: - DropDown(const JsonNode &, ComboBox &); + DropDown(const JsonNode &, ComboBox &, Point dropDownPosition); bool receiveEvent(const Point & position, int eventType) const override; void clickPressed(const Point & cursorPosition) override; void setItem(const void *); + + void updateListItems(); private: std::shared_ptr buildItem(const JsonNode & config); void sliderMove(int slidPos); - void updateListItems(); ComboBox & comboBox; std::vector> items; @@ -54,7 +55,7 @@ class ComboBox : public CButton void setItem(const void *); public: - ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key = {}, bool playerColoredButton = false); + ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, Point dropDownPosition, EShortcut key = {}, bool playerColoredButton = false); //define this callback to fill input vector with data for the combo box std::function &)> onConstructItems; @@ -66,4 +67,6 @@ public: std::function getItemText; void setItem(int id); + + void updateListItems(); }; diff --git a/client/widgets/CreatureCostBox.cpp b/client/widgets/CreatureCostBox.cpp index 546c71864..24dd5ec36 100644 --- a/client/widgets/CreatureCostBox.cpp +++ b/client/widgets/CreatureCostBox.cpp @@ -38,8 +38,8 @@ void CreatureCostBox::createItems(TResources res) TResources::nziterator iter(res); while(iter.valid()) { - ImagePtr image = std::make_shared(AnimationPath::builtin("RESOURCE"), iter->resType); - LabelPtr text = std::make_shared(15, 43, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "0"); + auto image = std::make_shared(AnimationPath::builtin("RESOURCE"), iter->resType); + auto text = std::make_shared(15, 43, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "0"); resources.insert(std::make_pair(iter->resType, std::make_pair(text, image))); iter++; diff --git a/client/widgets/GraphicalPrimitiveCanvas.cpp b/client/widgets/GraphicalPrimitiveCanvas.cpp new file mode 100644 index 000000000..c37325c55 --- /dev/null +++ b/client/widgets/GraphicalPrimitiveCanvas.cpp @@ -0,0 +1,85 @@ +/* + * GraphicalPrimitiveCanvas.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "GraphicalPrimitiveCanvas.h" + +#include "../render/Canvas.h" + +GraphicalPrimitiveCanvas::GraphicalPrimitiveCanvas(Rect dimensions) +{ + pos = dimensions + pos.topLeft(); +} + +void GraphicalPrimitiveCanvas::showAll(Canvas & to) +{ + auto const & translatePoint = [this](const Point & input){ + int x = input.x < 0 ? pos.w + input.x : input.x; + int y = input.y < 0 ? pos.h + input.y : input.y; + + return Point(x,y); + }; + + for (auto const & entry : primitives) + { + switch (entry.type) + { + case PrimitiveType::LINE: + { + to.drawLine(pos.topLeft() + translatePoint(entry.a), pos.topLeft() + translatePoint(entry.b), entry.color, entry.color); + break; + } + case PrimitiveType::FILLED_BOX: + { + to.drawColorBlended(Rect(pos.topLeft() + translatePoint(entry.a), translatePoint(entry.b)), entry.color); + break; + } + case PrimitiveType::RECTANGLE: + { + to.drawBorder(Rect(pos.topLeft() + translatePoint(entry.a), translatePoint(entry.b)), entry.color); + break; + } + } + } +} + +void GraphicalPrimitiveCanvas::addLine(const Point & from, const Point & to, const ColorRGBA & color) +{ + primitives.push_back({color, from, to, PrimitiveType::LINE}); +} + +void GraphicalPrimitiveCanvas::addBox(const Point & topLeft, const Point & size, const ColorRGBA & color) +{ + primitives.push_back({color, topLeft, size, PrimitiveType::FILLED_BOX}); +} + +void GraphicalPrimitiveCanvas::addRectangle(const Point & topLeft, const Point & size, const ColorRGBA & color) +{ + primitives.push_back({color, topLeft, size, PrimitiveType::RECTANGLE}); +} + +TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color) : + GraphicalPrimitiveCanvas(position) +{ + addBox(Point(0,0), Point(-1, -1), color); +} + +TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine, int width) : + GraphicalPrimitiveCanvas(position) +{ + addBox(Point(0,0), Point(-1, -1), color); + for (int i = 0; i < width; ++i) + addRectangle(Point(i,i), Point(-1-i*2, -1-i*2), colorLine); +} + +SimpleLine::SimpleLine(Point pos1, Point pos2, ColorRGBA color) : + GraphicalPrimitiveCanvas(Rect(pos1, pos2 - pos1)) +{ + addLine(Point(0,0), Point(-1, -1), color); +} diff --git a/client/widgets/GraphicalPrimitiveCanvas.h b/client/widgets/GraphicalPrimitiveCanvas.h new file mode 100644 index 000000000..2cf508b94 --- /dev/null +++ b/client/widgets/GraphicalPrimitiveCanvas.h @@ -0,0 +1,54 @@ +/* + * GraphicalPrimitiveCanvas.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../gui/CIntObject.h" + +class GraphicalPrimitiveCanvas : public CIntObject +{ + enum class PrimitiveType + { + LINE, + RECTANGLE, + FILLED_BOX + }; + + struct PrimitiveEntry + { + ColorRGBA color; + Point a; + Point b; + PrimitiveType type; + }; + + std::vector primitives; + + void showAll(Canvas & to) override; + +public: + GraphicalPrimitiveCanvas(Rect position); + + void addLine(const Point & from, const Point & to, const ColorRGBA & color); + void addBox(const Point & topLeft, const Point & size, const ColorRGBA & color); + void addRectangle(const Point & topLeft, const Point & size, const ColorRGBA & color); +}; + +class TransparentFilledRectangle : public GraphicalPrimitiveCanvas +{ +public: + TransparentFilledRectangle(Rect position, ColorRGBA color); + TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine, int width = 1); +}; + +class SimpleLine : public GraphicalPrimitiveCanvas +{ +public: + SimpleLine(Point pos1, Point pos2, ColorRGBA color); +}; diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 76ccd2838..8ae5795fb 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -35,7 +35,6 @@ CPicture::CPicture(std::shared_ptr image, const Point & position) : bg(image) - , visible(true) , needRefresh(false) { pos += position; @@ -53,7 +52,6 @@ CPicture::CPicture( const ImagePath & bmpname ) CPicture::CPicture( const ImagePath & bmpname, const Point & position ) : bg(GH.renderHandler().loadImage(bmpname)) - , visible(true) , needRefresh(false) { pos.x += position.x; @@ -81,13 +79,13 @@ CPicture::CPicture(std::shared_ptr image, const Rect &SrcRect, int x, in void CPicture::show(Canvas & to) { - if (visible && needRefresh) + if (needRefresh) showAll(to); } void CPicture::showAll(Canvas & to) { - if(bg && visible) + if(bg) { if (srcRect.has_value()) to.draw(bg, pos.topLeft(), *srcRect); diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 2a9f3ae10..94fef0c1c 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -34,10 +34,6 @@ public: /// If set to true, iamge will be redrawn on each frame bool needRefresh; - /// If set to false, image will not be rendered - /// Deprecated, use CIntObject::disable()/enable() instead - bool visible; - std::shared_ptr getSurface() { return bg; diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 4d5eff1d5..fa6c174d1 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -21,8 +21,9 @@ #include "../gui/WindowHandler.h" #include "../eventsSDL/InputHandler.h" #include "../windows/CTradeWindow.h" -#include "../widgets/TextControls.h" #include "../widgets/CGarrisonInt.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" +#include "../widgets/TextControls.h" #include "../windows/CCastleInterface.h" #include "../windows/InfoWindows.h" #include "../render/Canvas.h" @@ -95,16 +96,16 @@ void LRClickableAreaWTextComp::clickPressed(const Point & cursorPosition) LOCPLINT->showInfoDialog(text, comp); } -LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, int BaseType) - : LRClickableAreaWText(Pos), baseType(BaseType), bonusValue(-1) +LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, ComponentType BaseType) + : LRClickableAreaWText(Pos) { - type = -1; + component.type = BaseType; } std::shared_ptr LRClickableAreaWTextComp::createComponent() const { - if(baseType >= 0) - return std::make_shared(CComponent::Etype(baseType), type, bonusValue); + if(component.type != ComponentType::NONE) + return std::make_shared(component); else return std::shared_ptr(); } @@ -121,9 +122,10 @@ void LRClickableAreaWTextComp::showPopupWindow(const Point & cursorPosition) } CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * hero) - : CIntObject(LCLICK | HOVER), + : CIntObject(LCLICK | SHOW_POPUP | HOVER), hero(hero), - clickFunctor(nullptr) + clickFunctor(nullptr), + clickRFunctor(nullptr) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -147,12 +149,23 @@ void CHeroArea::addClickCallback(ClickFunctor callback) clickFunctor = callback; } +void CHeroArea::addRClickCallback(ClickFunctor callback) +{ + clickRFunctor = callback; +} + void CHeroArea::clickPressed(const Point & cursorPosition) { if(clickFunctor) clickFunctor(); } +void CHeroArea::showPopupWindow(const Point & cursorPosition) +{ + if(clickRFunctor) + clickRFunctor(); +} + void CHeroArea::hover(bool on) { if (on && hero) @@ -164,17 +177,11 @@ void CHeroArea::hover(bool on) void LRClickableAreaOpenTown::clickPressed(const Point & cursorPosition) { if(town) - { LOCPLINT->openTownWindow(town); - if ( type == 2 ) - LOCPLINT->castleInt->builds->buildingClicked(BuildingID::VILLAGE_HALL); - else if ( type == 3 && town->fortLevel() ) - LOCPLINT->castleInt->builds->buildingClicked(BuildingID::FORT); - } } LRClickableAreaOpenTown::LRClickableAreaOpenTown(const Rect & Pos, const CGTownInstance * Town) - : LRClickableAreaWTextComp(Pos, -1), town(Town) + : LRClickableAreaWTextComp(Pos), town(Town) { } @@ -521,14 +528,16 @@ CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - auto creatureData = (*CGI->creh)[creature->stacks.begin()->second->getCreatureID()].get(); - creatureImage = std::make_shared(graphics->getAnimation(AnimationPath::builtin("TWCRPORT")), creatureData->getIconIndex()); + auto creatureID = creature->getCreature(); + int32_t creatureIconIndex = CGI->creatures()->getById(creatureID)->getIconIndex(); + + creatureImage = std::make_shared(graphics->getAnimation(AnimationPath::builtin("TWCRPORT")), creatureIconIndex); creatureImage->center(Point(parent->pos.x + parent->pos.w / 2, parent->pos.y + creatureImage->pos.h / 2 + 11)); bool isHeroSelected = LOCPLINT->localState->getCurrentHero() != nullptr; std::string textContent = isHeroSelected - ? creature->getHoverText(LOCPLINT->localState->getCurrentHero()) - : creature->getHoverText(LOCPLINT->playerID); + ? creature->getPopupText(LOCPLINT->localState->getCurrentHero()) + : creature->getPopupText(LOCPLINT->playerID); //TODO: window is bigger than OH3 //TODO: vertical alignment does not match H3. Commented below example that matches H3 for creatures count but supports only 1 line: @@ -542,20 +551,21 @@ void MoraleLuckBox::set(const AFactionMember * node) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - const int textId[] = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:} + const std::array textId = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:} const int noneTxtId = 108; //Russian version uses same text for neutral morale\luck - const int neutralDescr[] = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat. - const int componentType[] = {CComponent::luck, CComponent::morale}; - const int hoverTextBase[] = {7, 4}; + const std::array neutralDescr = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat. + const std::array componentType = {ComponentType::LUCK, ComponentType::MORALE}; + const std::array hoverTextBase = {7, 4}; TConstBonusListPtr modifierList = std::make_shared(); - bonusValue = 0; + + component.value = 0; if(node) - bonusValue = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList); + component.value = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList); - int mrlt = (bonusValue>0)-(bonusValue<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good + int mrlt = (component.value>0)-(component.value<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good hoverText = CGI->generaltexth->heroscrn[hoverTextBase[morale] - mrlt]; - baseType = componentType[morale]; + component.type = componentType[morale]; text = CGI->generaltexth->arraytxt[textId[morale]]; boost::algorithm::replace_first(text,"%s",CGI->generaltexth->arraytxt[neutralDescr[morale]-mrlt]); @@ -563,27 +573,32 @@ void MoraleLuckBox::set(const AFactionMember * node) || node->getBonusBearer()->hasBonusOfType(BonusType::NON_LIVING))) { text += CGI->generaltexth->arraytxt[113]; //unaffected by morale - bonusValue = 0; + component.value = 0; } else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE)) { auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE)); text += "\n" + noMorale->Description(); - bonusValue = 0; + component.value = 0; } else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK)) { auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_LUCK)); text += "\n" + noLuck->Description(); - bonusValue = 0; + component.value = 0; } else { std::string addInfo = ""; for(auto & bonus : * modifierList) { - if(bonus->val) - addInfo += "\n" + bonus->Description(); + if(bonus->val) { + const std::string& description = bonus->Description(); + //arraytxt already contains \n + if (description.size() && description[0] != '\n') + addInfo += '\n'; + addInfo += description; + } } text = addInfo.empty() ? text + CGI->generaltexth->arraytxt[noneTxtId] @@ -595,15 +610,16 @@ void MoraleLuckBox::set(const AFactionMember * node) else imageName = morale ? "IMRL42" : "ILCK42"; - image = std::make_shared(AnimationPath::builtin(imageName), bonusValue + 3); + image = std::make_shared(AnimationPath::builtin(imageName), *component.value + 3); image->moveBy(Point(pos.w/2 - image->pos.w/2, pos.h/2 - image->pos.h/2));//center icon + if(settings["general"]["enableUiEnhancements"].Bool()) + label = std::make_shared(small ? 30 : 42, small ? 20 : 38, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(modifierList->totalValue())); } MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small) : morale(Morale), small(Small) { - bonusValue = 0; pos = r + pos.topLeft(); defActions = 255-DISPOSE; } @@ -648,30 +664,40 @@ void CCreaturePic::setAmount(int newAmount) amount->setText(""); } -TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color) : - color(color), colorLine(ColorRGBA()), drawLine(false) +SelectableSlot::SelectableSlot(Rect area, Point oversize, const int width) + : LRClickableAreaWTextComp(area) + , selected(false) { - pos = position + pos.topLeft(); + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + selection = std::make_shared( Rect(-oversize, area.dimensions() + oversize * 2), Colors::TRANSPARENCY, Colors::YELLOW, width); + selectSlot(false); } -TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine) : - color(color), colorLine(colorLine), drawLine(true) +SelectableSlot::SelectableSlot(Rect area, Point oversize) + : SelectableSlot(area, oversize, 1) { - pos = position + pos.topLeft(); } -void TransparentFilledRectangle::showAll(Canvas & to) +SelectableSlot::SelectableSlot(Rect area, const int width) + : SelectableSlot(area, Point(), width) { - to.drawColorBlended(pos, color); - if(drawLine) - to.drawBorder(pos, colorLine); } -SimpleLine::SimpleLine(Point pos1, Point pos2, ColorRGBA color) : - pos1(pos1), pos2(pos2), color(color) -{} - -void SimpleLine::showAll(Canvas & to) +void SelectableSlot::selectSlot(bool on) { - to.drawLine(pos1 + pos.topLeft(), pos2 + pos.topLeft(), color, color); + selection->setEnabled(on); + selected = on; +} + +bool SelectableSlot::isSelected() const +{ + return selected; +} + +void SelectableSlot::setSelectionWidth(int width) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + selection = std::make_shared( selection->pos - pos.topLeft(), Colors::TRANSPARENCY, Colors::YELLOW, width); + selectSlot(selected); } diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 021193e9a..36a741755 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -10,6 +10,7 @@ #pragma once #include "../gui/CIntObject.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN @@ -32,6 +33,7 @@ class CCreatureAnim; class CComponent; class CAnimImage; class LRClickableArea; +class TransparentFilledRectangle; /// Shows a text by moving the mouse cursor over the object class CHoverableArea: public virtual CIntObject @@ -39,7 +41,7 @@ class CHoverableArea: public virtual CIntObject public: std::string hoverText; - virtual void hover (bool on) override; + void hover (bool on) override; CHoverableArea(); virtual ~CHoverableArea(); @@ -189,12 +191,15 @@ public: CHeroArea(int x, int y, const CGHeroInstance * hero); void addClickCallback(ClickFunctor callback); + void addRClickCallback(ClickFunctor callback); void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; void hover(bool on) override; private: const CGHeroInstance * hero; std::shared_ptr portrait; ClickFunctor clickFunctor; + ClickFunctor clickRFunctor; ClickFunctor showPopupHandler; }; @@ -202,13 +207,12 @@ private: class LRClickableAreaWTextComp: public LRClickableAreaWText { public: - int type; - int baseType; - int bonusValue; + Component component; + void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), int BaseType = -1); + LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), ComponentType baseType = ComponentType::NONE); std::shared_ptr createComponent() const; }; @@ -235,6 +239,7 @@ public: class MoraleLuckBox : public LRClickableAreaWTextComp { std::shared_ptr image; + std::shared_ptr label; public: bool morale; //true if morale, false if luck bool small; @@ -244,23 +249,16 @@ public: MoraleLuckBox(bool Morale, const Rect &r, bool Small=false); }; -class TransparentFilledRectangle : public CIntObject +class SelectableSlot : public LRClickableAreaWTextComp { - ColorRGBA color; - ColorRGBA colorLine; - bool drawLine; -public: - TransparentFilledRectangle(Rect position, ColorRGBA color); - TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine); - void showAll(Canvas & to) override; -}; + std::shared_ptr selection; + bool selected; -class SimpleLine : public CIntObject -{ - Point pos1; - Point pos2; - ColorRGBA color; public: - SimpleLine(Point pos1, Point pos2, ColorRGBA color); - void showAll(Canvas & to) override; -}; \ No newline at end of file + SelectableSlot(Rect area, Point oversize, const int width); + SelectableSlot(Rect area, Point oversize); + SelectableSlot(Rect area, const int width = 1); + void selectSlot(bool on); + bool isSelected() const; + void setSelectionWidth(int width); +}; diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index 955c01649..68b81bbe8 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -21,23 +21,24 @@ void CSlider::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) { - double v = 0; + double newPosition = 0; if(getOrientation() == Orientation::HORIZONTAL) { - v = cursorPosition.x - pos.x - 24; - v *= positions; - v /= (pos.w - 48); + newPosition = cursorPosition.x - pos.x - 24; + newPosition *= positions; + newPosition /= (pos.w - 48); } else { - v = cursorPosition.y - pos.y - 24; - v *= positions; - v /= (pos.h - 48); + newPosition = cursorPosition.y - pos.y - 24; + newPosition *= positions; + newPosition /= (pos.h - 48); } - v += 0.5; - if(v!=value) + + int positionInteger = std::round(newPosition); + if(positionInteger != value) { - scrollTo(static_cast(v)); + scrollTo(static_cast(newPosition)); } } @@ -69,6 +70,11 @@ int CSlider::getValue() const return value; } +void CSlider::setValue(int to) +{ + scrollTo(value); +} + int CSlider::getCapacity() const { return capacity; @@ -119,7 +125,7 @@ void CSlider::scrollTo(int to) updateSliderPos(); - moved(to); + moved(getValue()); } void CSlider::clickPressed(const Point & cursorPosition) @@ -151,6 +157,11 @@ void CSlider::clickPressed(const Point & cursorPosition) bool CSlider::receiveEvent(const Point &position, int eventType) const { + if (eventType == LCLICK) + { + return pos.isInside(position) && !left->pos.isInside(position) && !right->pos.isInside(position); + } + if(eventType != WHEEL && eventType != GESTURE) { return CIntObject::receiveEvent(position, eventType); @@ -164,7 +175,7 @@ bool CSlider::receiveEvent(const Point &position, int eventType) const return testTarget.isInside(position); } -CSlider::CSlider(Point position, int totalw, std::function Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style) +CSlider::CSlider(Point position, int totalw, const std::function & Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style) : Scrollable(LCLICK | DRAG, position, orientation ), capacity(Capacity), amount(Amount), @@ -195,10 +206,10 @@ CSlider::CSlider(Point position, int totalw, std::function Moved, int right = std::make_shared(Point(), AnimationPath::builtin(getOrientation() == Orientation::HORIZONTAL ? "SCNRBRT.DEF" : "SCNRBDN.DEF"), CButton::tooltip()); slider = std::make_shared(Point(), AnimationPath::builtin("SCNRBSL.DEF"), CButton::tooltip()); } - slider->actOnDown = true; - slider->soundDisabled = true; - left->soundDisabled = true; - right->soundDisabled = true; + slider->setActOnDown(true); + slider->setSoundDisabled(true); + left->setSoundDisabled(true); + right->setSoundDisabled(true); if (getOrientation() == Orientation::HORIZONTAL) right->moveBy(Point(totalw - right->pos.w, 0)); @@ -297,3 +308,31 @@ void CSlider::scrollToMax() { scrollTo(amount); } + +SliderNonlinear::SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style) + : CSlider(position, length, Moved, 1, values.size(), Value, orientation, style) + , scaledValues(values) +{ + +} + +int SliderNonlinear::getValue() const +{ + return scaledValues.at(CSlider::getValue()); +} + +void SliderNonlinear::setValue(int to) +{ + size_t nearest = 0; + + for(size_t i = 0; i < scaledValues.size(); ++i) + { + int nearestDistance = std::abs(to - scaledValues[nearest]); + int currentDistance = std::abs(to - scaledValues[i]); + + if(currentDistance < nearestDistance) + nearest = i; + } + + scrollTo(nearest); +} diff --git a/client/widgets/Slider.h b/client/widgets/Slider.h index f7a8cad10..b6e8be677 100644 --- a/client/widgets/Slider.h +++ b/client/widgets/Slider.h @@ -59,10 +59,11 @@ public: /// Amount modifier void setAmount(int to); + virtual void setValue(int to); /// Accessors int getAmount() const; - int getValue() const; + virtual int getValue() const; int getCapacity() const; void addCallback(std::function callback); @@ -80,7 +81,20 @@ public: /// @param Capacity maximal number of visible at once elements /// @param Amount total amount of elements, including not visible /// @param Value starting position - CSlider(Point position, int length, std::function Moved, int Capacity, int Amount, + CSlider(Point position, int length, const std::function & Moved, int Capacity, int Amount, int Value, Orientation orientation, EStyle style = BROWN); ~CSlider(); }; + +class SliderNonlinear : public CSlider +{ + /// If non-empty then slider has non-linear values, e.g. if slider is at position 5 out of 10 then actual "value" is not 5, but 5th value in this vector + std::vector scaledValues; + + using CSlider::setAmount; // make private +public: + void setValue(int to) override; + int getValue() const override; + + SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style); +}; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 7785fca2a..32b477818 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -47,8 +47,8 @@ void CLabel::showAll(Canvas & to) } -CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) - : CTextContainer(Align, Font, Color), text(Text) +CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text, int maxWidth) + : CTextContainer(Align, Font, Color), text(Text), maxWidth(maxWidth) { setRedrawParent(true); autoRedraw = true; @@ -56,6 +56,8 @@ CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA pos.y += y; pos.w = pos.h = 0; + trimText(); + if(alignment == ETextAlignment::TOPLEFT) // causes issues for MIDDLE { pos.w = (int)graphics->fonts[font]->getStringWidth(visibleText().c_str()); @@ -81,6 +83,9 @@ void CLabel::setAutoRedraw(bool value) void CLabel::setText(const std::string & Txt) { text = Txt; + + trimText(); + if(autoRedraw) { if(background || !parent) @@ -90,6 +95,18 @@ void CLabel::setText(const std::string & Txt) } } +void CLabel::setMaxWidth(int width) +{ + maxWidth = width; +} + +void CLabel::trimText() +{ + if(maxWidth > 0) + while ((int)graphics->fonts[font]->getStringWidth(visibleText().c_str()) > maxWidth) + TextOperations::trimRightUnicode(text); +} + void CLabel::setColor(const ColorRGBA & Color) { color = Color; @@ -104,7 +121,7 @@ void CLabel::setColor(const ColorRGBA & Color) size_t CLabel::getWidth() { - return graphics->fonts[font]->getStringWidth(visibleText());; + return graphics->fonts[font]->getStringWidth(visibleText()); } CMultiLineLabel::CMultiLineLabel(Rect position, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) : @@ -352,6 +369,17 @@ void CTextBox::sliderMoved(int to) label->scrollTextTo(to); } +void CTextBox::trimToFit() +{ + if (slider) + return; + + pos.w = label->textSize.x; + pos.h = label->textSize.y; + label->pos.w = label->textSize.x; + label->pos.h = label->textSize.y; +} + void CTextBox::resize(Point newSize) { pos.w = newSize.x; @@ -375,18 +403,20 @@ void CTextBox::setText(const std::string & text) else if(slider) { // decrease width again if slider still used - label->pos.w = pos.w - 32; + label->pos.w = pos.w - 16; + assert(label->pos.w > 0); label->setText(text); slider->setAmount(label->textSize.y); } else if(label->textSize.y > label->pos.h) { // create slider and update widget - label->pos.w = pos.w - 32; + label->pos.w = pos.w - 16; + assert(label->pos.w > 0); label->setText(text); OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); - slider = std::make_shared(Point(pos.w - 32, 0), pos.h, std::bind(&CTextBox::sliderMoved, this, _1), + slider = std::make_shared(Point(pos.w - 16, 0), pos.h, std::bind(&CTextBox::sliderMoved, this, _1), label->pos.h, label->textSize.y, 0, Orientation::VERTICAL, CSlider::EStyle(sliderStyle)); slider->setScrollStep((int)graphics->fonts[label->font]->getLineHeight()); slider->setPanningStep(1); @@ -442,7 +472,7 @@ void CGStatusBar::clear() } CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, ETextAlignment Align, const ColorRGBA & Color) - : CLabel(background_->pos.x, background_->pos.y, Font, Align, Color, "") + : CLabel(background_->pos.x, background_->pos.y, Font, Align, Color, "", background_->pos.w) , enteringText(false) { addUsedEvents(LCLICK); @@ -532,14 +562,14 @@ Point CGStatusBar::getBorderSize() return Point(); } -CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput) - : CLabel(Pos.x, Pos.y, font, ETextAlignment::CENTER), - cb(CB), - CFocusable(std::make_shared(this)) +CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, ETextAlignment alignment, bool giveFocusToInput) + : CLabel(Pos.x, Pos.y, font, alignment), + cb(CB) { setRedrawParent(true); pos.h = Pos.h; pos.w = Pos.w; + maxWidth = Pos.w; background.reset(); addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT); @@ -550,11 +580,12 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB) - :cb(CB), CFocusable(std::make_shared(this)) + :cb(CB) { pos += Pos.topLeft(); pos.h = Pos.h; pos.w = Pos.w; + maxWidth = Pos.w; OBJ_CONSTRUCTION; background = std::make_shared(bgName, bgOffset.x, bgOffset.y); @@ -566,13 +597,13 @@ CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath } CTextInput::CTextInput(const Rect & Pos, std::shared_ptr srf) - :CFocusable(std::make_shared(this)) { pos += Pos.topLeft(); OBJ_CONSTRUCTION; background = std::make_shared(srf, Pos); pos.w = background->pos.w; pos.h = background->pos.h; + maxWidth = Pos.w; background->pos = pos; addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT); @@ -581,20 +612,15 @@ CTextInput::CTextInput(const Rect & Pos, std::shared_ptr srf) #endif } -std::atomic CKeyboardFocusListener::usageIndex(0); +std::atomic CFocusable::usageIndex(0); -CKeyboardFocusListener::CKeyboardFocusListener(CTextInput * textInput) - :textInput(textInput) +void CFocusable::focusGot() { -} - -void CKeyboardFocusListener::focusGot() -{ - GH.startTextInput(textInput->pos); + GH.startTextInput(pos); usageIndex++; } -void CKeyboardFocusListener::focusLost() +void CFocusable::focusLost() { if(0 == --usageIndex) { @@ -681,7 +707,7 @@ void CTextInput::textInputed(const std::string & enteredText) return; std::string oldText = text; - text += enteredText; + setText(getText() + enteredText); filters(text, oldText); if(text != oldText) @@ -747,12 +773,6 @@ void CTextInput::numberFilter(std::string & text, const std::string & oldText, i } CFocusable::CFocusable() - :CFocusable(std::make_shared()) -{ -} - -CFocusable::CFocusable(std::shared_ptr focusListener) - : focusListener(focusListener) { focus = false; focusables.push_back(this); @@ -763,7 +783,7 @@ CFocusable::~CFocusable() if(hasFocus()) { inputWithFocus = nullptr; - focusListener->focusLost(); + focusLost(); } focusables -= this; @@ -777,13 +797,13 @@ bool CFocusable::hasFocus() const void CFocusable::giveFocus() { focus = true; - focusListener->focusGot(); + focusGot(); redraw(); if(inputWithFocus) { inputWithFocus->focus = false; - inputWithFocus->focusListener->focusLost(); + inputWithFocus->focusLost(); inputWithFocus->redraw(); } @@ -799,6 +819,9 @@ void CFocusable::moveFocus() if(i == focusables.end()) i = focusables.begin(); + if (*i == this) + return; + if((*i)->isActive()) { (*i)->giveFocus(); @@ -812,7 +835,7 @@ void CFocusable::removeFocus() if(this == inputWithFocus) { focus = false; - focusListener->focusLost(); + focusLost(); redraw(); inputWithFocus = nullptr; diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 35e21245c..d9ce3dab4 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -43,9 +43,11 @@ class CLabel : public CTextContainer protected: Point getBorderSize() override; virtual std::string visibleText(); + virtual void trimText(); std::shared_ptr background; std::string text; + int maxWidth; bool autoRedraw; //whether control will redraw itself on setTxt public: @@ -53,11 +55,12 @@ public: std::string getText(); virtual void setAutoRedraw(bool option); virtual void setText(const std::string & Txt); + virtual void setMaxWidth(int width); virtual void setColor(const ColorRGBA & Color); size_t getWidth(); CLabel(int x = 0, int y = 0, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, - const ColorRGBA & Color = Colors::WHITE, const std::string & Text = ""); + const ColorRGBA & Color = Colors::WHITE, const std::string & Text = "", int maxWidth = 0); void showAll(Canvas & to) override; //shows statusbar (with current text) }; @@ -113,6 +116,9 @@ public: CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA & Color = Colors::WHITE); void resize(Point newSize); + /// Resizes text box to minimal size needed to fit current text + /// No effect if text is too large to fit and requires slider + void trimToFit(); void setText(const std::string & Txt); void sliderMoved(int to); }; @@ -160,25 +166,12 @@ public: void clear() override; void setEnteringMode(bool on) override; void setEnteredText(const std::string & text) override; - -}; - -class CFocusable; - -class IFocusListener -{ -public: - virtual void focusGot() {}; - virtual void focusLost() {}; - virtual ~IFocusListener() = default; }; /// UIElement which can get input focus class CFocusable : public virtual CIntObject { -private: - std::shared_ptr focusListener; - + static std::atomic usageIndex; public: bool focus; //only one focusable control can have focus at one moment @@ -187,45 +180,34 @@ public: void removeFocus(); //remove focus bool hasFocus() const; + void focusGot(); + void focusLost(); + static std::list focusables; //all existing objs static CFocusable * inputWithFocus; //who has focus now CFocusable(); - CFocusable(std::shared_ptr focusListener); ~CFocusable(); }; -class CTextInput; -class CKeyboardFocusListener : public IFocusListener -{ -private: - static std::atomic usageIndex; - CTextInput * textInput; - -public: - CKeyboardFocusListener(CTextInput * textInput); - void focusGot() override; - void focusLost() override; -}; - /// Text input box where players can enter text class CTextInput : public CLabel, public CFocusable { std::string newText; std::string helpBox; //for right-click help - + protected: std::string visibleText() override; public: - + CFunctionList cb; CFunctionList filters; void setText(const std::string & nText) override; void setText(const std::string & nText, bool callCb); void setHelpText(const std::string &); - CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput = true); + CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, ETextAlignment alignment, bool giveFocusToInput); CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList & CB); CTextInput(const Rect & Pos, std::shared_ptr srf); diff --git a/client/widgets/markets/CAltarArtifacts.cpp b/client/widgets/markets/CAltarArtifacts.cpp new file mode 100644 index 000000000..1c917ff70 --- /dev/null +++ b/client/widgets/markets/CAltarArtifacts.cpp @@ -0,0 +1,217 @@ +/* + * CAltarArtifacts.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "CAltarArtifacts.h" + +#include "../../gui/CGuiHandler.h" +#include "../../widgets/Buttons.h" +#include "../../widgets/TextControls.h" + +#include "../../CGameInfo.h" +#include "../../CPlayerInterface.h" + +#include "../../../CCallback.h" + +#include "../../../lib/networkPacks/ArtifactLocation.h" +#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/mapObjects/CGHeroInstance.h" +#include "../../../lib/mapObjects/CGMarket.h" + +CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) + : CTradeBase(market, hero) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + assert(market); + auto altarObj = dynamic_cast(market); + altarId = altarObj->id; + altarArtifacts = altarObj; + + deal = std::make_shared(dealButtonPos, AnimationPath::builtin("ALTSACR.DEF"), + CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); }); + labels.emplace_back(std::make_shared(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477])); + labels.emplace_back(std::make_shared(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478])); + selectedCost = std::make_shared(302, 500, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + selectedArt = std::make_shared(Point(280, 442)); + + sacrificeAllButton = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"), + CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this)); + sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty()); + + sacrificeBackpackButton = std::make_shared(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"), + CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this)); + sacrificeBackpackButton->block(hero->artifactsInBackpack.empty()); + + heroArts = std::make_shared(Point(-365, -11)); + heroArts->setHero(hero); + + int slotNum = 0; + for(auto & altarSlotPos : posSlotsAltar) + { + auto altarSlot = std::make_shared(Rect(altarSlotPos, Point(44, 44)), EType::ARTIFACT_PLACEHOLDER, -1, false, slotNum); + altarSlot->clickPressedCallback = std::bind(&CAltarArtifacts::onSlotClickPressed, this, _1, hRight); + altarSlot->subtitle.clear(); + items.front().emplace_back(altarSlot); + slotNum++; + } + + expForHero->setText(std::to_string(0)); + CTradeBase::deselect(); +}; + +TExpType CAltarArtifacts::calcExpAltarForHero() +{ + TExpType expOnAltar(0); + for(const auto & tradeSlot : tradeSlotsMap) + expOnAltar += calcExpCost(tradeSlot.first); + expForHero->setText(std::to_string(expOnAltar)); + return expOnAltar; +} + +void CAltarArtifacts::makeDeal() +{ + std::vector positions; + for(const auto & [artInst, altarSlot] : tradeSlotsMap) + { + positions.push_back(artInst->getId()); + } + LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector(), std::vector(), hero); + + tradeSlotsMap.clear(); + // The event for removing artifacts from the altar will not be triggered. Therefore, we clean the altar immediately. + for(auto item : items[0]) + { + item->setID(-1); + item->subtitle.clear(); + } + calcExpAltarForHero(); + deal->block(tradeSlotsMap.empty()); +} + +void CAltarArtifacts::sacrificeAll() +{ + LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, altarId, false, true, true); +} + +void CAltarArtifacts::sacrificeBackpack() +{ + LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, altarId, false, false, true); +} + +void CAltarArtifacts::setSelectedArtifact(const CArtifactInstance * art) +{ + selectedArt->setArtifact(art); + selectedCost->setText(art == nullptr ? "" : std::to_string(calcExpCost(art))); +} + +std::shared_ptr CAltarArtifacts::getAOHset() const +{ + return heroArts; +} + +ObjectInstanceID CAltarArtifacts::getObjId() const +{ + return altarId; +} + +void CAltarArtifacts::updateSlots() +{ + assert(altarArtifacts->artifactsInBackpack.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS); + assert(tradeSlotsMap.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS); + + auto slotsToAdd = tradeSlotsMap; + for(auto & altarSlot : items[0]) + if(altarSlot->id != -1) + { + if(tradeSlotsMap.find(altarSlot->getArtInstance()) == tradeSlotsMap.end()) + { + altarSlot->setID(-1); + altarSlot->subtitle.clear(); + } + else + { + slotsToAdd.erase(altarSlot->getArtInstance()); + } + } + + for(auto & tradeSlot : slotsToAdd) + { + assert(tradeSlot.second->id == -1); + assert(altarArtifacts->getSlotByInstance(tradeSlot.first) != ArtifactPosition::PRE_FIRST); + tradeSlot.second->setArtInstance(tradeSlot.first); + tradeSlot.second->subtitle = std::to_string(calcExpCost(tradeSlot.first)); + } + for(auto & slotInfo : altarArtifacts->artifactsInBackpack) + { + if(tradeSlotsMap.find(slotInfo.artifact) == tradeSlotsMap.end()) + { + for(auto & altarSlot : items[0]) + if(altarSlot->id == -1) + { + altarSlot->setArtInstance(slotInfo.artifact); + altarSlot->subtitle = std::to_string(calcExpCost(slotInfo.artifact)); + tradeSlotsMap.try_emplace(slotInfo.artifact, altarSlot); + break; + } + } + } + calcExpAltarForHero(); + deal->block(tradeSlotsMap.empty()); +} + +void CAltarArtifacts::putBackArtifacts() +{ + // TODO: If the backpack capacity limit is enabled, artifacts may remain on the altar. + // Perhaps should be erased in CGameHandler::objectVisitEnded if id of visited object will be available + if(!altarArtifacts->artifactsInBackpack.empty()) + LOCPLINT->cb->bulkMoveArtifacts(altarId, heroArts->getHero()->id, false, true, true); +} + +void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr & altarSlot, std::shared_ptr & hCurSlot) +{ + assert(altarSlot); + + if(const auto pickedArtInst = heroArts->getPickedArtifact()) + { + if(pickedArtInst->canBePutAt(altarArtifacts)) + { + if(pickedArtInst->artType->isTradable()) + { + if(altarSlot->id == -1) + tradeSlotsMap.try_emplace(pickedArtInst, altarSlot); + deal->block(false); + + LOCPLINT->cb->swapArtifacts(ArtifactLocation(heroArts->getHero()->id, ArtifactPosition::TRANSITION_POS), + ArtifactLocation(altarId, ArtifactPosition::ALTAR)); + } + else + { + logGlobal->warn("Cannot put special artifact on altar!"); + return; + } + } + } + else if(const CArtifactInstance * art = altarSlot->getArtInstance()) + { + const auto slot = altarArtifacts->getSlotByInstance(art); + assert(slot != ArtifactPosition::PRE_FIRST); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(altarId, slot), ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); + tradeSlotsMap.erase(art); + } +} + +TExpType CAltarArtifacts::calcExpCost(const CArtifactInstance * art) +{ + int dmp = 0; + int expOfArt = 0; + market->getOffer(art->getTypeId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP); + return hero->calculateXp(expOfArt); +} diff --git a/client/widgets/markets/CAltarArtifacts.h b/client/widgets/markets/CAltarArtifacts.h new file mode 100644 index 000000000..f2f63a50b --- /dev/null +++ b/client/widgets/markets/CAltarArtifacts.h @@ -0,0 +1,52 @@ +/* + * CAltarArtifacts.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../CArtifactsOfHeroAltar.h" +#include "CTradeBase.h" + +class CAltarArtifacts : public CExperienceAltar +{ +public: + CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero); + TExpType calcExpAltarForHero() override; + void makeDeal() override; + void sacrificeAll() override; + void sacrificeBackpack(); + void setSelectedArtifact(const CArtifactInstance * art); + std::shared_ptr getAOHset() const; + ObjectInstanceID getObjId() const; + void updateSlots(); + void putBackArtifacts(); + +private: + ObjectInstanceID altarId; + const CArtifactSet * altarArtifacts; + std::shared_ptr selectedArt; + std::shared_ptr selectedCost; + std::shared_ptr sacrificeBackpackButton; + std::shared_ptr heroArts; + std::map> tradeSlotsMap; + + const std::vector posSlotsAltar = + { + Point(317, 53), Point(371, 53), Point(425, 53), + Point(479, 53), Point(533, 53), Point(317, 123), + Point(371, 123), Point(425, 123), Point(479, 123), + Point(533, 123), Point(317, 193), Point(371, 193), + Point(425, 193), Point(479, 193), Point(533, 193), + Point(317, 263), Point(371, 263), Point(425, 263), + Point(479, 263), Point(533, 263), Point(398, 333), + Point(452, 333) + }; + + void onSlotClickPressed(const std::shared_ptr & altarSlot, std::shared_ptr & hCurSlot) override; + TExpType calcExpCost(const CArtifactInstance * art); +}; diff --git a/client/widgets/markets/CAltarCreatures.cpp b/client/widgets/markets/CAltarCreatures.cpp new file mode 100644 index 000000000..66161e2f7 --- /dev/null +++ b/client/widgets/markets/CAltarCreatures.cpp @@ -0,0 +1,251 @@ +/* + * CAltarCreatures.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "CAltarCreatures.h" + +#include "../../gui/CGuiHandler.h" +#include "../../widgets/Buttons.h" +#include "../../widgets/Slider.h" +#include "../../widgets/TextControls.h" + +#include "../../CGameInfo.h" +#include "../../CPlayerInterface.h" + +#include "../../../CCallback.h" + +#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/mapObjects/CGHeroInstance.h" +#include "../../../lib/mapObjects/CGMarket.h" + +CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero) + : CTradeBase(market, hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + deal = std::make_shared(dealButtonPos, AnimationPath::builtin("ALTSACR.DEF"), + CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();}); + labels.emplace_back(std::make_shared(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, + boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated()))); + labels.emplace_back(std::make_shared(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479])); + texts.emplace_back(std::make_unique(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + lSubtitle = std::make_shared(180, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + rSubtitle = std::make_shared(426, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + offerSlider = std::make_shared(Point(231, 481), 137, std::bind(&CAltarCreatures::onOfferSliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); + maxUnits = std::make_shared(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, offerSlider)); + + unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0); + expPerUnit.resize(GameConstants::ARMY_SIZE, 0); + sacrificeAllButton = std::make_shared( + Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this)); + + // Hero creatures panel + assert(leftTradePanel); + leftTradePanel->moveBy(Point(45, 110)); + leftTradePanel->updateSlotsCallback = std::bind(&CCreaturesSelling::updateSubtitle, this); + for(const auto & slot : leftTradePanel->slots) + slot->clickPressedCallback = [this](const std::shared_ptr & heroSlot) {CAltarCreatures::onSlotClickPressed(heroSlot, hLeft);}; + + // Altar creatures panel + rightTradePanel = std::make_shared([this](const std::shared_ptr & altarSlot) + { + CAltarCreatures::onSlotClickPressed(altarSlot, hRight); + }, leftTradePanel->slots); + rightTradePanel->moveBy(Point(334, 110)); + + leftTradePanel->deleteSlotsCheck = rightTradePanel->deleteSlotsCheck = std::bind(&CCreaturesSelling::slotDeletingCheck, this, _1); + readExpValues(); + expForHero->setText(std::to_string(0)); + CAltarCreatures::deselect(); +}; + +void CAltarCreatures::readExpValues() +{ + int dump; + for(auto heroSlot : leftTradePanel->slots) + { + if(heroSlot->id >= 0) + market->getOffer(heroSlot->id, 0, dump, expPerUnit[heroSlot->serial], EMarketMode::CREATURE_EXP); + } +} + +void CAltarCreatures::updateControls() +{ + int sliderAmount = 0; + if(hLeft) + { + std::optional lastSlot; + for(auto slot = SlotID(0); slot.num < GameConstants::ARMY_SIZE; slot++) + { + if(hero->getStackCount(slot) > unitsOnAltar[slot.num]) + { + if(lastSlot.has_value()) + { + lastSlot = std::nullopt; + break; + } + else + { + lastSlot = slot; + } + } + } + sliderAmount = hero->getStackCount(SlotID(hLeft->serial)); + if(lastSlot.has_value() && lastSlot.value() == SlotID(hLeft->serial)) + sliderAmount--; + } + offerSlider->setAmount(sliderAmount); + offerSlider->block(!offerSlider->getAmount()); + if(hLeft) + offerSlider->scrollTo(unitsOnAltar[hLeft->serial]); + maxUnits->block(offerSlider->getAmount() == 0); +} + +void CAltarCreatures::updateSubtitlesForSelected() +{ + if(hLeft) + lSubtitle->setText(std::to_string(offerSlider->getValue())); + else + lSubtitle->setText(""); + if(hRight) + rSubtitle->setText(hRight->subtitle); + else + rSubtitle->setText(""); +} + +void CAltarCreatures::updateSlots() +{ + rightTradePanel->deleteSlots(); + leftTradePanel->deleteSlots(); + assert(leftTradePanel->slots.size() == rightTradePanel->slots.size()); + readExpValues(); + leftTradePanel->updateSlots(); +} + +void CAltarCreatures::deselect() +{ + CTradeBase::deselect(); + offerSlider->block(true); + maxUnits->block(true); + updateSubtitlesForSelected(); +} + +TExpType CAltarCreatures::calcExpAltarForHero() +{ + TExpType expOnAltar(0); + auto oneUnitExp = expPerUnit.begin(); + for(const auto units : unitsOnAltar) + expOnAltar += *oneUnitExp++ * units; + auto resultExp = hero->calculateXp(expOnAltar); + expForHero->setText(std::to_string(resultExp)); + return resultExp; +} + +void CAltarCreatures::makeDeal() +{ + deselect(); + offerSlider->scrollTo(0); + expForHero->setText(std::to_string(0)); + + std::vector ids; + std::vector toSacrifice; + + for(int i = 0; i < unitsOnAltar.size(); i++) + { + if(unitsOnAltar[i]) + { + ids.push_back(SlotID(i)); + toSacrifice.push_back(unitsOnAltar[i]); + } + } + + LOCPLINT->cb->trade(market, EMarketMode::CREATURE_EXP, ids, {}, toSacrifice, hero); + + for(int & units : unitsOnAltar) + units = 0; + + for(auto heroSlot : rightTradePanel->slots) + { + heroSlot->setType(EType::CREATURE_PLACEHOLDER); + heroSlot->subtitle.clear(); + } +} + +void CAltarCreatures::sacrificeAll() +{ + std::optional lastSlot; + for(auto heroSlot : leftTradePanel->slots) + { + auto stackCount = hero->getStackCount(SlotID(heroSlot->serial)); + if(stackCount > unitsOnAltar[heroSlot->serial]) + { + if(!lastSlot.has_value()) + lastSlot = SlotID(heroSlot->serial); + unitsOnAltar[heroSlot->serial] = stackCount; + } + } + assert(lastSlot.has_value()); + unitsOnAltar[lastSlot.value().num]--; + + if(hRight) + offerSlider->scrollTo(unitsOnAltar[hRight->serial]); + for(auto altarSlot : rightTradePanel->slots) + updateAltarSlot(altarSlot); + updateSubtitlesForSelected(); + + deal->block(calcExpAltarForHero() == 0); +} + +void CAltarCreatures::updateAltarSlot(std::shared_ptr slot) +{ + auto units = unitsOnAltar[slot->serial]; + slot->setType(units > 0 ? EType::CREATURE : EType::CREATURE_PLACEHOLDER); + slot->subtitle = units > 0 ? + boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : ""; +} + +void CAltarCreatures::onOfferSliderMoved(int newVal) +{ + if(hLeft) + unitsOnAltar[hLeft->serial] = newVal; + if(hRight) + updateAltarSlot(hRight); + deal->block(calcExpAltarForHero() == 0); + updateControls(); + updateSubtitlesForSelected(); +} + +void CAltarCreatures::onSlotClickPressed(const std::shared_ptr & newSlot, std::shared_ptr & hCurSide) +{ + if(hCurSide == newSlot) + return; + + auto * oppositeSlot = &hLeft; + auto oppositePanel = leftTradePanel; + CTradeBase::onSlotClickPressed(newSlot, hCurSide); + if(hCurSide == hLeft) + { + oppositeSlot = &hRight; + oppositePanel = rightTradePanel; + } + std::shared_ptr oppositeNewSlot; + for(const auto & slot : oppositePanel->slots) + if(slot->serial == newSlot->serial) + { + oppositeNewSlot = slot; + break; + } + assert(oppositeNewSlot); + CTradeBase::onSlotClickPressed(oppositeNewSlot, *oppositeSlot); + updateControls(); + updateSubtitlesForSelected(); + redraw(); +} diff --git a/client/widgets/markets/CAltarCreatures.h b/client/widgets/markets/CAltarCreatures.h new file mode 100644 index 000000000..24ede7fa5 --- /dev/null +++ b/client/widgets/markets/CAltarCreatures.h @@ -0,0 +1,37 @@ +/* + * CAltarCreatures.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CTradeBase.h" + +class CAltarCreatures : public CExperienceAltar, public CCreaturesSelling +{ +public: + CAltarCreatures(const IMarket * market, const CGHeroInstance * hero); + void updateSlots(); + void deselect() override; + TExpType calcExpAltarForHero() override; + void makeDeal() override; + void sacrificeAll() override; + void updateAltarSlot(std::shared_ptr slot); + +private: + std::shared_ptr maxUnits; + std::vector unitsOnAltar; + std::vector expPerUnit; + std::shared_ptr lSubtitle; + std::shared_ptr rSubtitle; + + void readExpValues(); + void updateControls(); + void updateSubtitlesForSelected(); + void onOfferSliderMoved(int newVal); + void onSlotClickPressed(const std::shared_ptr & newSlot, std::shared_ptr & hCurSide) override; +}; diff --git a/client/widgets/markets/CTradeBase.cpp b/client/widgets/markets/CTradeBase.cpp new file mode 100644 index 000000000..62f792862 --- /dev/null +++ b/client/widgets/markets/CTradeBase.cpp @@ -0,0 +1,105 @@ +/* + * CTradeBase.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CTradeBase.h" + +#include "../MiscWidgets.h" + +#include "../../gui/CGuiHandler.h" +#include "../../widgets/Buttons.h" +#include "../../widgets/TextControls.h" + +#include "../../CGameInfo.h" + +#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/mapObjects/CGHeroInstance.h" + +CTradeBase::CTradeBase(const IMarket * market, const CGHeroInstance * hero) + : market(market) + , hero(hero) +{ +} + +void CTradeBase::removeItems(const std::set> & toRemove) +{ + for(auto item : toRemove) + removeItem(item); +} + +void CTradeBase::removeItem(std::shared_ptr item) +{ + rightTradePanel->slots.erase(std::remove(rightTradePanel->slots.begin(), rightTradePanel->slots.end(), item)); + + if(hRight == item) + hRight.reset(); +} + +void CTradeBase::getEmptySlots(std::set> & toRemove) +{ + for(auto item : leftTradePanel->slots) + if(!hero->getStackCount(SlotID(item->serial))) + toRemove.insert(item); +} + +void CTradeBase::deselect() +{ + if(hLeft) + hLeft->selectSlot(false); + if(hRight) + hRight->selectSlot(false); + hLeft = hRight = nullptr; + deal->block(true); +} + +void CTradeBase::onSlotClickPressed(const std::shared_ptr & newSlot, std::shared_ptr & hCurSlot) +{ + if(newSlot == hCurSlot) + return; + + if(hCurSlot) + hCurSlot->selectSlot(false); + hCurSlot = newSlot; + newSlot->selectSlot(true); +} + +CExperienceAltar::CExperienceAltar() +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + // Experience needed to reach next level + texts.emplace_back(std::make_shared(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + // Total experience on the Altar + texts.emplace_back(std::make_shared(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + expToLevel = std::make_shared(75, 477, FONT_SMALL, ETextAlignment::CENTER); + expForHero = std::make_shared(75, 545, FONT_SMALL, ETextAlignment::CENTER); +} + +CCreaturesSelling::CCreaturesSelling() +{ + assert(hero); + CreaturesPanel::slotsData slots; + for(auto slotId = SlotID(0); slotId.num < GameConstants::ARMY_SIZE; slotId++) + { + if(const auto & creature = hero->getCreature(slotId)) + slots.emplace_back(std::make_tuple(creature->getId(), slotId, hero->getStackCount(slotId))); + } + leftTradePanel = std::make_shared(nullptr, slots); +} + +bool CCreaturesSelling::slotDeletingCheck(const std::shared_ptr & slot) +{ + return hero->getStackCount(SlotID(slot->serial)) == 0 ? true : false; +} + +void CCreaturesSelling::updateSubtitle() +{ + for(auto & heroSlot : leftTradePanel->slots) + heroSlot->subtitle = std::to_string(this->hero->getStackCount(SlotID(heroSlot->serial))); +} diff --git a/client/widgets/markets/CTradeBase.h b/client/widgets/markets/CTradeBase.h new file mode 100644 index 000000000..1586ba16f --- /dev/null +++ b/client/widgets/markets/CTradeBase.h @@ -0,0 +1,74 @@ +/* + * CTradeBase.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "TradePanels.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IMarket; +class CGHeroInstance; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CSlider; + +class CTradeBase +{ +public: + const IMarket * market; + const CGHeroInstance * hero; + + //all indexes: 1 = left, 0 = right + std::array>, 2> items; + std::shared_ptr leftTradePanel; + std::shared_ptr rightTradePanel; + + //highlighted items (nullptr if no highlight) + std::shared_ptr hLeft; + std::shared_ptr hRight; + std::shared_ptr deal; + std::shared_ptr offerSlider; + + std::vector> labels; + std::vector> buttons; + std::vector> texts; + + CTradeBase(const IMarket * market, const CGHeroInstance * hero); + void removeItems(const std::set> & toRemove); + void removeItem(std::shared_ptr item); + void getEmptySlots(std::set> & toRemove); + virtual void makeDeal() = 0; + virtual void deselect(); + virtual void onSlotClickPressed(const std::shared_ptr & newSlot, std::shared_ptr & hCurSlot); +}; + +// Market subclasses +class CExperienceAltar : virtual public CTradeBase, virtual public CIntObject +{ +public: + std::shared_ptr expToLevel; + std::shared_ptr expForHero; + std::shared_ptr sacrificeAllButton; + const Point dealButtonPos = Point(269, 520); + + CExperienceAltar(); + virtual void sacrificeAll() = 0; + virtual TExpType calcExpAltarForHero() = 0; +}; + +class CCreaturesSelling : virtual public CTradeBase, virtual public CIntObject +{ +public: + CCreaturesSelling(); + bool slotDeletingCheck(const std::shared_ptr & slot); + void updateSubtitle(); +}; diff --git a/client/widgets/markets/TradePanels.cpp b/client/widgets/markets/TradePanels.cpp new file mode 100644 index 000000000..16a48eb2f --- /dev/null +++ b/client/widgets/markets/TradePanels.cpp @@ -0,0 +1,392 @@ +/* + * TradePanels.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "TradePanels.h" + +#include "../../gui/CGuiHandler.h" +#include "../../render/Canvas.h" +#include "../../widgets/TextControls.h" +#include "../../windows/InfoWindows.h" + +#include "../../CGameInfo.h" +#include "../../CPlayerInterface.h" + +#include "../../../CCallback.h" + +#include "../../../lib/CGeneralTextHandler.h" +#include "../../../lib/mapObjects/CGHeroInstance.h" + +CTradeableItem::CTradeableItem(const Rect & area, EType Type, int ID, bool Left, int Serial) + : SelectableSlot(area, Point(1, 1)) + , artInstance(nullptr) + , type(EType(-1)) // set to invalid, will be corrected in setType + , id(ID) + , serial(Serial) + , left(Left) + , downSelection(false) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + addUsedEvents(LCLICK); + addUsedEvents(HOVER); + addUsedEvents(SHOW_POPUP); + + setType(Type); + + this->pos.w = area.w; + this->pos.h = area.h; +} + +void CTradeableItem::setType(EType newType) +{ + if(type != newType) + { + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + type = newType; + + if(getIndex() < 0) + { + image = std::make_shared(getFilename(), 0); + image->disable(); + } + else + { + image = std::make_shared(getFilename(), getIndex()); + } + } +} + +void CTradeableItem::setID(int newID) +{ + if(id != newID) + { + id = newID; + if(image) + { + int index = getIndex(); + if(index < 0) + image->disable(); + else + { + image->enable(); + image->setFrame(index); + } + } + } +} + +AnimationPath CTradeableItem::getFilename() +{ + switch(type) + { + case EType::RESOURCE: + return AnimationPath::builtin("RESOURCE"); + case EType::PLAYER: + return AnimationPath::builtin("CREST58"); + case EType::ARTIFACT_TYPE: + case EType::ARTIFACT_PLACEHOLDER: + case EType::ARTIFACT_INSTANCE: + return AnimationPath::builtin("artifact"); + case EType::CREATURE: + return AnimationPath::builtin("TWCRPORT"); + default: + return {}; + } +} + +int CTradeableItem::getIndex() +{ + if(id < 0) + return -1; + + switch(type) + { + case EType::RESOURCE: + case EType::PLAYER: + return id; + case EType::ARTIFACT_TYPE: + case EType::ARTIFACT_INSTANCE: + case EType::ARTIFACT_PLACEHOLDER: + return CGI->artifacts()->getByIndex(id)->getIconIndex(); + case EType::CREATURE: + return CGI->creatures()->getByIndex(id)->getIconIndex(); + default: + return -1; + } +} + +void CTradeableItem::showAll(Canvas & to) +{ + Point posToBitmap; + Point posToSubCenter; + + switch(type) + { + case EType::RESOURCE: + posToBitmap = Point(19, 9); + posToSubCenter = Point(35, 57); + break; + case EType::CREATURE_PLACEHOLDER: + case EType::CREATURE: + posToSubCenter = Point(29, 77); + break; + case EType::PLAYER: + posToSubCenter = Point(31, 77); + break; + case EType::ARTIFACT_PLACEHOLDER: + case EType::ARTIFACT_INSTANCE: + posToSubCenter = Point(22, 51); + if (downSelection) + posToSubCenter.y += 8; + break; + case EType::ARTIFACT_TYPE: + posToSubCenter = Point(35, 57); + posToBitmap = Point(13, 0); + break; + } + + if(image) + { + image->moveTo(pos.topLeft() + posToBitmap); + CIntObject::showAll(to); + } + + to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle); +} + +void CTradeableItem::clickPressed(const Point & cursorPosition) +{ + if(clickPressedCallback) + clickPressedCallback(shared_from_this()); +} + +void CTradeableItem::showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to) +{ + Rect oldPos = pos; + std::string oldSub = subtitle; + downSelection = true; + + moveTo(dstPos); + subtitle = customSub; + showAll(to); + + downSelection = false; + moveTo(oldPos.topLeft()); + subtitle = oldSub; +} + +void CTradeableItem::hover(bool on) +{ + if(!on) + { + GH.statusbar()->clear(); + return; + } + + switch(type) + { + case EType::CREATURE: + case EType::CREATURE_PLACEHOLDER: + GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated())); + break; + case EType::ARTIFACT_PLACEHOLDER: + if(id < 0) + GH.statusbar()->write(CGI->generaltexth->zelp[582].first); + else + GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated()); + break; + } +} + +void CTradeableItem::showPopupWindow(const Point & cursorPosition) +{ + switch(type) + { + case EType::CREATURE: + case EType::CREATURE_PLACEHOLDER: + break; + case EType::ARTIFACT_TYPE: + case EType::ARTIFACT_PLACEHOLDER: + //TODO: it's would be better for market to contain actual CArtifactInstance and not just ids of certain artifact type so we can use getEffectiveDescription. + if (id >= 0) + CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated()); + break; + } +} + +std::string CTradeableItem::getName(int number) const +{ + switch(type) + { + case EType::PLAYER: + return CGI->generaltexth->capColors[id]; + case EType::RESOURCE: + return CGI->generaltexth->restypes[id]; + case EType::CREATURE: + if (number == 1) + return CGI->creh->objects[id]->getNameSingularTranslated(); + else + return CGI->creh->objects[id]->getNamePluralTranslated(); + case EType::ARTIFACT_TYPE: + case EType::ARTIFACT_INSTANCE: + return CGI->artifacts()->getByIndex(id)->getNameTranslated(); + } + logGlobal->error("Invalid trade item type: %d", (int)type); + return ""; +} + +const CArtifactInstance * CTradeableItem::getArtInstance() const +{ + switch(type) + { + case EType::ARTIFACT_PLACEHOLDER: + case EType::ARTIFACT_INSTANCE: + return artInstance; + default: + return nullptr; + } +} + +void CTradeableItem::setArtInstance(const CArtifactInstance * art) +{ + assert(type == EType::ARTIFACT_PLACEHOLDER || type == EType::ARTIFACT_INSTANCE); + artInstance = art; + if(art) + setID(art->getTypeId()); + else + setID(-1); +} + +void TradePanelBase::updateSlots() +{ + if(updateSlotsCallback) + updateSlotsCallback(); +} + +void TradePanelBase::deselect() +{ + for(const auto & slot : slots) + slot->selectSlot(false); +} + +void TradePanelBase::clearSubtitles() +{ + for(const auto & slot : slots) + slot->subtitle.clear(); +} + +void TradePanelBase::updateOffer(CTradeableItem & slot, int cost, int qty) +{ + slot.subtitle = std::to_string(qty); + if(cost != 1) + { + slot.subtitle.append("/"); + slot.subtitle.append(std::to_string(cost)); + } +} + +void TradePanelBase::deleteSlots() +{ + if(deleteSlotsCheck) + slots.erase(std::remove_if(slots.begin(), slots.end(), deleteSlotsCheck), slots.end()); +} + +ResourcesPanel::ResourcesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles) +{ + assert(resourcesForTrade.size() == slotsPos.size()); + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + for(const auto & res : resourcesForTrade) + { + auto slot = slots.emplace_back(std::make_shared(Rect(slotsPos[res.num], slotDimension), + EType::RESOURCE, res.num, true, res.num)); + slot->clickPressedCallback = clickPressedCallback; + slot->setSelectionWidth(selectionWidth); + } + updateSlotsCallback = updateSubtitles; +} + +ArtifactsPanel::ArtifactsPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles, + const std::vector & arts) +{ + assert(slotsForTrade == slotsPos.size()); + assert(slotsForTrade == arts.size()); + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + for(auto slotIdx = 0; slotIdx < slotsForTrade; slotIdx++) + { + auto artType = arts[slotIdx].getNum(); + if(artType != ArtifactID::NONE) + { + auto slot = slots.emplace_back(std::make_shared(Rect(slotsPos[slotIdx], slotDimension), + EType::ARTIFACT_TYPE, artType, false, slotIdx)); + slot->clickPressedCallback = clickPressedCallback; + slot->setSelectionWidth(selectionWidth); + } + } + updateSlotsCallback = updateSubtitles; +} + +PlayersPanel::PlayersPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback) +{ + assert(PlayerColor::PLAYER_LIMIT_I <= slotsPos.size() + 1); + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + std::vector players; + for(auto player = PlayerColor(0); player < PlayerColor::PLAYER_LIMIT_I; player++) + { + if(player != LOCPLINT->playerID && LOCPLINT->cb->getPlayerStatus(player) == EPlayerStatus::INGAME) + players.emplace_back(player); + } + + slots.resize(players.size()); + int slotNum = 0; + for(auto & slot : slots) + { + slot = std::make_shared(Rect(slotsPos[slotNum], slotDimension), EType::PLAYER, players[slotNum].num, false, slotNum); + slot->clickPressedCallback = clickPressedCallback; + slot->setSelectionWidth(selectionWidth); + slot->subtitle = CGI->generaltexth->capColors[players[slotNum].num]; + slotNum++; + } +} + +CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, const slotsData & initialSlots) +{ + assert(initialSlots.size() <= GameConstants::ARMY_SIZE); + assert(slotsPos.size() <= GameConstants::ARMY_SIZE); + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + for(const auto & [creatureId, slotId, creaturesNum] : initialSlots) + { + auto slot = slots.emplace_back(std::make_shared(Rect(slotsPos[slotId.num], slotDimension), + creaturesNum == 0 ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, creatureId.num, true, slotId)); + slot->clickPressedCallback = clickPressedCallback; + if(creaturesNum != 0) + slot->subtitle = std::to_string(creaturesNum); + slot->setSelectionWidth(selectionWidth); + } +} + +CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, + const std::vector> & srcSlots, bool emptySlots) +{ + assert(slots.size() <= GameConstants::ARMY_SIZE); + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + for(const auto & srcSlot : srcSlots) + { + auto slot = slots.emplace_back(std::make_shared(Rect(slotsPos[srcSlot->serial], srcSlot->pos.dimensions()), + emptySlots ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, srcSlot->id, true, srcSlot->serial)); + slot->clickPressedCallback = clickPressedCallback; + slot->subtitle = emptySlots ? "" : srcSlot->subtitle; + slot->setSelectionWidth(selectionWidth); + } +} diff --git a/client/widgets/markets/TradePanels.h b/client/widgets/markets/TradePanels.h new file mode 100644 index 000000000..23bc1b336 --- /dev/null +++ b/client/widgets/markets/TradePanels.h @@ -0,0 +1,141 @@ +/* + * TradePanels.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../MiscWidgets.h" +#include "../Images.h" + +#include "../../../lib/networkPacks/TradeItem.h" + +enum class EType +{ + RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE +}; + +class CTradeableItem : public SelectableSlot, public std::enable_shared_from_this +{ +public: + std::shared_ptr image; + AnimationPath getFilename(); + int getIndex(); + using ClickPressedFunctor = std::function&)>; + + const CArtifactInstance * artInstance; //holds ptr to artifact instance id type artifact + EType type; + int id; + const int serial; + const bool left; + std::string subtitle; + ClickPressedFunctor clickPressedCallback; + + void setType(EType newType); + void setID(int newID); + + const CArtifactInstance * getArtInstance() const; + void setArtInstance(const CArtifactInstance * art); + + bool downSelection; + + void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to); + + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + void showAll(Canvas & to) override; + void clickPressed(const Point & cursorPosition) override; + std::string getName(int number = -1) const; + CTradeableItem(const Rect & area, EType Type, int ID, bool Left, int Serial); +}; + +class TradePanelBase : public CIntObject +{ +public: + using UpdateSlotsFunctor = std::function; + using DeleteSlotsCheck = std::function&)>; + + std::vector> slots; + UpdateSlotsFunctor updateSlotsCallback; + DeleteSlotsCheck deleteSlotsCheck; + std::shared_ptr selected; + const int selectionWidth = 2; + + virtual void updateSlots(); + virtual void deselect(); + virtual void clearSubtitles(); + void updateOffer(CTradeableItem & slot, int, int); + void deleteSlots(); +}; + +class ResourcesPanel : public TradePanelBase +{ + const std::vector resourcesForTrade = + { + GameResID::WOOD, GameResID::MERCURY, GameResID::ORE, + GameResID::SULFUR, GameResID::CRYSTAL, GameResID::GEMS, + GameResID::GOLD + }; + const std::vector slotsPos = + { + Point(0, 0), Point(83, 0), Point(166, 0), + Point(0, 79), Point(83, 79), Point(166, 79), + Point(83, 158) + }; + const Point slotDimension = Point(69, 66); + +public: + ResourcesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles); +}; + +class ArtifactsPanel : public TradePanelBase +{ + const std::vector slotsPos = + { + Point(0, 0), Point(83, 0), Point(166, 0), + Point(0, 79), Point(83, 79), Point(166, 79), + Point(83, 158) + }; + const size_t slotsForTrade = 7; + const Point slotDimension = Point(69, 66); + +public: + ArtifactsPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles, + const std::vector & arts); +}; + +class PlayersPanel : public TradePanelBase +{ + const std::vector slotsPos = + { + Point(0, 0), Point(83, 0), Point(166, 0), + Point(0, 118), Point(83, 118), Point(166, 118), + Point(83, 236) + }; + const Point slotDimension = Point(58, 64); + +public: + explicit PlayersPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback); +}; + +class CreaturesPanel : public TradePanelBase +{ + const std::vector slotsPos = + { + Point(0, 0), Point(83, 0), Point(166, 0), + Point(0, 98), Point(83, 98), Point(166, 98), + Point(83, 196) + }; + const Point slotDimension = Point(58, 64); + +public: + using slotsData = std::vector>; + + CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, const slotsData & initialSlots); + CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, + const std::vector> & srcSlots, bool emptySlots = true); +}; diff --git a/client/windows/CAltarWindow.cpp b/client/windows/CAltarWindow.cpp new file mode 100644 index 000000000..b9112aaca --- /dev/null +++ b/client/windows/CAltarWindow.cpp @@ -0,0 +1,147 @@ +/* + * CAltarWindow.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "CAltarWindow.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../gui/Shortcut.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" + +#include "../CGameInfo.h" + +#include "../lib/networkPacks/ArtifactLocation.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +CAltarWindow::CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin(mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp")) + , hero(hero) + , windowClosedCallback(onWindowClosed) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + assert(mode == EMarketMode::ARTIFACT_EXP || mode == EMarketMode::CREATURE_EXP); + if(mode == EMarketMode::ARTIFACT_EXP) + createAltarArtifacts(market, hero); + else if(mode == EMarketMode::CREATURE_EXP) + createAltarCreatures(market, hero); + + updateExpToLevel(); + statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); +} + +void CAltarWindow::updateExpToLevel() +{ + altar->expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(altar->hero->exp) + 1) - altar->hero->exp)); +} + +void CAltarWindow::updateGarrisons() +{ + if(auto altarCreatures = std::static_pointer_cast(altar)) + altarCreatures->updateSlots(); +} + +bool CAltarWindow::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + +const CGHeroInstance * CAltarWindow::getHero() const +{ + return hero; +} + +void CAltarWindow::close() +{ + if(windowClosedCallback) + windowClosedCallback(); + + CWindowObject::close(); +} + +void CAltarWindow::createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + background = createBg(ImagePath::builtin("ALTRART2.bmp"), PLAYER_COLORED); + + auto altarArtifacts = std::make_shared(market, hero); + altar = altarArtifacts; + artSets.clear(); + addSetAndCallbacks(altarArtifacts->getAOHset()); altarArtifacts->putBackArtifacts(); + + changeModeButton = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"), + CGI->generaltexth->zelp[572], std::bind(&CAltarWindow::createAltarCreatures, this, market, hero)); + if(altar->hero->getAlignment() == EAlignment::GOOD) + changeModeButton->block(true); + quitButton = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), + CGI->generaltexth->zelp[568], [this, altarArtifacts]() + { + altarArtifacts->putBackArtifacts(); + CAltarWindow::close(); + }, EShortcut::GLOBAL_RETURN); + altar->setRedrawParent(true); + redraw(); +} + +void CAltarWindow::createAltarCreatures(const IMarket * market, const CGHeroInstance * hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + background = createBg(ImagePath::builtin("ALTARMON.bmp"), PLAYER_COLORED); + + altar = std::make_shared(market, hero); + + changeModeButton = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTART.DEF"), + CGI->generaltexth->zelp[580], std::bind(&CAltarWindow::createAltarArtifacts, this, market, hero)); + if(altar->hero->getAlignment() == EAlignment::EVIL) + changeModeButton->block(true); + quitButton = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), + CGI->generaltexth->zelp[568], std::bind(&CAltarWindow::close, this), EShortcut::GLOBAL_RETURN); + altar->setRedrawParent(true); + redraw(); +} + +void CAltarWindow::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) +{ + if(!getState().has_value()) + return; + + if(auto altarArtifacts = std::static_pointer_cast(altar)) + { + if(srcLoc.artHolder == altarArtifacts->getObjId() || destLoc.artHolder == altarArtifacts->getObjId()) + altarArtifacts->updateSlots(); + + if(const auto pickedArt = getPickedArtifact()) + altarArtifacts->setSelectedArtifact(pickedArt); + else + altarArtifacts->setSelectedArtifact(nullptr); + } + CWindowWithArtifacts::artifactMoved(srcLoc, destLoc, withRedraw); +} + +void CAltarWindow::showAll(Canvas & to) +{ + // This func is temporary workaround for compliance with CTradeWindow + CWindowObject::showAll(to); + + if(altar->hRight) + { + altar->hRight->showAllAt(altar->pos.topLeft() + Point(396, 423), "", to); + } + if(altar->hLeft) + { + altar->hLeft->showAllAt(altar->pos.topLeft() + Point(150, 423), "", to); + } +} diff --git a/client/windows/CAltarWindow.h b/client/windows/CAltarWindow.h new file mode 100644 index 000000000..3084ebf35 --- /dev/null +++ b/client/windows/CAltarWindow.h @@ -0,0 +1,40 @@ +/* + * CAltarWindow.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../widgets/markets/CAltarArtifacts.h" +#include "../widgets/markets/CAltarCreatures.h" +#include "../widgets/CWindowWithArtifacts.h" +#include "CWindowObject.h" + +class CAltarWindow : public CWindowObject, public CWindowWithArtifacts, public IGarrisonHolder +{ +public: + CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode); + void updateExpToLevel(); + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + const CGHeroInstance * getHero() const; + void close() override; + + void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override; + void showAll(Canvas & to) override; + +private: + const CGHeroInstance * hero; + std::shared_ptr altar; + std::shared_ptr changeModeButton; + std::shared_ptr quitButton; + std::function windowClosedCallback; + std::shared_ptr statusBar; + + void createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero); + void createAltarCreatures(const IMarket * market, const CGHeroInstance * hero); +}; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index a0bc94cc8..02eb5760a 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -34,6 +34,7 @@ #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" +#include "../render/CAnimation.h" #include "../render/ColorFilter.h" #include "../adventureMap/AdventureMapInterface.h" #include "../adventureMap/CList.h" @@ -157,7 +158,7 @@ void CBuildingRect::showPopupWindow(const Point & cursorPosition) if (bid < BuildingID::DWELL_FIRST) { CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()), - std::make_shared(CComponent::building, bld->town->faction->getIndex(), bld->bid)); + std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(bld->town->faction->getId(), bld->bid))); } else { @@ -426,7 +427,11 @@ void CHeroGSlot::clickPressed(const Point & cursorPosition) if(hero && isSelected()) { setHighlight(false); - LOCPLINT->openHeroWindow(hero); + + if(other->hero) + LOCPLINT->showHeroExchange(hero->id, other->hero->id); + else + LOCPLINT->openHeroWindow(hero); } else if(other->hero && other->isSelected()) { @@ -514,14 +519,6 @@ void HeroSlots::update() visitingHero->set(town->visitingHero); } -void HeroSlots::splitClicked() -{ - if(!!town->visitingHero && town->garrisonHero && (visitingHero->isSelected() || garrisonedHero->isSelected())) - { - LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id); - } -} - void HeroSlots::swapArmies() { bool allow = true; @@ -561,27 +558,6 @@ void HeroSlots::swapArmies() LOCPLINT->cb->swapGarrisonHero(town); } -class SORTHELP -{ -public: - bool operator() (const CIntObject * a, const CIntObject * b) - { - auto b1 = dynamic_cast(a); - auto b2 = dynamic_cast(b); - - if(!b1 && !b2) - return intptr_t(a) < intptr_t(b); - if(b1 && !b2) - return false; - if(!b1 && b2) - return true; - - return (*b1)<(*b2); - } -}; - -SORTHELP buildSorter; - CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): town(Town), selectedBuilding(nullptr) @@ -649,6 +625,21 @@ void CCastleBuildings::recreate() buildings.push_back(std::make_shared(this, town, toAdd)); } + auto const & buildSorter = [] (const CIntObject * a, const CIntObject * b) + { + auto b1 = dynamic_cast(a); + auto b2 = dynamic_cast(b); + + if(!b1 && !b2) + return intptr_t(a) < intptr_t(b); + if(b1 && !b2) + return false; + if(!b1 && b2) + return true; + + return (*b1)<(*b2); + }; + boost::sort(children, buildSorter); //TODO: create building in blit order } @@ -860,7 +851,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID) void CCastleBuildings::enterBuilding(BuildingID building) { - std::vector> comps(1, std::make_shared(CComponent::building, town->subID, building)); + std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building))); LOCPLINT->showInfoDialog( town->town->buildings.find(building)->second->getDescriptionTranslated(), comps); } @@ -872,7 +863,7 @@ void CCastleBuildings::enterCastleGate() return;//only visiting hero can use castle gates } std::vector availableTowns; - std::vector Towns = LOCPLINT->cb->getTownsInfo(true); + std::vector Towns = LOCPLINT->localState->getOwnedTowns(); for(auto & Town : Towns) { const CGTownInstance *t = Town; @@ -883,9 +874,22 @@ void CCastleBuildings::enterCastleGate() availableTowns.push_back(t->id.getNum());//add to the list } } + + std::vector> images; + for(auto & t : Towns) + { + if(!settings["general"]["enableUiEnhancements"].Bool()) + break; + std::shared_ptr a = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITPA")); + a->preload(); + images.push_back(a->getImage(t->town->clientInfo.icons[t->hasFort()][false] + 2)->scaleFast(Point(35, 23))); + } + auto gateIcon = std::make_shared(town->town->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window - GH.windows().createAndPushWindow(availableTowns, gateIcon, CGI->generaltexth->jktexts[40], - CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1)); + auto wnd = std::make_shared(availableTowns, gateIcon, CGI->generaltexth->jktexts[40], + CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1), 0, images); + wnd->onPopup = [Towns](int index) { CRClickPopup::createAndPush(Towns[index], GH.getCursorPosition()); }; + GH.windows().pushWindow(wnd); } void CCastleBuildings::enterDwelling(int level) @@ -901,7 +905,7 @@ void CCastleBuildings::enterDwelling(int level) { LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, -87); + GH.windows().createAndPushWindow(town, level, town->getUpperArmy(), recruitCb, nullptr, -87); } void CCastleBuildings::enterToTheQuickRecruitmentWindow() @@ -920,7 +924,7 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow() void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) { - std::vector> comps(1, std::make_shared(CComponent::building,town->subID,building)); + std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building))); std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated(); std::string hasNotProduced; std::string hasProduced; @@ -967,11 +971,7 @@ void CCastleBuildings::enterMagesGuild() if(hero && !hero->hasSpellbook()) //hero doesn't have spellbok { - const StartInfo *si = LOCPLINT->cb->getStartInfo(); - // it would be nice to find a way to move this hack to config/mapOverrides.json - if(si && si->campState && // We're in campaign, - (si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian", - (hero->subID == 45)) // and the hero is Yog (based on Solmyr) + if(hero->isCampaignYog()) { // "Yog has given up magic in all its forms..." LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]); @@ -986,7 +986,7 @@ void CCastleBuildings::enterMagesGuild() CFunctionList onYes = [this](){ openMagesGuild(); }; CFunctionList onNo = onYes; onYes += [hero](){ LOCPLINT->cb->buyArtifact(hero, ArtifactID::SPELLBOOK); }; - std::vector> components(1, std::make_shared(CComponent::artifact,ArtifactID::SPELLBOOK,0)); + std::vector> components(1, std::make_shared(ComponentType::ARTIFACT, ArtifactID(ArtifactID::SPELLBOOK))); LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[214], onYes, onNo, components); } @@ -1110,7 +1110,7 @@ void CCreaInfo::clickPressed(const Point & cursorPosition) { LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, offset); + GH.windows().createAndPushWindow(town, level, town->getUpperArmy(), recruitCb, nullptr, offset); } std::string CCreaInfo::genGrowthText() @@ -1129,7 +1129,7 @@ void CCreaInfo::showPopupWindow(const Point & cursorPosition) if (showAvailable) GH.windows().createAndPushWindow(GH.screenDimensions().x / 2, GH.screenDimensions().y / 2, town, level); else - CRClickPopup::createAndPush(genGrowthText(), std::make_shared(CComponent::creature, creature->getId())); + CRClickPopup::createAndPush(genGrowthText(), std::make_shared(ComponentType::CREATURE, creature->getId())); } bool CCreaInfo::getShowAvailable() @@ -1180,7 +1180,7 @@ void CTownInfo::showPopupWindow(const Point & cursorPosition) { if(building) { - auto c = std::make_shared(CComponent::building, building->town->faction->getIndex(), building->bid); + auto c = std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(building->town->faction->getId(), building->bid)); CRClickPopup::createAndPush(CInfoWindow::genText(building->getNameTranslated(), building->getDescriptionTranslated()), c); } } @@ -1213,11 +1213,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst exit = std::make_shared(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); exit->setImageOrder(4, 5, 6, 7); - auto split = std::make_shared(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]() - { - garr->splitClick(); - heroes->splitClicked(); - }); + auto split = std::make_shared(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [this]() { garr->splitClick(); }); garr->addSplitBtn(split); Rect barRect(9, 182, 732, 18); @@ -1262,6 +1258,11 @@ void CCastleInterface::updateGarrisons() redraw(); } +bool CCastleInterface::holdsGarrison(const CArmedInstance * army) +{ + return army == town || army == town->getUpperArmy() || army == town->visitingHero; +} + void CCastleInterface::close() { if(town->tempOwner == LOCPLINT->playerID) //we may have opened window for an allied town @@ -1323,21 +1324,25 @@ void CCastleInterface::recreateIcons() hall = std::make_shared(80, 413, town, true); fort = std::make_shared(122, 413, town, false); - fastTownHall = std::make_shared(Point(80, 413), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&](){ builds->enterTownHall(); }); - fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); - fastTownHall->setAnimateLonelyFrame(true); + fastTownHall = std::make_shared(Point(80, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterTownHall(); }); + fastTownHall->setOverlay(std::make_shared(AnimationPath::builtin("ITMTL"), town->hallLevel())); - fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); - fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); - fastArmyPurchase->setAnimateLonelyFrame(true); + int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1; + fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase->setOverlay(std::make_shared(AnimationPath::builtin("itmcl"), imageIndex)); fastMarket = std::make_shared(Rect(163, 410, 64, 42), [&]() { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) { - if (town->getOwner() == LOCPLINT->playerID) - GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); }); fastTavern = std::make_shared(Rect(15, 387, 58, 64), [&]() @@ -1406,11 +1411,11 @@ CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * state = LOCPLINT->cb->canBuildStructure(town, building->bid); - static int panelIndex[12] = + constexpr std::array panelIndex = { 3, 3, 3, 0, 0, 2, 2, 1, 2, 2, 3, 3 }; - static int iconIndex[12] = + constexpr std::array iconIndex = { -1, -1, -1, 0, 0, 1, 2, -1, 1, 1, -1, -1 }; @@ -1419,7 +1424,7 @@ CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * header = std::make_shared(AnimationPath::builtin("TPTHBAR"), panelIndex[static_cast(state)], 0, 1, 73); if(iconIndex[static_cast(state)] >=0) mark = std::make_shared(AnimationPath::builtin("TPTHCHK"), iconIndex[static_cast(state)], 0, 136, 56); - name = std::make_shared(75, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated()); + name = std::make_shared(78, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated(), 150); //todo: add support for all possible states if(state >= EBuildingState::BUILDING_ERROR) @@ -1495,8 +1500,8 @@ CHallInterface::CHallInterface(const CGTownInstance * Town): } } } - int posX = pos.w/2 - (int)boxList[row].size()*154/2 - ((int)boxList[row].size()-1)*20 + 194*(int)col, - posY = 35 + 104*(int)row; + int posX = pos.w/2 - (int)boxList[row].size()*154/2 - ((int)boxList[row].size()-1)*20 + 194*(int)col; + int posY = 35 + 104*(int)row; if(building) boxes[row].push_back(std::make_shared(posX, posY, town, building)); @@ -1526,15 +1531,24 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin //Create components for all required resources std::vector> components; - for(int i = 0; iresources[i]) { - std::string text = std::to_string(building->resources[i]); - int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i]; - if(resAfterBuy < 0 && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) - text = "{H3Red|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text; - components.push_back(std::make_shared(CComponent::resource, i, text, CComponent::small)); + MetaString message; + int resourceAmount = LOCPLINT->cb->getResourceAmount(i); + bool canAfford = resourceAmount >= building->resources[i]; + + if(!canAfford && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) + { + message.appendRawString("{H3Red|%d}/%d"); + message.replaceNumber(resourceAmount); + } + else + message.appendRawString("%d"); + + message.replaceNumber(building->resources[i]); + components.push_back(std::make_shared(ComponentType::RESOURCE, i, message.toString(), CComponent::small)); } } @@ -1749,7 +1763,7 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * if(getMyBuilding() != nullptr) { buildingIcon = std::make_shared(town->town->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); - buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated()); + buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated(), 152); if(vstd::contains(town->builtBuildings, getMyBuilding()->bid)) { @@ -1763,7 +1777,7 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * { hoverText = boost::str(boost::format(CGI->generaltexth->tcommands[21]) % getMyCreature()->getNamePluralTranslated()); new CCreaturePic(159, 4, getMyCreature(), false); - new CLabel(78, 11, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyCreature()->getNamePluralTranslated()); + new CLabel(78, 11, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyCreature()->getNamePluralTranslated(), 152); Rect sizes(287, 4, 96, 18); values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[190], CGI->generaltexth->fcommands[0], getMyCreature()->getAttack(false))); @@ -1889,12 +1903,12 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition) { - LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); + LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); } void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition) { - CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); + CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); } void CMageGuildScreen::Scroll::hover(bool on) @@ -1935,7 +1949,7 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art cancelText.appendTextID("core.genrltxt.596"); cancelText.replaceTextID(creature->getNameSingularTextID()); - std::string costString = std::to_string(aid.toArtifact(CGI->artifacts())->getPrice()); + std::string costString = std::to_string(aid.toEntity(CGI)->getPrice()); title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 2cc86700c..32c1d426c 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -133,7 +133,6 @@ public: HeroSlots(const CGTownInstance * town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty); ~HeroSlots(); - void splitClicked(); //for hero meeting only (splitting stacks is handled by garrison int) void update(); void swapArmies(); //exchange garrisoned and visiting heroes or move hero to\from garrison }; @@ -227,7 +226,6 @@ class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder std::shared_ptr fort; std::shared_ptr exit; - std::shared_ptr split; std::shared_ptr fastTownHall; std::shared_ptr fastArmyPurchase; std::shared_ptr fastMarket; @@ -249,7 +247,8 @@ public: CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from = nullptr); ~CCastleInterface(); - virtual void updateGarrisons() override; + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void castleTeleport(int where); void townChange(); diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index bece268cf..65b457733 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -225,10 +225,11 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds." boost::replace_first(spellText, "%s", spell->getNameTranslated()); //FIXME: support permanent duration - int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain; + int duration = battleStack->getFirstBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain; boost::replace_first(spellText, "%d", std::to_string(duration)); spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); + labels.push_back(std::make_shared(firstPos.x + offset.x * printed + 46, firstPos.y + offset.y * printed + 36, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(duration))); clickableAreas.push_back(std::make_shared(Rect(firstPos + offset * printed, Point(50, 38)), spellText, spellText)); if(++printed >= 8) // interface limit reached break; @@ -325,7 +326,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) std::vector> resComps; for(TResources::nziterator i(totalCost); i.valid(); i++) { - resComps.push_back(std::make_shared(CComponent::resource, i->resType, (int)i->resVal)); + resComps.push_back(std::make_shared(ComponentType::RESOURCE, i->resType, i->resVal)); } if(LOCPLINT->cb->getResourceAmount().canAfford(totalCost)) @@ -339,7 +340,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) }; auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CGI->generaltexth->zelp[446], onClick); - upgradeBtn->addOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); + upgradeBtn->setOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); if(buttonsToCreate == 1) // single upgrade avaialbe upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; @@ -365,7 +366,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex]; parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CButton::tooltipLocalized(tooltipText), onSwitch); - parent->switchButtons[buttonIndex]->addOverlay(std::make_shared(AnimationPath::builtin("stackWindow/switchModeIcons"), buttonIndex)); + parent->switchButtons[buttonIndex]->setOverlay(std::make_shared(AnimationPath::builtin("stackWindow/switchModeIcons"), buttonIndex)); } parent->switchButtons[parent->activeTab]->disable(); } @@ -542,7 +543,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(battleStack->isShooter()), battleStack->getDefense(battleStack->isShooter())); addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(battleStack->isShooter()) * dmgMultiply, battleStack->getMaxDamage(battleStack->isShooter()) * dmgMultiply); addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), battleStack->getMaxHealth()); - addStatLabel(EStat::SPEED, parent->info->creature->speed(), battleStack->speed()); + addStatLabel(EStat::SPEED, parent->info->creature->getMovementRange(), battleStack->getMovementRange()); if(battleStack->isShooter()) addStatLabel(EStat::SHOTS, battleStack->shots.total(), battleStack->shots.available()); @@ -562,7 +563,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(shooter), parent->info->stackNode->getDefense(shooter)); addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(shooter) * dmgMultiply, parent->info->stackNode->getMaxDamage(shooter) * dmgMultiply); addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), parent->info->stackNode->getMaxHealth()); - addStatLabel(EStat::SPEED, parent->info->creature->speed(), parent->info->stackNode->speed()); + addStatLabel(EStat::SPEED, parent->info->creature->getMovementRange(), parent->info->stackNode->getMovementRange()); if(shooter) addStatLabel(EStat::SHOTS, parent->info->stackNode->valOfBonuses(BonusType::SHOTS)); @@ -582,10 +583,10 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s const CCommanderInstance * commander = parent->info->commander; expRankIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), 4, 0, pos.x, pos.y); - auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), CComponent::experience); + auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), ComponentType::EXPERIENCE); expArea = area; area->text = CGI->generaltexth->allTexts[2]; - area->bonusValue = commander->getExpRank(); + area->component.value = commander->getExpRank(); boost::replace_first(area->text, "%d", std::to_string(commander->getExpRank())); boost::replace_first(area->text, "%d", std::to_string(CGI->heroh->reqExp(commander->getExpRank() + 1))); boost::replace_first(area->text, "%d", std::to_string(commander->experience)); @@ -611,8 +612,8 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s if(art) { parent->stackArtifactIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), art->artType->getIconIndex(), 0, pos.x, pos.y); - parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), CComponent::artifact); - parent->stackArtifactHelp->type = art->artType->getId(); + parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), ComponentType::ARTIFACT); + parent->stackArtifactHelp->component.subType = art->artType->getId(); if(parent->info->owner) { @@ -665,6 +666,7 @@ CStackWindow::CStackWindow(const CStack * stack, bool popup) { info->stack = stack; info->stackNode = stack->base; + info->commander = dynamic_cast(stack->base); info->creature = stack->unitType(); info->creatureCount = stack->getCount(); info->popupWindow = popup; @@ -763,7 +765,8 @@ void CStackWindow::init() void CStackWindow::initBonusesList() { - BonusList output, input; + BonusList output; + BonusList input; input = *(info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all)); while(!input.empty()) @@ -974,11 +977,12 @@ void CStackWindow::removeStackArtifact(ArtifactPosition pos) const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId()); if(slot != ArtifactPosition::PRE_FIRST) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner, slot)); + auto artLoc = ArtifactLocation(info->owner->id, pos); + artLoc.creature = info->stackNode->armyObj->findStack(info->stackNode); + LOCPLINT->cb->swapArtifacts(artLoc, ArtifactLocation(info->owner->id, slot)); stackArtifactButton.reset(); stackArtifactHelp.reset(); stackArtifactIcon.reset(); redraw(); } } - diff --git a/client/windows/CCreatureWindow.h b/client/windows/CCreatureWindow.h index b20a1ef39..de5997764 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -72,6 +72,7 @@ class CStackWindow : public CWindowObject { std::vector> spellIcons; std::vector> clickableAreas; + std::vector> labels; public: ActiveSpellsSection(CStackWindow * owner, int yOffset); }; diff --git a/client/windows/CHeroBackpackWindow.cpp b/client/windows/CHeroBackpackWindow.cpp index a4ff6d1c8..692863cad 100644 --- a/client/windows/CHeroBackpackWindow.cpp +++ b/client/windows/CHeroBackpackWindow.cpp @@ -15,36 +15,78 @@ #include "../widgets/Buttons.h" #include "../widgets/Images.h" +#include "../widgets/TextControls.h" #include "CMessage.h" #include "render/Canvas.h" #include "CPlayerInterface.h" CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero) - : CWindowObject((EOptions)0) + : CStatusbarWindow(0) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - stretchedBackground = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 410, 425)); - - arts = std::make_shared(Point(windowMargin, windowMargin)); - arts->setHero(hero); + stretchedBackground = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 0, 0)); + arts = std::make_shared(); + arts->moveBy(Point(windowMargin, windowMargin)); addSetAndCallbacks(arts); - + arts->setHero(hero); addCloseCallback(std::bind(&CHeroBackpackWindow::close, this)); + quitButton = std::make_shared(Point(), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), + [this]() { WindowBase::close(); }, EShortcut::GLOBAL_RETURN); + pos.w = stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin; + pos.h = stretchedBackground->pos.h = arts->pos.h + quitButton->pos.h + 3 * windowMargin; + quitButton->moveTo(Point(pos.x + pos.w / 2 - quitButton->pos.w / 2, pos.y + arts->pos.h + 2 * windowMargin)); + statusbar = CGStatusBar::create(0, pos.h, ImagePath::builtin("ADROLLVR.bmp"), pos.w); + pos.h += statusbar->pos.h; - quitButton = std::make_shared(Point(), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN); - - stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin; - stretchedBackground->pos.h = arts->pos.h + quitButton->pos.h + 3 * windowMargin; - pos.w = stretchedBackground->pos.w; - pos.h = stretchedBackground->pos.h; center(); - - quitButton->moveBy(Point(GH.screenDimensions().x / 2 - quitButton->pos.w / 2 - quitButton->pos.x, arts->pos.h + 2 * windowMargin)); } void CHeroBackpackWindow::showAll(Canvas & to) { CIntObject::showAll(to); - CMessage::drawBorder(PlayerColor(LOCPLINT->playerID), to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); + CMessage::drawBorder(PlayerColor(LOCPLINT->playerID), to, pos.w+28, pos.h+29, pos.x-14, pos.y-15); +} + +CHeroQuickBackpackWindow::CHeroQuickBackpackWindow(const CGHeroInstance * hero, ArtifactPosition targetSlot) + : CWindowObject(0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + stretchedBackground = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 0, 0)); + arts = std::make_shared(targetSlot); + arts->moveBy(Point(windowMargin, windowMargin)); + addSetAndCallbacks(static_cast>(arts)); + arts->setHero(hero); + addCloseCallback(std::bind(&CHeroQuickBackpackWindow::close, this)); + addUsedEvents(GESTURE); + pos.w = stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin; + pos.h = stretchedBackground->pos.h = arts->pos.h + windowMargin; +} + +void CHeroQuickBackpackWindow::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(on) + return; + + arts->swapSelected(); + close(); +} + +void CHeroQuickBackpackWindow::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) +{ + arts->selectSlotAt(currentPosition); + redraw(); +} + +void CHeroQuickBackpackWindow::showAll(Canvas & to) +{ + if(arts->getSlotsNum() == 0) + { + // Dirty solution for closing that window + close(); + return; + } + CMessage::drawBorder(PlayerColor(LOCPLINT->playerID), to, pos.w + 28, pos.h + 29, pos.x - 14, pos.y - 15); + CIntObject::showAll(to); } diff --git a/client/windows/CHeroBackpackWindow.h b/client/windows/CHeroBackpackWindow.h index 73d736b0a..9126e320c 100644 --- a/client/windows/CHeroBackpackWindow.h +++ b/client/windows/CHeroBackpackWindow.h @@ -14,16 +14,31 @@ class CFilledTexture; -class CHeroBackpackWindow : public CWindowObject, public CWindowWithArtifacts +class CHeroBackpackWindow : public CStatusbarWindow, public CWindowWithArtifacts { public: CHeroBackpackWindow(const CGHeroInstance * hero); -private: +protected: std::shared_ptr arts; std::shared_ptr quitButton; std::shared_ptr stretchedBackground; - const int windowMargin = 10; + const int windowMargin = 5; + + void showAll(Canvas & to) override; +}; + +class CHeroQuickBackpackWindow : public CWindowObject, public CWindowWithArtifacts +{ +public: + CHeroQuickBackpackWindow(const CGHeroInstance * hero, ArtifactPosition targetSlot); + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; + +private: + std::shared_ptr arts; + std::shared_ptr stretchedBackground; + const int windowMargin = 5; void showAll(Canvas & to) override; }; diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 00b522cc2..2a5412dc1 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -20,7 +20,7 @@ #include "../widgets/CComponent.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" -#include "../widgets/MiscWidgets.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../../lib/GameSettings.h" #include "../../lib/CGeneralTextHandler.h" @@ -234,4 +234,4 @@ void CHeroOverview::genControls() labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 186 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->getNameTranslated())); i++; } -} \ No newline at end of file +} diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 7d05fe38a..06a98bdcc 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -88,8 +88,8 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) if(settings["general"]["enableUiEnhancements"].Bool()) { questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); - backpackButton = std::make_shared(Point(424, 429), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); - backpackButton->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + backpackButton = std::make_shared(Point(424, 429), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); + backpackButton->setOverlay(std::make_shared(ImagePath::builtin("heroWindow/backpackButtonIcon"))); dismissButton = std::make_shared(Point(534, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); } else @@ -106,7 +106,8 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) if(hero->commander) { - commanderButton = std::make_shared(Point(317, 18), AnimationPath::builtin("buttons/commander"), CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); + commanderButton = std::make_shared(Point(317, 18), AnimationPath::builtin("heroCommander"), CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); + commanderButton->setOverlay(std::make_shared(ImagePath::builtin("heroWindow/commanderButtonIcon"))); } //right list of heroes @@ -119,9 +120,9 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v) { - auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), CComponent::primskill); + auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), ComponentType::PRIM_SKILL); area->text = CGI->generaltexth->arraytxt[2+v]; - area->type = v; + area->component.subType = PrimarySkill(v); area->hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % CGI->generaltexth->primarySkillNames[v]); primSkillAreas.push_back(area); @@ -154,7 +155,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) { Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); - secSkillAreas.push_back(std::make_shared(r, CComponent::secskill)); + secSkillAreas.push_back(std::make_shared(r, ComponentType::SEC_SKILL)); secSkillImages.push_back(std::make_shared(secSkills, 0, 0, r.x, r.y)); int x = (i % 2) ? 212 : 68; @@ -190,17 +191,17 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) assert(hero == curHero); name->setText(curHero->getNameTranslated()); - title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->type->heroClass->getNameTranslated()).str()); + title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->getClassNameTranslated()).str()); specArea->text = curHero->type->getSpecialtyDescriptionTranslated(); specImage->setFrame(curHero->type->imageIndex); specName->setText(curHero->type->getSpecialtyNameTranslated()); tacticsButton = std::make_shared(Point(539, 483), AnimationPath::builtin("hsbtns8.def"), std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS); - tacticsButton->addHoverText(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); + tacticsButton->addHoverText(EButtonState::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); - dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated())); - portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()); + dismissButton->addHoverText(EButtonState::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->getClassNameTranslated())); + portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->getClassNameTranslated()); portraitArea->text = curHero->getBiographyTranslated(); portraitImage->setFrame(curHero->getIconIndex()); @@ -232,20 +233,21 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) //primary skills support for(size_t g=0; gbonusValue = curHero->getPrimSkillLevel(static_cast(g)); - primSkillValues[g]->setText(std::to_string(primSkillAreas[g]->bonusValue)); + int value = curHero->getPrimSkillLevel(static_cast(g)); + primSkillAreas[g]->component.value = value; + primSkillValues[g]->setText(std::to_string(value)); } //secondary skills support for(size_t g=0; g< secSkillAreas.size(); ++g) { - int skill = curHero->secSkills[g].first; - int level = curHero->getSecSkillLevel(SecondarySkill(curHero->secSkills[g].first)); + SecondarySkill skill = curHero->secSkills[g].first; + int level = curHero->getSecSkillLevel(skill); std::string skillName = CGI->skillh->getByIndex(skill)->getNameTranslated(); std::string skillValue = CGI->generaltexth->levels[level-1]; - secSkillAreas[g]->type = skill; - secSkillAreas[g]->bonusValue = level; + secSkillAreas[g]->component.subType = skill; + secSkillAreas[g]->component.value = level; secSkillAreas[g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); secSkillAreas[g]->hoverText = boost::str(boost::format(heroscrn[21]) % skillValue % skillName); secSkillImages[g]->setFrame(skill*3 + level + 2); @@ -305,7 +307,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) formations->resetCallback(); //setting formations formations->setSelected(curHero->formation == EArmyFormation::TIGHT ? 1 : 0); - formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, value);}); + formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, static_cast(value));}); morale->set(curHero); luck->set(curHero); @@ -334,20 +336,11 @@ void CHeroWindow::commanderWindow() if(pickedArtInst) { const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); - if(freeSlot < ArtifactPosition::COMMANDER_AFTER_LAST) //we don't want to put it in commander's backpack! + if(vstd::contains(ArtifactUtils::commanderSlots(), freeSlot)) // We don't want to put it in commander's backpack! { - ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS); - ArtifactLocation dst(curHero->commander.get(), freeSlot); - - if(pickedArtInst->canBePutAt(dst, true)) - { //equip clicked stack - if(dst.getArt()) - { - LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(hero, - ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId()))); - } - LOCPLINT->cb->swapArtifacts(src, dst); - } + ArtifactLocation dst(curHero->id, freeSlot); + dst.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS), dst); } } else @@ -361,3 +354,8 @@ void CHeroWindow::updateGarrisons() garr->recreateSlots(); morale->set(curHero); } + +bool CHeroWindow::holdsGarrison(const CArmedInstance * army) +{ + return army == curHero; +} diff --git a/client/windows/CHeroWindow.h b/client/windows/CHeroWindow.h index 8f5bb2776..b8ead7a20 100644 --- a/client/windows/CHeroWindow.h +++ b/client/windows/CHeroWindow.h @@ -107,9 +107,9 @@ public: void commanderWindow(); void switchHero(); //changes displayed hero void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void createBackpackWindow(); //friends - friend void CHeroArtPlace::clickPressed(const Point & cursorPosition); friend class CPlayerInterface; }; diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 85d157bbe..997b0bb52 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -256,7 +256,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr break; case HERO_PRIMARY_SKILL: text = CGI->generaltexth->arraytxt[2+getSubID()]; - comp = std::make_shared(CComponent::primskill, getSubID(), (int)getValue()); + comp = std::make_shared(ComponentType::PRIM_SKILL, PrimarySkill(getSubID()), getValue()); break; case HERO_MANA: text = CGI->generaltexth->allTexts[149]; @@ -271,7 +271,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr if(value) { text = CGI->skillh->getByIndex(subID)->getDescriptionTranslated((int)value); - comp = std::make_shared(CComponent::secskill, subID, (int)value); + comp = std::make_shared(ComponentType::SEC_SKILL, SecondarySkill(subID), (int)value); } break; } @@ -662,6 +662,11 @@ void CKingdomInterface::updateGarrisons() garrison->updateGarrisons(); } +bool CKingdomInterface::holdsGarrison(const CArmedInstance * army) +{ + return army->getOwner() == LOCPLINT->playerID; +} + void CKingdomInterface::artifactAssembled(const ArtifactLocation& artLoc) { if(auto arts = std::dynamic_pointer_cast(tabArea->getItem())) @@ -709,6 +714,15 @@ void CKingdHeroList::updateGarrisons() } } +bool CKingdHeroList::holdsGarrison(const CArmedInstance * army) +{ + for(std::shared_ptr object : heroes->getItems()) + if(IGarrisonHolder * garrison = dynamic_cast(object.get())) + if (garrison->holdsGarrison(army)) + return true; + return false; +} + std::shared_ptr CKingdHeroList::createHeroItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images @@ -761,6 +775,15 @@ void CKingdTownList::updateGarrisons() } } +bool CKingdTownList::holdsGarrison(const CArmedInstance * army) +{ + for(std::shared_ptr object : towns->getItems()) + if(IGarrisonHolder * garrison = dynamic_cast(object.get())) + if (garrison->holdsGarrison(army)) + return true; + return false; +} + std::shared_ptr CKingdTownList::createTownItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images @@ -798,12 +821,13 @@ CTownItem::CTownItem(const CGTownInstance * Town) available.push_back(std::make_shared(Point(48+37*(int)i, 78), town, (int)i, true, false)); } - fastTownHall = std::make_shared(Point(69, 31), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterTownHall(); }); - fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); - fastTownHall->setAnimateLonelyFrame(true); - fastArmyPurchase = std::make_shared(Point(111, 31), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterToTheQuickRecruitmentWindow(); }); - fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); - fastArmyPurchase->setAnimateLonelyFrame(true); + fastTownHall = std::make_shared(Point(69, 31), AnimationPath::builtin("castleInterfaceQuickAccessz"), CButton::tooltip(), [this]() { std::make_shared(town)->enterTownHall(); }); + fastTownHall->setOverlay(std::make_shared(AnimationPath::builtin("ITMTL"), town->hallLevel())); + + int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1; + fastArmyPurchase = std::make_shared(Point(111, 31), AnimationPath::builtin("castleInterfaceQuickAccessz"), CButton::tooltip(), [this]() { std::make_shared(town)->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase->setOverlay(std::make_shared(AnimationPath::builtin("itmcl"), imageIndex)); + fastTavern = std::make_shared(Rect(5, 6, 58, 64), [&]() { if(town->builtBuildings.count(BuildingID::TAVERN)) @@ -836,6 +860,11 @@ void CTownItem::updateGarrisons() garr->recreateSlots(); } +bool CTownItem::holdsGarrison(const CArmedInstance * army) +{ + return army == town || army == town->getUpperArmy() || army == town->visitingHero; +} + void CTownItem::update() { std::string incomeVal = std::to_string(town->dailyIncome()[EGameResID::GOLD]); @@ -947,7 +976,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero) std::string overlay = CGI->generaltexth->overview[8+it]; auto button = std::make_shared(Point(364+(int)it*112, 46), AnimationPath::builtin("OVBUTN3"), CButton::tooltip(hover, overlay), 0); - button->addTextOverlay(CGI->generaltexth->allTexts[stringID[it]], FONT_SMALL, Colors::YELLOW); + button->setTextOverlay(CGI->generaltexth->allTexts[stringID[it]], FONT_SMALL, Colors::YELLOW); artButtons->addToggle((int)it, button); } artButtons->addCallback(std::bind(&CTabbedInt::setActive, artsTabs, _1)); @@ -998,6 +1027,11 @@ void CHeroItem::updateGarrisons() garr->recreateSlots(); } +bool CHeroItem::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + std::shared_ptr CHeroItem::onTabSelected(size_t index) { return artTabs.at(index); diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 2b951d2dd..26dd395a0 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -253,6 +253,7 @@ public: void townChanged(const CGTownInstance *town); void heroRemoved(); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void artifactRemoved(const ArtifactLocation &artLoc) override; void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; void artifactDisassembled(const ArtifactLocation &artLoc) override; @@ -290,6 +291,7 @@ public: CTownItem(const CGTownInstance * Town); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void update(); }; @@ -322,6 +324,7 @@ public: std::shared_ptr heroArts; void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; CHeroItem(const CGHeroInstance * hero); }; @@ -340,6 +343,7 @@ public: CKingdHeroList(size_t maxSize); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; }; /// Tab with all town-specific data @@ -358,4 +362,5 @@ public: void townChanged(const CGTownInstance * town); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; }; diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 335839991..b60973945 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -13,8 +13,6 @@ #include "../lobby/SelectionTab.h" -#include - #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" @@ -29,6 +27,7 @@ #include "../render/Graphics.h" #include "../../lib/CGeneralTextHandler.h" +#include "../../lib/TextOperations.h" #include "../../lib/CConfigHandler.h" #include "../../lib/campaign/CampaignState.h" #include "../../lib/mapping/CMap.h" @@ -39,9 +38,10 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/filesystem/Filesystem.h" -#include "../../lib/serializer/BinaryDeserializer.h" +#include "../../lib/serializer/CLoadFile.h" #include "../../lib/StartInfo.h" #include "../../lib/rmg/CMapGenOptions.h" +#include "../../lib/Languages.h" CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType) : CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), tabType(tabType) @@ -98,13 +98,13 @@ Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, in std::vector CMapOverviewWidget::createMinimaps(ResourcePath resource) const { - std::vector ret = std::vector(); + auto ret = std::vector(); CMapService mapService; std::unique_ptr map; try { - map = mapService.loadMap(resource); + map = mapService.loadMap(resource, nullptr); } catch (const std::exception & e) { @@ -117,7 +117,7 @@ std::vector CMapOverviewWidget::createMinimaps(ResourcePath resource) co std::vector CMapOverviewWidget::createMinimaps(std::unique_ptr & map) const { - std::vector ret = std::vector(); + auto ret = std::vector(); for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) ret.push_back(createMinimapForLayer(map, i)); @@ -161,15 +161,15 @@ CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): std::unique_ptr campaignMap = nullptr; if(p.tabType != ESelectionScreen::newGame && config["variables"]["mapPreviewForSaves"].Bool()) { - CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::SAVEGAME)), ESerializationVersion::MINIMAL); lf.checkMagicBytes(SAVEGAME_MAGIC); - std::unique_ptr mapHeader = std::make_unique(); + auto mapHeader = std::make_unique(); StartInfo * startInfo; lf >> *(mapHeader) >> startInfo; if(startInfo->campState) - campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario()); + campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario(), nullptr); res = ResourcePath(startInfo->fileURI, EResType::MAP); } if(!campaignMap) @@ -199,7 +199,7 @@ CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): if(p.date.empty()) { std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), p.tabType == ESelectionScreen::campaignList ? EResType::CAMPAIGN : EResType::MAP))); - w->setText(vstd::getFormattedDateTime(time)); + w->setText(TextOperations::getFormattedDateTimeLocal(time)); } else w->setText(p.date); diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 1602e80c3..bbd0b0e79 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -24,7 +24,7 @@ class CTextBox; class IImage; class Canvas; class TransparentFilledRectangle; -enum ESelectionScreen : ui8; +enum class ESelectionScreen : ui8; class CMapOverview; diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index 0684b4b14..fa575c7e6 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -11,79 +11,40 @@ #include "StdInc.h" #include "CMessage.h" -#include "../CGameInfo.h" -#include "../../lib/CGeneralTextHandler.h" #include "../../lib/TextOperations.h" -#include "../windows/InfoWindows.h" -#include "../widgets/Buttons.h" -#include "../widgets/CComponent.h" -#include "../widgets/Slider.h" -#include "../widgets/TextControls.h" #include "../gui/CGuiHandler.h" #include "../render/CAnimation.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" #include "../render/Canvas.h" #include "../render/Graphics.h" #include "../render/IFont.h" -#include "../renderSDL/SDL_Extensions.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/CComponent.h" +#include "../widgets/Images.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../windows/InfoWindows.h" -#include +constexpr int RIGHT_CLICK_POPUP_MIN_SIZE = 100; +constexpr int SIDE_MARGIN = 11; +constexpr int TOP_MARGIN = 20; +constexpr int BOTTOM_MARGIN = 16; +constexpr int INTERVAL_BETWEEN_BUTTONS = 18; +constexpr int INTERVAL_BETWEEN_TEXT_AND_BUTTONS = 24; -const int BETWEEN_COMPS_ROWS = 10; -const int BEFORE_COMPONENTS = 30; -const int BETWEEN_COMPS = 30; -const int SIDE_MARGIN = 30; - -template std::pair max(const std::pair &x, const std::pair &y) -{ - std::pair ret; - ret.first = std::max(x.first,y.first); - ret.second = std::max(x.second,y.second); - return ret; -} - -//One image component + subtitles below it -class ComponentResolved : public CIntObject -{ -public: - std::shared_ptr comp; - - //blit component with image centered at this position - void showAll(Canvas & to) override; - - //ComponentResolved(); - ComponentResolved(std::shared_ptr Comp); - ~ComponentResolved(); -}; -// Full set of components for blitting on dialog box -struct ComponentsToBlit -{ - std::vector< std::vector>> comps; - int w, h; - - void blitCompsOnSur(bool blitOr, int inter, int &curh, SDL_Surface *ret); - ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr); - ~ComponentsToBlit(); -}; - -namespace -{ - std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; - std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; - - std::shared_ptr background;//todo: should be CFilledTexture -} +static std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; +static std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; void CMessage::init() { - for(int i=0; ipreload(); - for(int j=0; j < dialogBorders[i]->size(0); j++) + for(int j = 0; j < dialogBorders[i]->size(0); j++) { auto image = dialogBorders[i]->getImage(j, 0); //assume blue color initially @@ -92,8 +53,6 @@ void CMessage::init() piecesOfBox[i].push_back(image); } } - - background = GH.renderHandler().loadImage(ImagePath::builtin("DIBOXBCK.BMP"), EImageBlitMode::OPAQUE); } void CMessage::dispose() @@ -102,57 +61,45 @@ void CMessage::dispose() item.reset(); } -SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) +std::vector CMessage::breakText(std::string text, size_t maxLineWidth, EFonts font) { - //prepare surface - SDL_Surface * ret = CSDL_Ext::newSurface(w,h); - for (int i=0; iwidth())//background - { - for (int j=0; jheight()) - { - background->draw(ret, i, j); - } - } + assert(maxLineWidth != 0); + if(maxLineWidth == 0) + return {text}; - drawBorder(playerColor, ret, w, h); - return ret; -} - -std::vector CMessage::breakText( std::string text, size_t maxLineWidth, EFonts font ) -{ std::vector ret; - boost::algorithm::trim_right_if(text,boost::algorithm::is_any_of(std::string(" "))); + boost::algorithm::trim_right_if(text, boost::algorithm::is_any_of(std::string(" "))); // each iteration generates one output line - while (text.length()) + while(text.length()) { - ui32 lineWidth = 0; //in characters or given char metric - ui32 wordBreak = -1; //last position for line break (last space character) - ui32 currPos = 0; //current position in text - bool opened = false; //set to true when opening brace is found - std::string color = ""; //color found + ui32 lineWidth = 0; //in characters or given char metric + ui32 wordBreak = -1; //last position for line break (last space character) + ui32 currPos = 0; //current position in text + bool opened = false; //set to true when opening brace is found + std::string color; //color found size_t symbolSize = 0; // width of character, in bytes size_t glyphWidth = 0; // width of printable glyph, pixels // loops till line is full or end of text reached - while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) + while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) { symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]); glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos); // candidate for line break - if (ui8(text[currPos]) <= ui8(' ')) + if(ui8(text[currPos]) <= ui8(' ')) wordBreak = currPos; /* We don't count braces in string length. */ - if (text[currPos] == '{') + if(text[currPos] == '{') { - opened=true; + opened = true; std::smatch match; - std::regex expr("^\\{(.*?)\\|"); + std::regex expr("^\\{(.*?)\\|"); std::string tmp = text.substr(currPos); if(std::regex_search(tmp, match, expr)) { @@ -164,23 +111,23 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi } } } - else if (text[currPos]=='}') + else if(text[currPos] == '}') { - opened=false; + opened = false; color = ""; } else - lineWidth += (ui32)glyphWidth; - currPos += (ui32)symbolSize; + lineWidth += glyphWidth; + currPos += symbolSize; } // long line, create line break - if (currPos < text.length() && (text[currPos] != 0x0a)) + if(currPos < text.length() && (text[currPos] != 0x0a)) { - if (wordBreak != ui32(-1)) + if(wordBreak != ui32(-1)) currPos = wordBreak; else - currPos -= (ui32)symbolSize; + currPos -= symbolSize; } //non-blank line @@ -188,7 +135,7 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi { ret.push_back(text.substr(0, currPos)); - if (opened) + if(opened) /* Close the brace for the current line. */ ret.back() += '}'; @@ -199,7 +146,7 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi ret.push_back(""); //add empty string, no extra actions needed } - if (text.length() != 0 && text[0] == 0x0a) + if(text.length() != 0 && text[0] == 0x0a) { /* Remove LF */ text.erase(0, 1); @@ -208,19 +155,19 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi { // trim only if line does not starts with LF // FIXME: necessary? All lines will be trimmed before returning anyway - boost::algorithm::trim_left_if(text,boost::algorithm::is_any_of(std::string(" "))); + boost::algorithm::trim_left_if(text, boost::algorithm::is_any_of(std::string(" "))); } - if (opened) + if(opened) { /* Add an opening brace for the next line. */ - if (text.length() != 0) + if(text.length() != 0) text.insert(0, "{" + color); } } /* Trim whitespaces of every line. */ - for (auto & elem : ret) + for(auto & elem : ret) boost::algorithm::trim(elem); return ret; @@ -240,118 +187,127 @@ std::string CMessage::guessHeader(const std::string & msg) int CMessage::guessHeight(const std::string & txt, int width, EFonts font) { const auto f = graphics->fonts[font]; - auto lines = CMessage::breakText(txt, width, font); - int lineHeight = static_cast(f->getLineHeight()); - return lineHeight * (int)lines.size(); + const auto lines = CMessage::breakText(txt, width, font); + size_t lineHeight = f->getLineHeight(); + return lineHeight * lines.size(); } int CMessage::getEstimatedComponentHeight(int numComps) { - if (numComps > 8) //Bigger than 8 components - return invalid value + if(numComps > 8) //Bigger than 8 components - return invalid value return std::numeric_limits::max(); - else if (numComps > 2) + if(numComps > 2) return 160; // 32px * 1 row + 20 to offset - else if (numComps) + if(numComps > 0) return 118; // 118 px to offset return 0; } void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player) { - bool blitOr = false; - if(dynamic_cast(ret)) //it's selection window, so we'll blit "or" between components - blitOr = true; - - const int sizes[][2] = {{400, 125}, {500, 150}, {600, 200}, {480, 400}}; + // possible sizes of text boxes section of window + // game should pick smallest one that can fit text without slider + // or, if not possible - pick last one and use slider + constexpr std::array textAreaSizes = { + // FIXME: this size should only be used for single-line texts: Point(206, 72), + Point(270, 72), + Point(270, 136), + Point(270, 200), + Point(400, 136), + Point(400, 200), + Point(590, 200) + }; assert(ret && ret->text); - for(int i = 0; - i < std::size(sizes) - && sizes[i][0] < GH.screenDimensions().x - 150 - && sizes[i][1] < GH.screenDimensions().y - 150 - && ret->text->slider; - i++) + + // STEP 1: DETERMINE SIZE OF ALL ELEMENTS + + for(const auto & area : textAreaSizes) { - ret->text->resize(Point(sizes[i][0], sizes[i][1])); + ret->text->resize(area); + if(!ret->text->slider) + break; // suitable size found, use it } + int textHeight = ret->text->pos.h; + if(ret->text->slider) - { ret->text->slider->addUsedEvents(CIntObject::WHEEL | CIntObject::KEYBOARD); + + int buttonsWidth = 0; + int buttonsHeight = 0; + if(!ret->buttons.empty()) + { + // Compute total width of buttons + buttonsWidth = INTERVAL_BETWEEN_BUTTONS * (ret->buttons.size() - 1); // space between all buttons + for(const auto & elem : ret->buttons) //and add buttons width + { + buttonsWidth += elem->pos.w; + vstd::amax(buttonsHeight, elem->pos.h); + } + } + + // STEP 2: COMPUTE WINDOW SIZE + + if(ret->buttons.empty() && !ret->components) + { + // use more compact form for right-click popup with no buttons / components + + ret->pos.w = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.x + 2 * SIDE_MARGIN); + ret->pos.h = std::max(RIGHT_CLICK_POPUP_MIN_SIZE, ret->text->label->textSize.y + TOP_MARGIN + BOTTOM_MARGIN); } else { - ret->text->resize(ret->text->label->textSize + Point(10, 10)); - } - - std::pair winSize(ret->text->pos.w, ret->text->pos.h); //start with text size - - ComponentsToBlit comps(ret->components,500, blitOr); - if (ret->components.size()) - winSize.second += 10 + comps.h; //space to first component - - int bw = 0; - if (ret->buttons.size()) - { - int bh = 0; - // Compute total width of buttons - bw = 20*((int)ret->buttons.size()-1); // space between all buttons - for(auto & elem : ret->buttons) //and add buttons width + int windowContentWidth = ret->text->pos.w; + int windowContentHeight = ret->text->pos.h; + if(ret->components) { - bw+=elem->pos.w; - vstd::amax(bh, elem->pos.h); + vstd::amax(windowContentWidth, ret->components->pos.w); + windowContentHeight += INTERVAL_BETWEEN_TEXT_AND_BUTTONS + ret->components->pos.h; } - winSize.second += 20 + bh;//before button + button + if(!ret->buttons.empty()) + { + vstd::amax(windowContentWidth, buttonsWidth); + windowContentHeight += INTERVAL_BETWEEN_TEXT_AND_BUTTONS + buttonsHeight; + } + + ret->pos.w = windowContentWidth + 2 * SIDE_MARGIN; + ret->pos.h = windowContentHeight + TOP_MARGIN + BOTTOM_MARGIN; } - // Clip window size - vstd::amax(winSize.second, 50); - vstd::amax(winSize.first, 80); - vstd::amax(winSize.first, comps.w); - vstd::amax(winSize.first, bw); + // STEP 3: MOVE ALL ELEMENTS IN PLACE - vstd::amin(winSize.first, GH.screenDimensions().x - 150); + if(ret->buttons.empty() && !ret->components) + { + ret->text->trimToFit(); + ret->text->center(ret->pos.center()); + } + else + { + if(ret->components) + ret->components->moveBy(Point((ret->pos.w - ret->components->pos.w) / 2, TOP_MARGIN + ret->text->pos.h + INTERVAL_BETWEEN_TEXT_AND_BUTTONS)); - ret->bitmap = drawDialogBox (winSize.first + 2*SIDE_MARGIN, winSize.second + 2*SIDE_MARGIN, player); - ret->pos.h=ret->bitmap->h; - ret->pos.w=ret->bitmap->w; + ret->text->trimToFit(); + ret->text->moveBy(Point((ret->pos.w - ret->text->pos.w) / 2, TOP_MARGIN + (textHeight - ret->text->pos.h) / 2 )); + + if(!ret->buttons.empty()) + { + int buttonPosX = ret->pos.w / 2 - buttonsWidth / 2; + int buttonPosY = ret->pos.h - BOTTOM_MARGIN - ret->buttons[0]->pos.h; + + for(const auto & elem : ret->buttons) + { + elem->moveBy(Point(buttonPosX, buttonPosY)); + buttonPosX += elem->pos.w + INTERVAL_BETWEEN_BUTTONS; + } + } + } + + ret->backgroundTexture->pos = ret->pos; ret->center(); - - int curh = SIDE_MARGIN; - int xOffset = (ret->pos.w - ret->text->pos.w)/2; - - if(!ret->buttons.size() && !ret->components.size()) //improvement for very small text only popups -> center text vertically - { - if(ret->bitmap->h > ret->text->pos.h + 2*SIDE_MARGIN) - curh = (ret->bitmap->h - ret->text->pos.h)/2; - } - - ret->text->moveBy(Point(xOffset, curh)); - - curh += ret->text->pos.h; - - if (ret->components.size()) - { - curh += BEFORE_COMPONENTS; - comps.blitCompsOnSur (blitOr, BETWEEN_COMPS, curh, ret->bitmap); - } - if(ret->buttons.size()) - { - // Position the buttons at the bottom of the window - bw = (ret->bitmap->w/2) - (bw/2); - curh = ret->bitmap->h - SIDE_MARGIN - ret->buttons[0]->pos.h; - - for(auto & elem : ret->buttons) - { - elem->moveBy(Point(bw, curh)); - bw += elem->pos.w + 20; - } - } - for(size_t i=0; icomponents.size(); i++) - ret->components[i]->moveBy(Point(ret->pos.x, ret->pos.y)); } -void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y) +void CMessage::drawBorder(PlayerColor playerColor, Canvas & to, int w, int h, int x, int y) { if(playerColor.isSpectator()) playerColor = PlayerColor(1); @@ -362,188 +318,36 @@ void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int // Horizontal borders int start_x = x + box[0]->width(); const int stop_x = x + w - box[1]->width(); - const int bottom_y = y+h-box[7]->height()+1; - while (start_x < stop_x) { - int cur_w = stop_x - start_x; - if (cur_w > box[6]->width()) - cur_w = box[6]->width(); + const int bottom_y = y + h - box[7]->height() + 1; + while(start_x < stop_x) + { // Top border - Rect srcR(0, 0, cur_w, box[6]->height()); - Rect dstR(start_x, y, 0, 0); - box[6]->draw(ret, &dstR, &srcR); - + to.draw(box[6], Point(start_x, y)); // Bottom border - dstR.y = bottom_y; - box[7]->draw(ret, &dstR, &srcR); + to.draw(box[7], Point(start_x, bottom_y)); - start_x += cur_w; + start_x += box[6]->width(); } // Vertical borders int start_y = y + box[0]->height(); - const int stop_y = y + h - box[2]->height()+1; - const int right_x = x+w-box[5]->width(); - while (start_y < stop_y) { - int cur_h = stop_y - start_y; - if (cur_h > box[4]->height()) - cur_h = box[4]->height(); + const int stop_y = y + h - box[2]->height() + 1; + const int right_x = x + w - box[5]->width(); + while(start_y < stop_y) + { // Left border - Rect srcR(0, 0, box[4]->width(), cur_h); - Rect dstR(x, start_y, 0, 0); - box[4]->draw(ret, &dstR, &srcR); - + to.draw(box[4], Point(x, start_y)); // Right border - dstR.x = right_x; - box[5]->draw(ret, &dstR, &srcR); + to.draw(box[5], Point(right_x, start_y)); - start_y += cur_h; + start_y += box[4]->height(); } //corners - Rect dstR(x, y, box[0]->width(), box[0]->height()); - box[0]->draw(ret, &dstR, nullptr); - - dstR=Rect(x+w-box[1]->width(), y, box[1]->width(), box[1]->height()); - box[1]->draw(ret, &dstR, nullptr); - - dstR=Rect(x, y+h-box[2]->height()+1, box[2]->width(), box[2]->height()); - box[2]->draw(ret, &dstR, nullptr); - - dstR=Rect(x+w-box[3]->width(), y+h-box[3]->height()+1, box[3]->width(), box[3]->height()); - box[3]->draw(ret, &dstR, nullptr); -} - -ComponentResolved::ComponentResolved(std::shared_ptr Comp): - comp(Comp) -{ - //Temporary assign ownership on comp - if (parent) - parent->removeChild(this); - if (comp->parent) - { - comp->parent->addChild(this); - comp->parent->removeChild(comp.get()); - } - - addChild(comp.get()); - defActions = 255 - DISPOSE; - pos.x = pos.y = 0; - - pos.w = comp->pos.w; - pos.h = comp->pos.h; -} - -ComponentResolved::~ComponentResolved() -{ - if (parent) - { - removeChild(comp.get()); - parent->addChild(comp.get()); - } -} - -void ComponentResolved::showAll(Canvas & to) -{ - CIntObject::showAll(to); - comp->showAll(to); -} - -ComponentsToBlit::~ComponentsToBlit() = default; - -ComponentsToBlit::ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr) -{ - int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); - - w = h = 0; - if(SComps.empty()) - return; - - comps.resize(1); - int curw = 0; - int curr = 0; //current row - - for(auto & SComp : SComps) - { - auto cur = std::make_shared(SComp); - - int toadd = (cur->pos.w + BETWEEN_COMPS + (blitOr ? orWidth : 0)); - if (curw + toadd > maxw) - { - curr++; - vstd::amax(w,curw); - curw = cur->pos.w; - comps.resize(curr+1); - } - else - { - curw += toadd; - vstd::amax(w,curw); - } - - comps[curr].push_back(cur); - } - - for(auto & elem : comps) - { - int maxHeight = 0; - for(size_t j=0;jpos.h); - - h += maxHeight + BETWEEN_COMPS_ROWS; - } -} - -void ComponentsToBlit::blitCompsOnSur( bool blitOr, int inter, int &curh, SDL_Surface *ret ) -{ - int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); - - for (auto & elem : comps)//for each row - { - int totalw=0, maxHeight=0; - for(size_t j=0;jpos.w; - vstd::amax(maxHeight, cur->pos.h); - } - - //add space between comps in this row - if(blitOr) - totalw += (inter*2+orWidth) * ((int)elem.size() - 1); - else - totalw += (inter) * ((int)elem.size() - 1); - - int middleh = curh + maxHeight/2;//axis for image aligment - int curw = ret->w/2 - totalw/2; - - for(size_t j=0;jmoveTo(Point(curw, curh)); - - //blit component - Canvas canvas = Canvas::createFromSurface(ret); - - cur->showAll(canvas); - curw += cur->pos.w; - - //if there is subsequent component blit "or" - if(j<(elem.size()-1)) - { - if(blitOr) - { - curw+=inter; - - graphics->fonts[FONT_MEDIUM]->renderTextLeft(ret, CGI->generaltexth->allTexts[4], Colors::WHITE, - Point(curw,middleh-((int)graphics->fonts[FONT_MEDIUM]->getLineHeight()/2))); - - curw+=orWidth; - } - curw+=inter; - } - } - curh += maxHeight + BETWEEN_COMPS_ROWS; - } + to.draw(box[0], Point(x, y)); + to.draw(box[1], Point(x + w - box[1]->width(), y)); + to.draw(box[2], Point(x, y + h - box[2]->height() + 1)); + to.draw(box[3], Point(x + w - box[3]->width(), y + h - box[3]->height() + 1)); } diff --git a/client/windows/CMessage.h b/client/windows/CMessage.h index 84bbfada8..31c5d466e 100644 --- a/client/windows/CMessage.h +++ b/client/windows/CMessage.h @@ -15,12 +15,12 @@ struct SDL_Surface; class CInfoWindow; class CComponent; +class Canvas; VCMI_LIB_NAMESPACE_BEGIN class ColorRGBA; VCMI_LIB_NAMESPACE_END - /// Class which draws formatted text messages and generates chat windows class CMessage { @@ -29,7 +29,7 @@ class CMessage public: /// Draw border on exiting surface - static void drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x=0, int y=0); + static void drawBorder(PlayerColor playerColor, Canvas & to, int w, int h, int x, int y); static void drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player); diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index 79edef9cb..8b5f70edb 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -76,7 +76,7 @@ void CPuzzleWindow::showAll(Canvas & to) void CPuzzleWindow::show(Canvas & to) { - static int animSpeed = 2; + constexpr int animSpeed = 2; if(currentAlpha < animSpeed) { diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index cd6480d8e..b6a53430f 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -160,7 +160,7 @@ void CQuestLog::recreateLabelList() } MetaString text; - quests[i].quest->getRolloverText (text, false); + quests[i].quest->getRolloverText (quests[i].obj->cb, text, false); if (quests[i].obj) { if (auto seersHut = dynamic_cast(quests[i].obj)) @@ -236,7 +236,7 @@ void CQuestLog::selectQuest(int which, int labelId) MetaString text; std::vector components; - currentQuest->quest->getVisitText(text, components, true); + currentQuest->quest->getVisitText(currentQuest->obj->cb, text, components, true); if(description->slider) description->slider->scrollToMin(); // scroll text to start position description->setText(text.toString()); //TODO: use special log entry text diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index bc43ea654..8c3ab46e9 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -25,7 +25,7 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" -#include "../widgets/MiscWidgets.h" +#include "../widgets/GraphicalPrimitiveCanvas.h" #include "../widgets/CComponent.h" #include "../widgets/TextControls.h" #include "../adventureMap/AdventureMapInterface.h" @@ -94,16 +94,16 @@ public: return A->getNameTranslated() < B->getNameTranslated(); } -} spellsorter; +}; CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED | (settings["general"]["enableUiEnhancements"].Bool() ? BORDERED : 0)), + CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), myHero(_myHero), myInt(_myInt), - isBigSpellbook(settings["general"]["enableUiEnhancements"].Bool()), + isBigSpellbook(settings["gameTweaks"]["enableLargeSpellbook"].Bool()), spellsPerPage(24), offL(-11), offR(195), @@ -124,70 +124,22 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m offL = offR = offT = offB = offRM = 0; spellsPerPage = 12; } + pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); - //initializing castable spells - mySpells.reserve(CGI->spellh->objects.size()); - for(const CSpell * spell : CGI->spellh->objects) + if(settings["general"]["enableUiEnhancements"].Bool()) { - if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell)) - mySpells.push_back(spell); - } - std::sort(mySpells.begin(), mySpells.end(), spellsorter); + Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16); + const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); + const ColorRGBA borderColor = ColorRGBA(128, 100, 75); + const ColorRGBA grayedColor = ColorRGBA(158, 130, 105); + searchBoxRectangle = std::make_shared(r.resize(1), rectangleColor, borderColor); + searchBoxDescription = std::make_shared(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, CGI->generaltexth->translate("vcmi.spellBook.search")); - //initializing sizes of spellbook's parts - for(auto & elem : sitesPerTabAdv) - elem = 0; - for(auto & elem : sitesPerTabBattle) - elem = 0; - - for(const auto spell : mySpells) - { - int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv; - - ++sitesPerOurTab[4]; - - spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop) - { - ++sitesPerOurTab[school]; - }); - } - if(sitesPerTabAdv[4] % spellsPerPage == 0) - sitesPerTabAdv[4]/=spellsPerPage; - else - sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1; - - for(int v=0; v<4; ++v) - { - if(sitesPerTabAdv[v] <= spellsPerPage - 2) - sitesPerTabAdv[v] = 1; - else - { - if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0) - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1; - else - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2; - } - } - - if(sitesPerTabBattle[4] % spellsPerPage == 0) - sitesPerTabBattle[4]/=spellsPerPage; - else - sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1; - - for(int v=0; v<4; ++v) - { - if(sitesPerTabBattle[v] <= spellsPerPage - 2) - sitesPerTabBattle[v] = 1; - else - { - if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0) - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1; - else - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2; - } + searchBox = std::make_shared(r, FONT_SMALL, std::bind(&CSpellWindow::searchInput, this), ETextAlignment::CENTER, true); } + processSpells(); //numbers of spell pages computed @@ -228,7 +180,8 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m interactiveAreas.push_back(std::make_shared( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); //areas for spells - int xpos = 117 + offL + pos.x, ypos = 90 + offT + pos.y; + int xpos = 117 + offL + pos.x; + int ypos = 90 + offT + pos.y; for(int v=0; vlocalState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap; schoolTab->setFrame(selectedTab, 0); - int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap; + int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap; // spellbook last page battle index is not reset after battle, so this needs to stay here vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1)); setCurrentPage(cp); @@ -292,14 +245,116 @@ std::shared_ptr CSpellWindow::createBigSpellBook() Canvas tmp5 = Canvas(Point(409, 141)); tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38)); canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415)); + // carpet + Canvas tmp6 = Canvas(Point(590, 59)); + tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59)); + canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59)); + // remove bookmarks + for (int i = 0; i < 56; i++) + canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464)); + for (int i = 0; i < 56; i++) + canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464)); + for (int i = 0; i < 57; i++) + canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464)); + for (int i = 0; i < 56; i++) + canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464)); + // draw bookmarks + canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47)); + canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41)); + canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45)); + canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47)); return GH.renderHandler().createImage(canvas.getInternalSurface()); } +void CSpellWindow::searchInput() +{ + if(searchBox) + searchBoxDescription->setEnabled(searchBox->getText().empty()); + + processSpells(); + + int cp = 0; + // spellbook last page battle index is not reset after battle, so this needs to stay here + vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1)); + setCurrentPage(cp); + computeSpellsPerArea(); +} + +void CSpellWindow::processSpells() +{ + mySpells.clear(); + + //initializing castable spells + mySpells.reserve(CGI->spellh->objects.size()); + for(const CSpell * spell : CGI->spellh->objects) + { + bool searchTextFound = !searchBox || boost::algorithm::contains(boost::algorithm::to_lower_copy(spell->getNameTranslated()), boost::algorithm::to_lower_copy(searchBox->getText())); + if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell) && searchTextFound) + mySpells.push_back(spell); + } + + SpellbookSpellSorter spellsorter; + std::sort(mySpells.begin(), mySpells.end(), spellsorter); + + //initializing sizes of spellbook's parts + for(auto & elem : sitesPerTabAdv) + elem = 0; + for(auto & elem : sitesPerTabBattle) + elem = 0; + + for(const auto spell : mySpells) + { + int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv; + + ++sitesPerOurTab[4]; + + spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop) + { + ++sitesPerOurTab[school]; + }); + } + if(sitesPerTabAdv[4] % spellsPerPage == 0) + sitesPerTabAdv[4]/=spellsPerPage; + else + sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1; + + for(int v=0; v<4; ++v) + { + if(sitesPerTabAdv[v] <= spellsPerPage - 2) + sitesPerTabAdv[v] = 1; + else + { + if((sitesPerTabAdv[v] - (spellsPerPage - 2)) % spellsPerPage == 0) + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - (spellsPerPage - 2)) / spellsPerPage + 1; + else + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - (spellsPerPage - 2)) / spellsPerPage + 2; + } + } + + if(sitesPerTabBattle[4] % spellsPerPage == 0) + sitesPerTabBattle[4]/=spellsPerPage; + else + sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1; + + for(int v=0; v<4; ++v) + { + if(sitesPerTabBattle[v] <= spellsPerPage - 2) + sitesPerTabBattle[v] = 1; + else + { + if((sitesPerTabBattle[v] - (spellsPerPage - 2)) % spellsPerPage == 0) + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - (spellsPerPage - 2)) / spellsPerPage + 1; + else + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - (spellsPerPage - 2)) / spellsPerPage + 2; + } + } +} + void CSpellWindow::fexitb() { (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; - (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap) = currentPage; + (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap) = currentPage; close(); } @@ -449,8 +504,16 @@ void CSpellWindow::setCurrentPage(int value) schoolPicture->visible = selectedTab!=4 && currentPage == 0; if(selectedTab != 4) schoolPicture->setFrame(selectedTab, 0); - leftCorner->visible = currentPage != 0; - rightCorner->visible = (currentPage+1) < pagesWithinCurrentTab(); + + if (currentPage != 0) + leftCorner->enable(); + else + leftCorner->disable(); + + if (currentPage + 1 < pagesWithinCurrentTab()) + rightCorner->enable(); + else + rightCorner->disable(); mana->setText(std::to_string(myHero->mana));//just in case, it will be possible to cast spell without closing book } @@ -458,13 +521,13 @@ void CSpellWindow::setCurrentPage(int value) void CSpellWindow::turnPageLeft() { if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15); + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK); } void CSpellWindow::turnPageRight() { if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15); + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15, EVideoType::SPELLBOOK); } void CSpellWindow::keyPressed(EShortcut key) @@ -558,7 +621,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) //battle spell on adv map or adventure map spell during combat => display infowindow, not cast if((combatSpell ^ inCombat) || inCastle) { - std::vector> hlp(1, std::make_shared(CComponent::spell, mySpell->id, 0)); + std::vector> hlp(1, std::make_shared(ComponentType::SPELL, mySpell->id)); LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp); } else if(combatSpell) @@ -587,7 +650,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) auto guard = vstd::makeScopeGuard([this]() { owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab; - owner->myInt->localState->spellbookSettings.spellbokLastPageAdvmap = owner->currentPage; + owner->myInt->localState->spellbookSettings.spellbookLastPageAdvmap = owner->currentPage; }); if(mySpell->getTargetType() == spells::AimType::LOCATION) @@ -614,7 +677,7 @@ void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition) boost::algorithm::replace_first(dmgInfo, "%d", std::to_string(causedDmg)); } - CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(CComponent::spell, mySpell->id)); + CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(ComponentType::SPELL, mySpell->id)); } } @@ -639,7 +702,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) mySpell = spell; if(mySpell) { - int32_t whichSchool = 0; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, + SpellSchool whichSchool; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, schoolLevel = owner->myHero->getSpellSchoolLevel(mySpell, &whichSchool); auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); @@ -648,7 +711,14 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool : owner->selectedTab], schoolLevel); + schoolBorder.reset(); + if (owner->selectedTab >= 4) + { + if (whichSchool.getNum() != SpellSchool()) + schoolBorder = std::make_shared(owner->schoolBorders.at(whichSchool.getNum()), schoolLevel); + } + else + schoolBorder = std::make_shared(owner->schoolBorders.at(owner->selectedTab), schoolLevel); } ColorRGBA firstLineColor, secondLineColor; diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 941de6428..16401a752 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -26,6 +26,8 @@ class CLabel; class CGStatusBar; class CPlayerInterface; class CSpellWindow; +class CTextInput; +class TransparentFilledRectangle; /// The spell window class CSpellWindow : public CWindowObject @@ -80,6 +82,10 @@ class CSpellWindow : public CWindowObject std::vector> interactiveAreas; + std::shared_ptr searchBox; + std::shared_ptr searchBoxRectangle; + std::shared_ptr searchBoxDescription; + bool isBigSpellbook; int spellsPerPage; int offL; @@ -99,6 +105,8 @@ class CSpellWindow : public CWindowObject const CGHeroInstance * myHero; //hero whose spells are presented CPlayerInterface * myInt; + void processSpells(); + void searchInput(); void computeSpellsPerArea(); //recalculates spellAreas::mySpell void setCurrentPage(int value); diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 8a1d3672a..6243ed634 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -12,317 +12,28 @@ #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" -#include "../widgets/Images.h" #include "../render/Canvas.h" -#include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../widgets/Buttons.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" -#include "../windows/InfoWindows.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../../CCallback.h" -#include "../../lib/VCMI_Lib.h" -#include "../../lib/CArtHandler.h" -#include "../../lib/CCreatureHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGMarket.h" -#include "../../lib/networkPacks/ArtifactLocation.h" - -CTradeWindow::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial) - : CIntObject(LCLICK | HOVER | SHOW_POPUP, pos), - type(EType(-1)),// set to invalid, will be corrected in setType - id(ID), - serial(Serial), - left(Left) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - downSelection = false; - hlp = nullptr; - setType(Type); -} - -void CTradeWindow::CTradeableItem::setType(EType newType) -{ - if(type != newType) - { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - type = newType; - - if(getIndex() < 0) - { - image = std::make_shared(getFilename(), 0); - image->disable(); - } - else - { - image = std::make_shared(getFilename(), getIndex()); - } - } -} - -void CTradeWindow::CTradeableItem::setID(int newID) -{ - if (id != newID) - { - id = newID; - if (image) - { - int index = getIndex(); - if (index < 0) - image->disable(); - else - { - image->enable(); - image->setFrame(index); - } - } - } -} - -AnimationPath CTradeWindow::CTradeableItem::getFilename() -{ - switch(type) - { - case RESOURCE: - return AnimationPath::builtin("RESOURCE"); - case PLAYER: - return AnimationPath::builtin("CREST58"); - case ARTIFACT_TYPE: - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - return AnimationPath::builtin("artifact"); - case CREATURE: - return AnimationPath::builtin("TWCRPORT"); - default: - return {}; - } -} - -int CTradeWindow::CTradeableItem::getIndex() -{ - if (id < 0) - return -1; - - switch(type) - { - case RESOURCE: - case PLAYER: - return id; - case ARTIFACT_TYPE: - case ARTIFACT_INSTANCE: - case ARTIFACT_PLACEHOLDER: - return CGI->artifacts()->getByIndex(id)->getIconIndex(); - case CREATURE: - return CGI->creatures()->getByIndex(id)->getIconIndex(); - default: - return -1; - } -} - -void CTradeWindow::CTradeableItem::showAll(Canvas & to) -{ - CTradeWindow *mw = dynamic_cast(parent); - assert(mw); - - Point posToBitmap; - Point posToSubCenter; - - switch(type) - { - case RESOURCE: - posToBitmap = Point(19,9); - posToSubCenter = Point(36, 59); - break; - case CREATURE_PLACEHOLDER: - case CREATURE: - posToSubCenter = Point(29, 76); - // Positing of unit count is different in Altar of Sacrifice and Freelancer's Guild - if(mw->mode == EMarketMode::CREATURE_EXP && downSelection) - posToSubCenter.y += 5; - break; - case PLAYER: - posToSubCenter = Point(31, 76); - break; - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - posToSubCenter = Point(19, 55); - if(downSelection) - posToSubCenter.y += 8; - break; - case ARTIFACT_TYPE: - posToSubCenter = Point(19, 58); - break; - } - - if (image) - { - image->moveTo(pos.topLeft() + posToBitmap); - CIntObject::showAll(to); - } - - to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle); -} - -void CTradeWindow::CTradeableItem::clickPressed(const Point & cursorPosition) -{ - CTradeWindow *mw = dynamic_cast(parent); - assert(mw); - if(type == ARTIFACT_PLACEHOLDER) - { - CAltarWindow *aw = static_cast(mw); - const auto pickedArtInst = aw->getPickedArtifact(); - - if(pickedArtInst) - { - aw->arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS); - aw->moveArtToAltar(this->shared_from_this(), pickedArtInst); - } - else if(const CArtifactInstance *art = getArtInstance()) - { - const auto hero = aw->arts->getHero(); - const auto slot = hero->getSlotByInstance(art); - assert(slot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero, slot), - ArtifactLocation(hero, ArtifactPosition::TRANSITION_POS)); - aw->arts->pickedArtFromSlot = slot; - aw->arts->artifactsOnAltar.erase(art); - setID(-1); - subtitle.clear(); - aw->deal->block(!aw->arts->artifactsOnAltar.size()); - } - - aw->calcTotalExp(); - return; - } - if(left) - { - if(mw->hLeft != this->shared_from_this()) - mw->hLeft = this->shared_from_this(); - else - return; - } - else - { - if(mw->hRight != this->shared_from_this()) - mw->hRight = this->shared_from_this(); - else - return; - } - mw->selectionChanged(left); -} - -void CTradeWindow::CTradeableItem::showAllAt(const Point &dstPos, const std::string &customSub, Canvas & to) -{ - Rect oldPos = pos; - std::string oldSub = subtitle; - downSelection = true; - - moveTo(dstPos); - subtitle = customSub; - showAll(to); - - downSelection = false; - moveTo(oldPos.topLeft()); - subtitle = oldSub; -} - -void CTradeWindow::CTradeableItem::hover(bool on) -{ - if(!on) - { - GH.statusbar()->clear(); - return; - } - - switch(type) - { - case CREATURE: - case CREATURE_PLACEHOLDER: - GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated())); - break; - case ARTIFACT_PLACEHOLDER: - if(id < 0) - GH.statusbar()->write(CGI->generaltexth->zelp[582].first); - else - GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated()); - break; - } -} - -void CTradeWindow::CTradeableItem::showPopupWindow(const Point & cursorPosition) -{ - switch(type) - { - case CREATURE: - case CREATURE_PLACEHOLDER: - //GH.statusbar->print(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->namePl)); - break; - case ARTIFACT_TYPE: - case ARTIFACT_PLACEHOLDER: - //TODO: it's would be better for market to contain actual CArtifactInstance and not just ids of certain artifact type so we can use getEffectiveDescription. - if(id >= 0) - CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated()); - break; - } -} - -std::string CTradeWindow::CTradeableItem::getName(int number) const -{ - switch(type) - { - case PLAYER: - return CGI->generaltexth->capColors[id]; - case RESOURCE: - return CGI->generaltexth->restypes[id]; - case CREATURE: - if(number == 1) - return CGI->creh->objects[id]->getNameSingularTranslated(); - else - return CGI->creh->objects[id]->getNamePluralTranslated(); - case ARTIFACT_TYPE: - case ARTIFACT_INSTANCE: - return CGI->artifacts()->getByIndex(id)->getNameTranslated(); - } - logGlobal->error("Invalid trade item type: %d", (int)type); - return ""; -} - -const CArtifactInstance * CTradeWindow::CTradeableItem::getArtInstance() const -{ - switch(type) - { - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - return hlp; - default: - return nullptr; - } -} - -void CTradeWindow::CTradeableItem::setArtInstance(const CArtifactInstance *art) -{ - assert(type == ARTIFACT_PLACEHOLDER || type == ARTIFACT_INSTANCE); - hlp = art; - if(art) - setID(art->artType->getId()); - else - setID(-1); -} CTradeWindow::CTradeWindow(const ImagePath & bgName, const IMarket *Market, const CGHeroInstance *Hero, const std::function & onWindowClosed, EMarketMode Mode): + CTradeBase(Market, Hero), CWindowObject(PLAYER_COLORED, bgName), - market(Market), onWindowClosed(onWindowClosed), - hero(Hero), readyToTrade(false) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -336,32 +47,24 @@ void CTradeWindow::initTypes() switch(mode) { case EMarketMode::RESOURCE_RESOURCE: - itemsType[1] = RESOURCE; - itemsType[0] = RESOURCE; + itemsType[1] = EType::RESOURCE; + itemsType[0] = EType::RESOURCE; break; case EMarketMode::RESOURCE_PLAYER: - itemsType[1] = RESOURCE; - itemsType[0] = PLAYER; + itemsType[1] = EType::RESOURCE; + itemsType[0] = EType::PLAYER; break; case EMarketMode::CREATURE_RESOURCE: - itemsType[1] = CREATURE; - itemsType[0] = RESOURCE; + itemsType[1] = EType::CREATURE; + itemsType[0] = EType::RESOURCE; break; case EMarketMode::RESOURCE_ARTIFACT: - itemsType[1] = RESOURCE; - itemsType[0] = ARTIFACT_TYPE; + itemsType[1] = EType::RESOURCE; + itemsType[0] = EType::ARTIFACT_TYPE; break; case EMarketMode::ARTIFACT_RESOURCE: - itemsType[1] = ARTIFACT_INSTANCE; - itemsType[0] = RESOURCE; - break; - case EMarketMode::CREATURE_EXP: - itemsType[1] = CREATURE; - itemsType[0] = CREATURE_PLACEHOLDER; - break; - case EMarketMode::ARTIFACT_EXP: - itemsType[1] = ARTIFACT_TYPE; - itemsType[0] = ARTIFACT_PLACEHOLDER; + itemsType[1] = EType::ARTIFACT_INSTANCE; + itemsType[0] = EType::RESOURCE; break; } } @@ -370,178 +73,115 @@ void CTradeWindow::initItems(bool Left) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - if(Left && (itemsType[1] == ARTIFACT_TYPE || itemsType[1] == ARTIFACT_INSTANCE)) + if(Left && (itemsType[1] == EType::ARTIFACT_TYPE || itemsType[1] == EType::ARTIFACT_INSTANCE)) { if(mode == EMarketMode::ARTIFACT_RESOURCE) { - auto item = std::make_shared(Point(137, 469), itemsType[Left], -1, 1, 0); + auto item = std::make_shared(Rect(Point(137, 469), Point()), itemsType[Left], -1, 1, 0); item->recActions &= ~(UPDATE | SHOWALL); items[Left].push_back(item); } } else { - std::vector *ids = getItemsIds(Left); - std::vector pos; - int amount = -1; - - getPositionsFor(pos, Left, itemsType[Left]); - - if(Left || !ids) - amount = 7; - else - amount = static_cast(ids->size()); - - if(ids) - vstd::amin(amount, ids->size()); - - for(int j=0; jsize()>j) ? (*ids)[j] : j; - if(id < 0 && mode != EMarketMode::ARTIFACT_EXP) //when sacrificing artifacts we need to prepare empty slots - continue; + if(hLeft) + for(const auto & slot : rightTradePanel->slots) + { + int h1, h2; //hlp variables for getting offer + market->getOffer(hLeft->id, slot->id, h1, h2, marketMode); - auto item = std::make_shared(pos[j].topLeft(), itemsType[Left], id, Left, j); - item->pos = pos[j] + this->pos.topLeft(); - items[Left].push_back(item); - } - vstd::clear_pointer(ids); - initSubs(Left); - } -} - -std::vector *CTradeWindow::getItemsIds(bool Left) -{ - std::vector *ids = nullptr; - - if(mode == EMarketMode::ARTIFACT_EXP) - return new std::vector(22, -1); - - if(Left) - { - switch(itemsType[1]) - { - case CREATURE: - ids = new std::vector; - for(int i = 0; i < 7; i++) - { - if(const CCreature *c = hero->getCreature(SlotID(i))) - ids->push_back(c->getId()); - else - ids->push_back(-1); - } - break; - } - } - else - { - switch(itemsType[0]) - { - case PLAYER: - ids = new std::vector; - for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++) - if(PlayerColor(i) != LOCPLINT->playerID && LOCPLINT->cb->getPlayerStatus(PlayerColor(i)) == EPlayerStatus::INGAME) - ids->push_back(i); - break; - - case ARTIFACT_TYPE: - ids = new std::vector(market->availableItemsIds(mode)); - break; - } - } - - return ids; -} - -void CTradeWindow::getPositionsFor(std::vector &poss, bool Left, EType type) const -{ - if(mode == EMarketMode::ARTIFACT_EXP && !Left) - { - //22 boxes, 5 in row, last row: two boxes centered - int h, w, x, y, dx, dy; - h = w = 44; - x = 317; - y = 53; - dx = 54; - dy = 70; - for (int i = 0; i < 4 ; i++) - for (int j = 0; j < 5 ; j++) - poss.push_back(Rect(x + dx*j, y + dy*i, w, h)); - - poss.push_back(Rect((int)(x + dx * 1.5), (y + dy * 4), w, h)); - poss.push_back(Rect((int)(x + dx * 2.5), (y + dy * 4), w, h)); - } - else - { - //seven boxes: - // X X X - // X X X - // X - int h, w, x, y, dx, dy; - int leftToRightOffset; - getBaseForPositions(type, dx, dy, x, y, h, w, !Left, leftToRightOffset); - - const std::vector tmp = - { - Rect(Point(x + 0 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 2 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 0 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 2 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 2 * dy), Point(w, h) ) + rightTradePanel->updateOffer(*slot, h1, h2); + } + else + rightTradePanel->clearSubtitles(); }; - vstd::concatenate(poss, tmp); - - if(!Left) + auto clickPressedTradePanel = [this](const std::shared_ptr & newSlot, bool left) { - for(Rect &r : poss) - r.x += leftToRightOffset; + CTradeBase::onSlotClickPressed(newSlot, left ? hLeft : hRight); + selectionChanged(left); + }; + + if(Left && mode == EMarketMode::CREATURE_RESOURCE) + { + CreaturesPanel::slotsData slots; + for(auto slotId = SlotID(0); slotId.num < GameConstants::ARMY_SIZE; slotId++) + { + if(const auto & creature = hero->getCreature(slotId)) + slots.emplace_back(std::make_tuple(creature->getId(), slotId, hero->getStackCount(slotId))); + } + leftTradePanel = std::make_shared(std::bind(clickPressedTradePanel, _1, true), slots); + leftTradePanel->moveBy(Point(45, 123)); + leftTradePanel->deleteSlotsCheck = [this](const std::shared_ptr & slot) + { + return this->hero->getStackCount(SlotID(slot->serial)) == 0 ? true : false; + }; + } + else if(Left && (mode == EMarketMode::RESOURCE_RESOURCE || mode == EMarketMode::RESOURCE_ARTIFACT || mode == EMarketMode::RESOURCE_PLAYER)) + { + leftTradePanel = std::make_shared( + [clickPressedTradePanel](const std::shared_ptr & newSlot) + { + clickPressedTradePanel(newSlot, true); + }, + [this]() + { + for(const auto & slot : leftTradePanel->slots) + slot->subtitle = std::to_string(LOCPLINT->cb->getResourceAmount(static_cast(slot->serial))); + }); + leftTradePanel->moveBy(Point(39, 182)); + leftTradePanel->updateSlots(); + } + else if(!Left && mode == EMarketMode::RESOURCE_RESOURCE) + { + rightTradePanel = std::make_shared( + [clickPressedTradePanel](const std::shared_ptr & newSlot) + { + clickPressedTradePanel(newSlot, false); + }, + [this, updRightSub]() + { + updRightSub(EMarketMode::RESOURCE_RESOURCE); + if(hLeft) + rightTradePanel->slots[hLeft->serial]->subtitle = CGI->generaltexth->allTexts[164]; // n/a + }); + rightTradePanel->moveBy(Point(327, 181)); + } + else if(!Left && (mode == EMarketMode::ARTIFACT_RESOURCE || mode == EMarketMode::CREATURE_RESOURCE)) + { + rightTradePanel = std::make_shared(std::bind(clickPressedTradePanel, _1, false), + std::bind(updRightSub, EMarketMode::ARTIFACT_RESOURCE)); + rightTradePanel->moveBy(Point(327, 181)); + } + else if(!Left && mode == EMarketMode::RESOURCE_ARTIFACT) + { + rightTradePanel = std::make_shared(std::bind(clickPressedTradePanel, _1, false), + std::bind(updRightSub, EMarketMode::RESOURCE_ARTIFACT), market->availableItemsIds(mode)); + rightTradePanel->moveBy(Point(327, 181)); + rightTradePanel->deleteSlotsCheck = [this](const std::shared_ptr & slot) + { + return vstd::contains(market->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT), ArtifactID(slot->id)) ? false : true; + }; + } + else if(!Left && mode == EMarketMode::RESOURCE_PLAYER) + { + rightTradePanel = std::make_shared(std::bind(clickPressedTradePanel, _1, false)); + rightTradePanel->moveBy(Point(333, 83)); } } } void CTradeWindow::initSubs(bool Left) { - for(auto item : items[Left]) - { + if(itemsType[Left] == EType::RESOURCE || itemsType[Left] == EType::ARTIFACT_TYPE) + { if(Left) - { - switch(itemsType[1]) - { - case CREATURE: - item->subtitle = std::to_string(hero->getStackCount(SlotID(item->serial))); - break; - case RESOURCE: - item->subtitle = std::to_string(LOCPLINT->cb->getResourceAmount(static_cast(item->serial))); - break; - } - } - else //right side - { - if(itemsType[0] == PLAYER) - { - item->subtitle = CGI->generaltexth->capColors[item->id]; - } - else if(hLeft)//artifact, creature - { - int h1, h2; //hlp variables for getting offer - market->getOffer(hLeft->id, item->id, h1, h2, mode); - if(item->id != hLeft->id || mode != EMarketMode::RESOURCE_RESOURCE) //don't allow exchanging same resources - { - std::ostringstream oss; - oss << h2; - if(h1!=1) - oss << "/" << h1; - item->subtitle = oss.str(); - } - else - item->subtitle = CGI->generaltexth->allTexts[164]; // n/a - } - else - item->subtitle = ""; - } + leftTradePanel->updateSlots(); + else + rightTradePanel->updateSlots(); + return; } } @@ -549,17 +189,12 @@ void CTradeWindow::showAll(Canvas & to) { CWindowObject::showAll(to); - if(hRight) - to.drawBorder(Rect::createAround(hRight->pos, 1), Colors::BRIGHT_YELLOW, 2); - if(hLeft && hLeft->type != ARTIFACT_INSTANCE) - to.drawBorder(Rect::createAround(hLeft->pos, 1), Colors::BRIGHT_YELLOW, 2); - if(readyToTrade) { if(hLeft) - hLeft->showAllAt(pos.topLeft() + selectionOffset(true), selectionSubtitle(true), to); + hLeft->showAllAt(pos.topLeft() + selectionOffset(true), updateSlotSubtitle(true), to); if(hRight) - hRight->showAllAt(pos.topLeft() + selectionOffset(false), selectionSubtitle(false), to); + hRight->showAllAt(pos.topLeft() + selectionOffset(false), updateSlotSubtitle(false), to); } } @@ -571,30 +206,6 @@ void CTradeWindow::close() CWindowObject::close(); } -void CTradeWindow::removeItems(const std::set> & toRemove) -{ - for(auto item : toRemove) - removeItem(item); -} - -void CTradeWindow::removeItem(std::shared_ptr item) -{ - items[item->left] -= item; - - if(hRight == item) - { - hRight.reset(); - selectionChanged(false); - } -} - -void CTradeWindow::getEmptySlots(std::set> & toRemove) -{ - for(auto item : items[1]) - if(!hero->getStackCount(SlotID(item->serial))) - toRemove.insert(item); -} - void CTradeWindow::setMode(EMarketMode Mode) { const IMarket *m = market; @@ -608,7 +219,6 @@ void CTradeWindow::setMode(EMarketMode Mode) { case EMarketMode::CREATURE_EXP: case EMarketMode::ARTIFACT_EXP: - GH.windows().createAndPushWindow(m, h, functor, Mode); break; default: GH.windows().createAndPushWindow(m, h, functor, Mode); @@ -616,7 +226,7 @@ void CTradeWindow::setMode(EMarketMode Mode) } } -void CTradeWindow::artifactSelected(CHeroArtPlace *slot) +void CTradeWindow::artifactSelected(CArtPlace * slot) { assert(mode == EMarketMode::ARTIFACT_RESOURCE); items[1][0]->setArtInstance(slot->getArt()); @@ -667,10 +277,10 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta title = (*CGI->townh)[ETownType::STRONGHOLD]->town->buildings[BuildingID::FREELANCERS_GUILD]->getNameTranslated(); break; case EMarketMode::RESOURCE_ARTIFACT: - title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); + title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); break; case EMarketMode::ARTIFACT_RESOURCE: - title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); + title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); // create image that copies part of background containing slot MISC_1 into position of slot MISC_5 // this is workaround for bug in H3 files where this slot for ragdoll on this screen is missing @@ -806,20 +416,37 @@ void CMarketplaceWindow::makeDeal() if(allowDeal) { - if(slider) + switch(mode) { - LOCPLINT->cb->trade(market, mode, leftIdToSend, hRight->id, slider->getValue() * r1, hero); + case EMarketMode::RESOURCE_RESOURCE: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero); slider->scrollTo(0); - } - else - { - LOCPLINT->cb->trade(market, mode, leftIdToSend, hRight->id, r2, hero); + break; + case EMarketMode::CREATURE_RESOURCE: + LOCPLINT->cb->trade(market, mode, SlotID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero); + slider->scrollTo(0); + break; + case EMarketMode::RESOURCE_PLAYER: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), PlayerColor(hRight->id), slider->getValue() * r1, hero); + slider->scrollTo(0); + break; + + case EMarketMode::RESOURCE_ARTIFACT: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), ArtifactID(hRight->id), r2, hero); + break; + case EMarketMode::ARTIFACT_RESOURCE: + LOCPLINT->cb->trade(market, mode, ArtifactInstanceID(leftIdToSend), GameResID(hRight->id), r2, hero); + break; } } madeTransaction = true; hLeft = nullptr; hRight = nullptr; + if(leftTradePanel) + leftTradePanel->deselect(); + assert(rightTradePanel); + rightTradePanel->deselect(); selectionChanged(true); } @@ -845,9 +472,9 @@ void CMarketplaceWindow::selectionChanged(bool side) if(slider) { int newAmount = -1; - if(itemsType[1] == RESOURCE) + if(itemsType[1] == EType::RESOURCE) newAmount = LOCPLINT->cb->getResourceAmount(static_cast(soldItemId)); - else if(itemsType[1] == CREATURE) + else if(itemsType[1] == EType::CREATURE) newAmount = hero->getStackCount(SlotID(hLeft->serial)) - (hero->stacksCount() == 1 && hero->needsLastStack()); else assert(0); @@ -857,7 +484,7 @@ void CMarketplaceWindow::selectionChanged(bool side) max->block(false); deal->block(false); } - else if(itemsType[1] == RESOURCE) //buying -> check if we can afford transaction + else if(itemsType[1] == EType::RESOURCE) //buying -> check if we can afford transaction { deal->block(LOCPLINT->cb->getResourceAmount(static_cast(soldItemId)) < r1); } @@ -875,7 +502,7 @@ void CMarketplaceWindow::selectionChanged(bool side) deal->block(true); } - if(side && itemsType[0] != PLAYER) //items[1] selection changed, recalculate offers + if(side && itemsType[0] != EType::PLAYER) //items[1] selection changed, recalculate offers initSubs(false); updateTraderText(); @@ -905,15 +532,13 @@ bool CMarketplaceWindow::printButtonFor(EMarketMode M) const } } -void CMarketplaceWindow::garrisonChanged() +void CMarketplaceWindow::updateGarrison() { if(mode != EMarketMode::CREATURE_RESOURCE) return; - std::set> toRemove; - getEmptySlots(toRemove); - removeItems(toRemove); - initSubs(true); + leftTradePanel->deleteSlots(); + leftTradePanel->updateSlots(); } void CMarketplaceWindow::artifactsChanged(bool Left) @@ -921,28 +546,19 @@ void CMarketplaceWindow::artifactsChanged(bool Left) assert(!Left); if(mode != EMarketMode::RESOURCE_ARTIFACT) return; - - std::vector available = market->availableItemsIds(mode); - std::set> toRemove; - for(auto item : items[0]) - if(!vstd::contains(available, item->id)) - toRemove.insert(item); - - removeItems(toRemove); - - // clear set to erase final instance of shared_ptr - we want to redraw screen only after it has been deleted - toRemove.clear(); + + rightTradePanel->deleteSlots(); redraw(); } -std::string CMarketplaceWindow::selectionSubtitle(bool Left) const +std::string CMarketplaceWindow::updateSlotSubtitle(bool Left) const { if(Left) { switch(itemsType[1]) { - case RESOURCE: - case CREATURE: + case EType::RESOURCE: + case EType::CREATURE: { int val = slider ? slider->getValue() * r1 @@ -950,7 +566,7 @@ std::string CMarketplaceWindow::selectionSubtitle(bool Left) const return std::to_string(val); } - case ARTIFACT_INSTANCE: + case EType::ARTIFACT_INSTANCE: return ((deal->isBlocked()) ? "0" : "1"); } } @@ -958,14 +574,14 @@ std::string CMarketplaceWindow::selectionSubtitle(bool Left) const { switch(itemsType[0]) { - case RESOURCE: + case EType::RESOURCE: if(slider) return std::to_string( slider->getValue() * r2 ); else return std::to_string(r2); - case ARTIFACT_TYPE: + case EType::ARTIFACT_TYPE: return ((deal->isBlocked()) ? "0" : "1"); - case PLAYER: + case EType::PLAYER: return (hRight ? CGI->generaltexth->capColors[hRight->id] : ""); } } @@ -979,26 +595,26 @@ Point CMarketplaceWindow::selectionOffset(bool Left) const { switch(itemsType[1]) { - case RESOURCE: - return Point(122, 446); - case CREATURE: + case EType::RESOURCE: + return Point(122, 448); + case EType::CREATURE: return Point(128, 450); - case ARTIFACT_INSTANCE: - return Point(134, 466); + case EType::ARTIFACT_INSTANCE: + return Point(134, 469); } } else { switch(itemsType[0]) { - case RESOURCE: + case EType::RESOURCE: if(mode == EMarketMode::ARTIFACT_RESOURCE) - return Point(410, 469); + return Point(410, 471); else - return Point(410, 446); - case ARTIFACT_TYPE: - return Point(425, 447); - case PLAYER: + return Point(410, 448); + case EType::ARTIFACT_TYPE: + return Point(411, 449); + case EType::PLAYER: return Point(417, 451); } } @@ -1012,49 +628,6 @@ void CMarketplaceWindow::resourceChanged() initSubs(true); } -void CMarketplaceWindow::getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const -{ - switch(type) - { - case RESOURCE: - dx = 82; - dy = 79; - x = 39; - y = 180; - h = 68; - w = 70; - break; - case PLAYER: - dx = 83; - dy = 118; - h = 64; - w = 58; - x = 44; - y = 83; - assert(Right); - break; - case CREATURE://45,123 - x = 45; - y = 123; - w = 58; - h = 64; - dx = 83; - dy = 98; - assert(!Right); - break; - case ARTIFACT_TYPE://45,123 - x = 340-289; - y = 180; - w = 44; - h = 44; - dx = 83; - dy = 79; - break; - } - - leftToRightOffset = 289; -} - void CMarketplaceWindow::updateTraderText() { if(readyToTrade) @@ -1104,431 +677,3 @@ void CMarketplaceWindow::updateTraderText() } traderText->setText(CGI->generaltexth->allTexts[gnrtxtnr]); } - -CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode) - : CTradeWindow(ImagePath::builtin(Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, onWindowClosed, Mode) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(Mode == EMarketMode::CREATURE_EXP) - { - //%s's Creatures - labels.push_back(std::make_shared(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, - boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated()))); - - //Altar of Sacrifice - labels.push_back(std::make_shared(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479])); - - //To sacrifice creatures, move them from your army on to the Altar and click Sacrifice - new CTextBox(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW); - - slider = std::make_shared(Point(231, 481), 137, std::bind(&CAltarWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - max = std::make_shared(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, slider)); - - sacrificedUnits.resize(GameConstants::ARMY_SIZE, 0); - sacrificeAll = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CAltarWindow::SacrificeAll,this)); - - initItems(true); - mimicCres(); - } - else - { - //Sacrifice artifacts for experience - labels.push_back(std::make_shared(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477])); - //%s's Creatures - labels.push_back(std::make_shared(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478])); - - sacrificeAll = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"), CGI->generaltexth->zelp[571], std::bind(&CAltarWindow::SacrificeAll,this)); - sacrificeAll->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty()); - sacrificeBackpack = std::make_shared(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"), CGI->generaltexth->zelp[570], std::bind(&CAltarWindow::SacrificeBackpack,this)); - sacrificeBackpack->block(hero->artifactsInBackpack.empty()); - - arts = std::make_shared(Point(-365, -12)); - arts->setHero(hero); - addSetAndCallbacks(arts); - - initItems(true); - initItems(false); - artIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), 0, 0, 281, 442); - artIcon->disable(); - } - - //Experience needed to reach next level - texts.push_back(std::make_shared(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); - //Total experience on the Altar - texts.push_back(std::make_shared(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); - - statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - ok = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), CGI->generaltexth->zelp[568], [&](){ close();}, EShortcut::GLOBAL_RETURN); - - deal = std::make_shared(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[585], std::bind(&CAltarWindow::makeDeal,this)); - - if(Mode == EMarketMode::CREATURE_EXP) - { - auto changeMode = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CTradeWindow::setMode,this, EMarketMode::ARTIFACT_EXP)); - if(Hero->getAlignment() == ::EAlignment::EVIL) - changeMode->block(true); - buttons.push_back(changeMode); - } - else if(Mode == EMarketMode::ARTIFACT_EXP) - { - auto changeMode = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CTradeWindow::setMode,this, EMarketMode::CREATURE_EXP)); - if(Hero->getAlignment() == ::EAlignment::GOOD) - changeMode->block(true); - buttons.push_back(changeMode); - } - - expPerUnit.resize(GameConstants::ARMY_SIZE, 0); - getExpValues(); - - expToLevel = std::make_shared(73, 475, FONT_SMALL, ETextAlignment::CENTER); - expOnAltar = std::make_shared(73, 543, FONT_SMALL, ETextAlignment::CENTER); - - setExpToLevel(); - calcTotalExp(); - blockTrade(); -} - -CAltarWindow::~CAltarWindow() = default; - -void CAltarWindow::getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const -{ - leftToRightOffset = 289; - x = 45; - y = 110; - w = 58; - h = 64; - dx = 83; - dy = 98; -} - -void CAltarWindow::sliderMoved(int to) -{ - if(hLeft) - sacrificedUnits[hLeft->serial] = to; - if(hRight) - updateRight(hRight); - - deal->block(!to); - calcTotalExp(); - redraw(); -} - -void CAltarWindow::makeDeal() -{ - if(mode == EMarketMode::CREATURE_EXP) - { - blockTrade(); - slider->scrollTo(0); - - std::vector ids; - std::vector toSacrifice; - - for(int i = 0; i < sacrificedUnits.size(); i++) - { - if(sacrificedUnits[i]) - { - ids.push_back(i); - toSacrifice.push_back(sacrificedUnits[i]); - } - } - - LOCPLINT->cb->trade(market, mode, ids, {}, toSacrifice, hero); - - for(int& val : sacrificedUnits) - val = 0; - - for(auto item : items[0]) - { - item->setType(CREATURE_PLACEHOLDER); - item->subtitle = ""; - } - } - else - { - std::vector positions; - for(const CArtifactInstance * art : arts->artifactsOnAltar) - { - positions.push_back(hero->getSlotByInstance(art)); - } - std::sort(positions.begin(), positions.end(), std::greater<>()); - - LOCPLINT->cb->trade(market, mode, positions, {}, {}, hero); - arts->artifactsOnAltar.clear(); - - for(auto item : items[0]) - { - item->setID(-1); - item->subtitle = ""; - } - - //arts->scrollBackpack(0); - deal->block(true); - } - - calcTotalExp(); -} - -void CAltarWindow::SacrificeAll() -{ - if(mode == EMarketMode::CREATURE_EXP) - { - bool movedAnything = false; - for(auto item : items[1]) - sacrificedUnits[item->serial] = hero->getStackCount(SlotID(item->serial)); - - sacrificedUnits[items[1].front()->serial]--; - - for(auto item : items[0]) - { - updateRight(item); - if(item->type == CREATURE) - movedAnything = true; - } - - deal->block(!movedAnything); - calcTotalExp(); - } - else - { - std::vector> artsForMove; - for(const auto& slotInfo : arts->visibleArtSet.artifactsWorn) - { - if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable()) - artsForMove.push_back(slotInfo.second.artifact); - } - for(auto artInst : artsForMove) - moveArtToAltar(nullptr, artInst); - arts->updateWornSlots(); - SacrificeBackpack(); - } - redraw(); -} - -void CAltarWindow::selectionChanged(bool side) -{ - if(mode != EMarketMode::CREATURE_EXP) - return; - - int stackCount = 0; - for (int i = 0; i < GameConstants::ARMY_SIZE; i++) - if(hero->getStackCount(SlotID(i)) > sacrificedUnits[i]) - stackCount++; - - slider->setAmount(hero->getStackCount(SlotID(hLeft->serial)) - (stackCount == 1)); - slider->block(!slider->getAmount()); - slider->scrollTo(sacrificedUnits[hLeft->serial]); - max->block(!slider->getAmount()); - selectOppositeItem(side); - readyToTrade = true; - redraw(); -} - -void CAltarWindow::selectOppositeItem(bool side) -{ - bool oppositeSide = !side; - int pos = vstd::find_pos(items[side], side ? hLeft : hRight); - int oppositePos = vstd::find_pos(items[oppositeSide], oppositeSide ? hLeft : hRight); - - if(pos >= 0 && pos != oppositePos) - { - if(oppositeSide) - hLeft = items[oppositeSide][pos]; - else - hRight = items[oppositeSide][pos]; - - selectionChanged(oppositeSide); - } -} - -void CAltarWindow::mimicCres() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - std::vector positions; - getPositionsFor(positions, false, CREATURE); - - for(auto item : items[1]) - { - auto hlp = std::make_shared(positions[item->serial].topLeft(), CREATURE_PLACEHOLDER, item->id, false, item->serial); - hlp->pos = positions[item->serial] + this->pos.topLeft(); - items[0].push_back(hlp); - } -} - -Point CAltarWindow::selectionOffset(bool Left) const -{ - if(Left) - return Point(150, 421); - else - return Point(396, 421); -} - -std::string CAltarWindow::selectionSubtitle(bool Left) const -{ - if(Left && slider && hLeft) - return std::to_string(slider->getValue()); - else if(!Left && hRight) - return hRight->subtitle; - else - return ""; -} - -void CAltarWindow::artifactsChanged(bool left) -{ - -} - -void CAltarWindow::garrisonChanged() -{ - if(mode != EMarketMode::CREATURE_EXP) - return; - - std::set> empty; - getEmptySlots(empty); - - removeItems(empty); - - initSubs(true); - getExpValues(); -} - -void CAltarWindow::getExpValues() -{ - int dump; - for(auto item : items[1]) - { - if(item->id >= 0) - market->getOffer(item->id, 0, dump, expPerUnit[item->serial], EMarketMode::CREATURE_EXP); - } -} - -void CAltarWindow::calcTotalExp() -{ - int val = 0; - if(mode == EMarketMode::CREATURE_EXP) - { - for (int i = 0; i < sacrificedUnits.size(); i++) - { - val += expPerUnit[i] * sacrificedUnits[i]; - } - } - else - { - auto artifactsOfHero = std::dynamic_pointer_cast(arts); - for(const CArtifactInstance * art : artifactsOfHero->artifactsOnAltar) - { - int dmp, valOfArt; - market->getOffer(art->artType->getId(), 0, dmp, valOfArt, mode); - val += valOfArt; //WAS val += valOfArt * arts->artifactsOnAltar.count(*i); - } - } - val = static_cast(hero->calculateXp(val)); - expOnAltar->setText(std::to_string(val)); -} - -void CAltarWindow::setExpToLevel() -{ - expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(hero->exp)+1) - hero->exp)); -} - -void CAltarWindow::blockTrade() -{ - hLeft = hRight = nullptr; - readyToTrade = false; - if(slider) - { - slider->block(true); - max->block(true); - } - deal->block(true); -} - -void CAltarWindow::updateRight(std::shared_ptr toUpdate) -{ - int val = sacrificedUnits[toUpdate->serial]; - toUpdate->setType(val ? CREATURE : CREATURE_PLACEHOLDER); - toUpdate->subtitle = val ? boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(val * expPerUnit[toUpdate->serial]))) : ""; //%s exp -} - -int CAltarWindow::firstFreeSlot() -{ - int ret = -1; - while(items[0][++ret]->id >= 0 && ret + 1 < items[0].size()); - return items[0][ret]->id == -1 ? ret : -1; -} - -void CAltarWindow::SacrificeBackpack() -{ - while(!arts->visibleArtSet.artifactsInBackpack.empty()) - { - if(!putOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact)) - break; - }; - calcTotalExp(); -} - -void CAltarWindow::artifactPicked() -{ - redraw(); -} - -void CAltarWindow::showAll(Canvas & to) -{ - CTradeWindow::showAll(to); - if(mode == EMarketMode::ARTIFACT_EXP && arts) - { - if(auto pickedArt = arts->getPickedArtifact()) - { - artIcon->setFrame(pickedArt->artType->getIconIndex()); - artIcon->showAll(to); - - int dmp, val; - market->getOffer(pickedArt->getTypeId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP); - val = static_cast(hero->calculateXp(val)); - - to.drawText(Point(304, 498), FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, std::to_string(val)); - } - } -} - -bool CAltarWindow::putOnAltar(std::shared_ptr altarSlot, const CArtifactInstance *art) -{ - if(!art->artType->isTradable()) //special art - { - logGlobal->warn("Cannot put special artifact on altar!"); - return false; - } - - if(!altarSlot || altarSlot->id != -1) - { - int slotIndex = firstFreeSlot(); - if(slotIndex < 0) - { - logGlobal->warn("No free slots on altar!"); - return false; - } - altarSlot = items[0][slotIndex]; - } - - int dmp, val; - market->getOffer(art->artType->getId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP); - val = static_cast(hero->calculateXp(val)); - - arts->artifactsOnAltar.insert(art); - arts->deleteFromVisible(art); - altarSlot->setArtInstance(art); - altarSlot->subtitle = std::to_string(val); - - deal->block(false); - return true; -} - -void CAltarWindow::moveArtToAltar(std::shared_ptr altarSlot, const CArtifactInstance *art) -{ - if(putOnAltar(altarSlot, art)) - { - CCS->curh->dragAndDropCursor(nullptr); - arts->unmarkSlots(); - } -} diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index 45a8cfee5..68195144d 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -9,76 +9,21 @@ */ #pragma once +#include "../widgets/markets/CTradeBase.h" #include "../widgets/CWindowWithArtifacts.h" #include "CWindowObject.h" -#include "../../lib/FunctionList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class IMarket; - -VCMI_LIB_NAMESPACE_END class CSlider; -class CTextBox; -class CPicture; class CGStatusBar; -class CTradeWindow : public CWindowObject, public CWindowWithArtifacts //base for markets and altar of sacrifice +class CTradeWindow : public CTradeBase, public CWindowObject, public CWindowWithArtifacts //base for markets and altar of sacrifice { public: - enum EType - { - RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE - }; - - class CTradeableItem : public CIntObject, public std::enable_shared_from_this - { - std::shared_ptr image; - AnimationPath getFilename(); - int getIndex(); - public: - const CArtifactInstance * hlp; //holds ptr to artifact instance id type artifact - EType type; - int id; - const int serial; - const bool left; - std::string subtitle; //empty if default - - void setType(EType newType); - void setID(int newID); - - const CArtifactInstance * getArtInstance() const; - void setArtInstance(const CArtifactInstance * art); - - CFunctionList callback; - bool downSelection; - - void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to); - - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - void showAll(Canvas & to) override; - void clickPressed(const Point & cursorPosition) override; - std::string getName(int number = -1) const; - CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial); - }; - - const IMarket * market; - const CGHeroInstance * hero; - - //all indexes: 1 = left, 0 = right - std::array>, 2> items; - - //highlighted items (nullptr if no highlight) - std::shared_ptr hLeft; - std::shared_ptr hRight; EType itemsType[2]; EMarketMode mode; std::shared_ptr ok; std::shared_ptr max; - std::shared_ptr deal; std::shared_ptr slider; //for choosing amount to be exchanged bool readyToTrade; @@ -91,28 +36,18 @@ public: void initSubs(bool Left); void initTypes(); void initItems(bool Left); - std::vector *getItemsIds(bool Left); //nullptr if default - void getPositionsFor(std::vector &poss, bool Left, EType type) const; - void removeItems(const std::set> & toRemove); - void removeItem(std::shared_ptr item); - void getEmptySlots(std::set> & toRemove); void setMode(EMarketMode Mode); //mode setter - void artifactSelected(CHeroArtPlace *slot); //used when selling artifacts -> called when user clicked on artifact slot - - virtual void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const = 0; + void artifactSelected(CArtPlace * slot); //used when selling artifacts -> called when user clicked on artifact slot virtual void selectionChanged(bool side) = 0; //true == left virtual Point selectionOffset(bool Left) const = 0; - virtual std::string selectionSubtitle(bool Left) const = 0; - virtual void garrisonChanged() = 0; + virtual std::string updateSlotSubtitle(bool Left) const = 0; + virtual void updateGarrison() = 0; virtual void artifactsChanged(bool left) = 0; protected: std::function onWindowClosed; std::shared_ptr statusBar; - std::vector> labels; std::vector> images; - std::vector> buttons; - std::vector> texts; }; class CMarketplaceWindow : public CTradeWindow @@ -130,64 +65,16 @@ public: void setMax(); void sliderMoved(int to); - void makeDeal(); + void makeDeal() override; void selectionChanged(bool side) override; //true == left CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); ~CMarketplaceWindow(); Point selectionOffset(bool Left) const override; - std::string selectionSubtitle(bool Left) const override; + std::string updateSlotSubtitle(bool Left) const override; - void garrisonChanged() override; //removes creatures with count 0 from the list (apparently whole stack has been sold) + void updateGarrison() override; //removes creatures with count 0 from the list (apparently whole stack has been sold) void artifactsChanged(bool left) override; void resourceChanged(); - - void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const override; void updateTraderText(); }; - -class CAltarWindow : public CTradeWindow -{ - std::shared_ptr artIcon; -public: - std::vector sacrificedUnits; //[slot_nr] -> how many creatures from that slot will be sacrificed - std::vector expPerUnit; - - std::shared_ptr sacrificeAll; - std::shared_ptr sacrificeBackpack; - std::shared_ptr expToLevel; - std::shared_ptr expOnAltar; - std::shared_ptr arts; - - CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); - ~CAltarWindow(); - - void getExpValues(); - - void selectionChanged(bool side) override; //true == left - void selectOppositeItem(bool side); - void SacrificeAll(); - void SacrificeBackpack(); - - void putOnAltar(int backpackIndex); - bool putOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art); - void makeDeal(); - void showAll(Canvas & to) override; - - void blockTrade(); - void sliderMoved(int to); - void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const override; - void mimicCres(); - - Point selectionOffset(bool Left) const override; - std::string selectionSubtitle(bool Left) const override; - void garrisonChanged() override; - void artifactsChanged(bool left) override; - void calcTotalExp(); - void setExpToLevel(); - void updateRight(std::shared_ptr toUpdate); - - void artifactPicked(); - int firstFreeSlot(); - void moveArtToAltar(std::shared_ptr, const CArtifactInstance * art); -}; diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp new file mode 100644 index 000000000..72f945547 --- /dev/null +++ b/client/windows/CTutorialWindow.cpp @@ -0,0 +1,123 @@ +/* + * CTutorialWindow.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CTutorialWindow.h" + +#include "../eventsSDL/InputHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CondSh.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../CPlayerInterface.h" +#include "../CGameInfo.h" +#include "../CVideoHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Images.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" +#include "../render/Canvas.h" + +CTutorialWindow::CTutorialWindow(const TutorialMode & m) + : CWindowObject(BORDERED, ImagePath::builtin("DIBOXBCK")), mode { m }, page { 0 } +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = Rect(pos.x, pos.y, 380, 400); //video: 320x240 + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); + + updateShadow(); + + center(); + + addUsedEvents(LCLICK); + + if(mode == TutorialMode::TOUCH_ADVENTUREMAP) videos = { "RightClick", "MapPanning", "MapZooming", "RadialWheel" }; + else if(mode == TutorialMode::TOUCH_BATTLE) videos = { "BattleDirection", "BattleDirectionAbort", "AbortSpell" }; + + labelTitle = std::make_shared(190, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.tutorialWindow.title")); + labelInformation = std::make_shared(Rect(5, 40, 370, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, ""); + buttonOk = std::make_shared(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::exit, this), EShortcut::GLOBAL_ACCEPT); //62x28 + buttonLeft = std::make_shared(Point(5, 217), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 + buttonRight = std::make_shared(Point(352, 217), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 + + setContent(); +} + +void CTutorialWindow::setContent() +{ + video = "tutorial/" + videos[page]; + + buttonLeft->block(page<1); + buttonRight->block(page>videos.size() - 2); + + labelInformation->setText(CGI->generaltexth->translate("vcmi.tutorialWindow.decription." + videos[page])); +} + +void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) +{ + if(GH.input().hasTouchInputDevice() && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool()) + { + if(LOCPLINT) + LOCPLINT->showingDialog->set(true); + GH.windows().pushWindow(std::make_shared(m)); + + Settings s = persistentStorage.write["gui"]["tutorialCompleted" + std::to_string(m)]; + s->Bool() = true; + } +} + +void CTutorialWindow::exit() +{ + if(LOCPLINT) + LOCPLINT->showingDialog->setn(false); + + close(); +} + +void CTutorialWindow::next() +{ + page++; + setContent(); + deactivate(); + activate(); +} + +void CTutorialWindow::previous() +{ + page--; + setContent(); + deactivate(); + activate(); +} + +void CTutorialWindow::show(Canvas & to) +{ + CCS->videoh->update(pos.x + 30, pos.y + 120, to.getInternalSurface(), true, false, + [&]() + { + CCS->videoh->close(); + CCS->videoh->open(VideoPath::builtin(video)); + }); + + CIntObject::show(to); +} + +void CTutorialWindow::activate() +{ + CCS->videoh->open(VideoPath::builtin(video)); + CIntObject::activate(); +} + +void CTutorialWindow::deactivate() +{ + CCS->videoh->close(); +} diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h new file mode 100644 index 000000000..d002f9aed --- /dev/null +++ b/client/windows/CTutorialWindow.h @@ -0,0 +1,54 @@ +/* + * CTutorialWindow.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../windows/CWindowObject.h" + +class CFilledTexture; +class CButton; +class CLabel; +class CMultiLineLabel; + +enum TutorialMode +{ + TOUCH_ADVENTUREMAP, + TOUCH_BATTLE +}; + +class CTutorialWindow : public CWindowObject +{ + TutorialMode mode; + std::shared_ptr background; + + std::shared_ptr buttonOk; + std::shared_ptr buttonLeft; + std::shared_ptr buttonRight; + + std::shared_ptr labelTitle; + std::shared_ptr labelInformation; + + std::string video; + std::vector videos; + + int page; + + void exit(); + void next(); + void previous(); + void setContent(); + +public: + CTutorialWindow(const TutorialMode & m); + static void openWindowFirstTime(const TutorialMode & m); + + void show(Canvas & to) override; + void activate() override; + void deactivate() override; +}; diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index c21c1a219..bf6845d07 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -39,7 +39,8 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName, Point ce options(options_), background(createBg(imageName, options & PLAYER_COLORED)) { - assert(parent == nullptr); //Safe to remove, but windows should not have parent + if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) + assert(parent == nullptr); //Safe to remove, but windows should not have parent defActions = 255-DISPOSE; @@ -60,7 +61,8 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName): options(options_), background(createBg(imageName, options_ & PLAYER_COLORED)) { - assert(parent == nullptr); //Safe to remove, but windows should not have parent + if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) + assert(parent == nullptr); //Safe to remove, but windows should not have parent defActions = 255-DISPOSE; @@ -230,7 +232,7 @@ void CWindowObject::showAll(Canvas & to) CIntObject::showAll(to); if ((options & BORDERED) && (pos.dimensions() != GH.screenDimensions())) - CMessage::drawBorder(color, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); + CMessage::drawBorder(color, to, pos.w+28, pos.h+29, pos.x-14, pos.y-15); } bool CWindowObject::isPopupWindow() const diff --git a/client/windows/CWindowObject.h b/client/windows/CWindowObject.h index dd49f107b..52111aee6 100644 --- a/client/windows/CWindowObject.h +++ b/client/windows/CWindowObject.h @@ -16,8 +16,6 @@ class CGStatusBar; class CWindowObject : public WindowBase { - std::shared_ptr createBg(const ImagePath & imageName, bool playerColored); - std::vector> shadowParts; void setShadow(bool on); @@ -32,13 +30,15 @@ protected: //To display border void updateShadow(); void setBackground(const ImagePath & filename); + std::shared_ptr createBg(const ImagePath & imageName, bool playerColored); public: enum EOptions { PLAYER_COLORED=1, //background will be player-colored RCLICK_POPUP=2, // window will behave as right-click popup BORDERED=4, // window will have border if current resolution is bigger than size of window - SHADOW_DISABLED=8 //this window won't display any shadow + SHADOW_DISABLED=8, //this window won't display any shadow + NEEDS_ANIMATED_BACKGROUND=16 //there are videos in the background that have to be played }; /* diff --git a/client/windows/CreaturePurchaseCard.h b/client/windows/CreaturePurchaseCard.h index 6cb6d4a93..e5a3388c2 100644 --- a/client/windows/CreaturePurchaseCard.h +++ b/client/windows/CreaturePurchaseCard.h @@ -58,8 +58,11 @@ private: static constexpr int CREATURE_HEIGHT = 132; }; - std::shared_ptr maxButton, minButton, creatureSwitcher; - std::shared_ptr availableAmount, purchaseAmount; + std::shared_ptr maxButton; + std::shared_ptr minButton; + std::shared_ptr creatureSwitcher; + std::shared_ptr availableAmount; + std::shared_ptr purchaseAmount; std::shared_ptr picture; std::shared_ptr cost; std::vector upgradesID; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index dd06d2dab..2f42729d5 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -17,6 +17,8 @@ #include "InfoWindows.h" #include "../CGameInfo.h" +#include "../CServerHandler.h" +#include "../Client.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CVideoHandler.h" @@ -37,6 +39,7 @@ #include "../render/Canvas.h" #include "../render/CAnimation.h" #include "../render/IRenderHandler.h" +#include "../render/IImage.h" #include "../../CCallback.h" @@ -48,6 +51,7 @@ #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/gameState/CGameState.h" #include "../lib/gameState/SThievesGuildInfo.h" +#include "../lib/gameState/TavernHeroesPool.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/GameSettings.h" @@ -159,7 +163,7 @@ void CRecruitmentWindow::buy() else { std::string txt; - if(dst->ID == Obj::HERO) + if(dwelling->ID != Obj::TOWN) { txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); @@ -324,8 +328,8 @@ CSplitWindow::CSplitWindow(const CCreature * creature, std::function(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true)); - rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false)); + leftInput = std::make_shared(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true), ETextAlignment::CENTER, true); + rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false), ETextAlignment::CENTER, true); //add filters to allow only number input leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax); @@ -401,7 +405,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std std::vector> comps; for(auto & skill : skills) { - auto comp = std::make_shared(CComponent::secskill, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); + auto comp = std::make_shared(ComponentType::SEC_SKILL, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); comp->onChoose = std::bind(&CLevelWindow::close, this); comps.push_back(comp); } @@ -409,7 +413,9 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std box = std::make_shared(comps, Rect(75, 300, pos.w - 150, 100)); } - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 170, 66); + portrait = std::make_shared(170, 66, hero); + portrait->addClickCallback(nullptr); + portrait->addRClickCallback([hero](){ GH.windows().createAndPushWindow(std::make_shared(hero)); }); ok = std::make_shared(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); //%s has gained a level. @@ -419,7 +425,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std std::string levelTitleText = CGI->generaltexth->translate("core.genrltxt.445"); boost::replace_first(levelTitleText, "%s", hero->getNameTranslated()); boost::replace_first(levelTitleText, "%d", std::to_string(hero->level)); - boost::replace_first(levelTitleText, "%s", hero->type->heroClass->getNameTranslated()); + boost::replace_first(levelTitleText, "%s", hero->getClassNameTranslated()); levelTitle = std::make_shared(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText); @@ -441,7 +447,8 @@ CLevelWindow::~CLevelWindow() CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPTAVERN")), onWindowClosed(onWindowClosed), - tavernObj(TavernObj) + tavernObj(TavernObj), + heroToInvite(nullptr) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -457,8 +464,8 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func oldSelected = -1; - h1 = std::make_shared(selected, 0, 72, 299, h[0]); - h2 = std::make_shared(selected, 1, 162, 299, h[1]); + h1 = std::make_shared(selected, 0, 72, 299, h[0], [this]() { if(!recruit->isBlocked()) recruitb(); }); + h2 = std::make_shared(selected, 1, 162, 299, h[1], [this]() { if(!recruit->isBlocked()) recruitb(); }); title = std::make_shared(197, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[37]); cost = std::make_shared(320, 328, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(GameConstants::HERO_GOLD_COST)); @@ -474,24 +481,24 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold { - recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[0]); //Cannot afford a Hero + recruit->addHoverText(EButtonState::NORMAL, CGI->generaltexth->tavernInfo[0]); //Cannot afford a Hero recruit->block(true); } else if(LOCPLINT->cb->howManyHeroes(true) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP)) { //Cannot recruit. You already have %d Heroes. - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(true))); + recruit->addHoverText(EButtonState::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(true))); recruit->block(true); } else if(LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) { //Cannot recruit. You already have %d Heroes. - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); + recruit->addHoverText(EButtonState::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); recruit->block(true); } else if(dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero) { - recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. + recruit->addHoverText(EButtonState::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. recruit->block(true); } else @@ -505,6 +512,34 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func CCS->videoh->open(townObj->town->clientInfo.tavernVideo); else CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); + + addInvite(); +} + +void CTavernWindow::addInvite() +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(!VLC->settings()->getBoolean(EGameSettings::HEROES_TAVERN_INVITE)) + return; + + const auto & heroesPool = CSH->client->gameState()->heroesPool; + for(auto & elem : heroesPool->unusedHeroesFromPool()) + { + bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, tavernObj->getOwner()); + if(heroAvailable) + inviteableHeroes[elem.first] = elem.second; + } + + if(!inviteableHeroes.empty()) + { + if(!heroToInvite) + heroToInvite = (*RandomGeneratorUtil::nextItem(inviteableHeroes, CRandomGenerator::getDefault())).second; + + inviteHero = std::make_shared(170, 444, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.tavernWindow.inviteHero")); + inviteHeroImage = std::make_shared(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[heroToInvite->getHeroType()]->imageIndex, 0, 245, 428); + inviteHeroImageArea = std::make_shared(Rect(245, 428, 48, 32), [this](){ GH.windows().createAndPushWindow(inviteableHeroes, [this](CGHeroInstance* h){ heroToInvite = h; addInvite(); }); }, [this](){ GH.windows().createAndPushWindow(std::make_shared(heroToInvite)); }); + } } void CTavernWindow::recruitb() @@ -512,7 +547,7 @@ void CTavernWindow::recruitb() const CGHeroInstance *toBuy = (selected ? h2 : h1)->h; const CGObjectInstance *obj = tavernObj; - LOCPLINT->cb->recruitHero(obj, toBuy); + LOCPLINT->cb->recruitHero(obj, toBuy, heroToInvite ? heroToInvite->getHeroType() : HeroTypeID::NONE); close(); } @@ -551,7 +586,7 @@ void CTavernWindow::show(Canvas & to) //Recruit %s the %s if (!recruit->isBlocked()) - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->type->heroClass->getNameTranslated())); + recruit->addHoverText(EButtonState::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->getClassNameTranslated())); } @@ -567,15 +602,23 @@ void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition) *_sel = _id; } +void CTavernWindow::HeroPortrait::clickDouble(const Point & cursorPosition) +{ + clickPressed(cursorPosition); + + if(onChoose) + onChoose(); +} + void CTavernWindow::HeroPortrait::showPopupWindow(const Point & cursorPosition) { if(h) GH.windows().createAndPushWindow(std::make_shared(h)); } -CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H) - : CIntObject(LCLICK | SHOW_POPUP | HOVER), - h(H), _sel(&sel), _id(id) +CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H, std::function OnChoose) + : CIntObject(LCLICK | DOUBLECLICK | SHOW_POPUP | HOVER), + h(H), _sel(&sel), _id(id), onChoose(OnChoose) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); h = H; @@ -597,7 +640,7 @@ CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const description = CGI->generaltexth->allTexts[215]; boost::algorithm::replace_first(description, "%s", h->getNameTranslated()); boost::algorithm::replace_first(description, "%d", std::to_string(h->level)); - boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated()); + boost::algorithm::replace_first(description, "%s", h->getClassNameTranslated()); boost::algorithm::replace_first(description, "%d", std::to_string(artifs)); portrait = std::make_shared(AnimationPath::builtin("portraitsLarge"), h->getIconIndex()); @@ -613,6 +656,33 @@ void CTavernWindow::HeroPortrait::hover(bool on) GH.statusbar()->clear(); } +CTavernWindow::HeroSelector::HeroSelector(std::map InviteableHeroes, std::function OnChoose) + : CWindowObject(BORDERED), inviteableHeroes(InviteableHeroes), onChoose(OnChoose) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = Rect(0, 0, 16 * 48, (inviteableHeroes.size() / 16 + (inviteableHeroes.size() % 16 != 0)) * 32); + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); + + int x = 0; + int y = 0; + for(auto & h : inviteableHeroes) + { + portraits.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[h.first]->imageIndex, 0, x * 48, y * 32)); + portraitAreas.push_back(std::make_shared(Rect(x * 48, y * 32, 48, 32), [this, h](){ close(); onChoose(inviteableHeroes[h.first]); }, [this, h](){ GH.windows().createAndPushWindow(std::make_shared(inviteableHeroes[h.first])); })); + + if(x > 0 && x % 15 == 0) + { + x = 0; + y++; + } + else + x++; + } + + center(); +} + static const std::string QUICK_EXCHANGE_MOD_PREFIX = "quick-exchange"; static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADEQE"; @@ -637,7 +707,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, auto genTitle = [](const CGHeroInstance * h) { boost::format fmt(CGI->generaltexth->allTexts[138]); - fmt % h->getNameTranslated() % h->level % h->type->heroClass->getNameTranslated(); + fmt % h->getNameTranslated() % h->level % h->getClassNameTranslated(); return boost::str(fmt); }; @@ -677,9 +747,9 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } - artifs[0] = std::make_shared(Point(-334, 150)); + artifs[0] = std::make_shared(Point(-334, 151)); artifs[0]->setHero(heroInst[0]); - artifs[1] = std::make_shared(Point(98, 150)); + artifs[1] = std::make_shared(Point(98, 151)); artifs[1]->setHero(heroInst[1]); addSetAndCallbacks(artifs[0]); @@ -693,9 +763,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, else primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32)); primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g]; - primSkillAreas[g]->type = g; - primSkillAreas[g]->bonusValue = 0; - primSkillAreas[g]->baseType = 0; + primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g)); primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1]; boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]); } @@ -708,14 +776,11 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, //secondary skill's clickable areas for(int g=0; gsecSkills.size(); ++g) { - int skill = hero->secSkills[g].first, - level = hero->secSkills[g].second; // <1, 3> + SecondarySkill skill = hero->secSkills[g].first; + int level = hero->secSkills[g].second; // <1, 3> secSkillAreas[b].push_back(std::make_shared()); secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) ); - secSkillAreas[b][g]->baseType = 1; - - secSkillAreas[b][g]->type = skill; - secSkillAreas[b][g]->bonusValue = level; + secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level); secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21]; @@ -813,12 +878,12 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.swapArtifacts(equipped, baclpack);})); moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(false, equipped, baclpack);})); - backpackButtonLeft = std::make_shared(Point(325, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + backpackButtonLeft = std::make_shared(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), std::bind(openBackpack, heroInst[0])); - backpackButtonLeft->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); - backpackButtonRight = std::make_shared(Point(419, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + backpackButtonLeft->setOverlay(std::make_shared(ImagePath::builtin("heroWindow/backpackButtonIcon"))); + backpackButtonRight = std::make_shared(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), std::bind(openBackpack, heroInst[1])); - backpackButtonRight->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + backpackButtonRight->setOverlay(std::make_shared(ImagePath::builtin("heroWindow/backpackButtonIcon"))); auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID(); auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID(); @@ -866,6 +931,11 @@ void CExchangeWindow::updateGarrisons() updateWidgets(); } +bool CExchangeWindow::holdsGarrison(const CArmedInstance * army) +{ + return garr->upperArmy() == army || garr->lowerArmy() == army; +} + void CExchangeWindow::questlog(int whichHero) { CCS->curh->dragAndDropCursor(nullptr); @@ -993,7 +1063,7 @@ void CTransformerWindow::makeDeal() for(auto & elem : items) { if(!elem->left) - LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, elem->id, 0, 0, hero); + LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, SlotID(elem->id), {}, {}, hero); } } @@ -1013,6 +1083,11 @@ void CTransformerWindow::updateGarrisons() item->update(); } +bool CTransformerWindow::holdsGarrison(const CArmedInstance * army) +{ + return army == hero; +} + CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("SKTRNBK")), hero(_hero), @@ -1082,7 +1157,7 @@ void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) { - CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(CComponent::secskill, ID, 1)); + CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(ComponentType::SEC_SKILL, ID, 1)); } void CUniversityWindow::CItem::hover(bool on) @@ -1151,10 +1226,10 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket clerkSpeech = std::make_shared(speechStr, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); title = std::make_shared(231, 26, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, titleStr); - std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); + std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); for(int i=0; i(this, goods[i], 54+i*104, 234)); + items.push_back(std::make_shared(this, goods[i].as(), 54+i*104, 234)); cancel = std::make_shared(Point(200, 313), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); @@ -1168,13 +1243,13 @@ void CUniversityWindow::close() CStatusbarWindow::close(); } -void CUniversityWindow::makeDeal(int skill) +void CUniversityWindow::makeDeal(SecondarySkill skill) { - LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, 6, skill, 1, hero); + LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, GameResID(GameResID::GOLD), skill, 1, hero); } -CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bool available) +CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, SecondarySkill SKILL, bool available) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS2.PCX")), owner(owner_) { @@ -1209,7 +1284,7 @@ CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bo statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); } -void CUnivConfirmWindow::makeDeal(int skill) +void CUnivConfirmWindow::makeDeal(SecondarySkill skill) { owner->makeDeal(skill); close(); @@ -1256,6 +1331,11 @@ void CGarrisonWindow::updateGarrisons() garr->recreateSlots(); } +bool CGarrisonWindow::holdsGarrison(const CArmedInstance * army) +{ + return garr->upperArmy() == army || garr->lowerArmy() == army; +} + CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("APHLFTBK")), fort(object), @@ -1275,9 +1355,7 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI for(int i = 0; i < slotsCount; i++) { - upgrade[i] = std::make_shared(Point(107 + i * 76, 171), AnimationPath(), CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); - for(auto image : { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" }) - upgrade[i]->addImage(AnimationPath::builtin(image)); + upgrade[i] = std::make_shared(Point(107 + i * 76, 171), AnimationPath::builtin("APHLF1R"), CButton::tooltip(getTextForSlot(SlotID(i))), [this, i](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); for(int j : {0,1}) { @@ -1286,19 +1364,25 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI } } - upgradeAll = std::make_shared(Point(30, 231), AnimationPath(), CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); - for(auto image : { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" }) - upgradeAll->addImage(AnimationPath::builtin(image)); + upgradeAll = std::make_shared(Point(30, 231), AnimationPath::builtin("APHLF4R"), CButton::tooltip(CGI->generaltexth->allTexts[432]), [this](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); - quit = std::make_shared(Point(294, 275), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT); + quit = std::make_shared(Point(294, 275), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), [this](){close();}, EShortcut::GLOBAL_ACCEPT); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); garr = std::make_shared(Point(108, 60), 18, Point(), hero, nullptr); updateGarrisons(); } +bool CHillFortWindow::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + void CHillFortWindow::updateGarrisons() { + constexpr std::array slotImages = { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" }; + constexpr std::array allImages = { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" }; + std::array costs;// costs [slot ID] [resource ID] = resource count for upgrade TResources totalSum; // totalSum[resource ID] = value @@ -1319,9 +1403,9 @@ void CHillFortWindow::updateGarrisons() } currState[i] = newState; - upgrade[i]->setIndex(currState[i] == -1 ? 0 : currState[i]); + upgrade[i]->setImage(AnimationPath::builtin(currState[i] == -1 ? slotImages[0] : slotImages[currState[i]])); upgrade[i]->block(currState[i] == -1); - upgrade[i]->addHoverText(CButton::NORMAL, getTextForSlot(SlotID(i))); + upgrade[i]->addHoverText(EButtonState::NORMAL, getTextForSlot(SlotID(i))); } //"Upgrade all" slot @@ -1341,7 +1425,7 @@ void CHillFortWindow::updateGarrisons() } currState[slotsCount] = newState; - upgradeAll->setIndex(newState); + upgradeAll->setImage(AnimationPath::builtin(allImages[newState])); garr->recreateSlots(); @@ -1473,7 +1557,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): //data for information table: // fields[row][column] = list of id's of players for this box - static std::vector< std::vector< PlayerColor > > SThievesGuildInfo::* fields[] = + constexpr std::vector< std::vector< PlayerColor > > SThievesGuildInfo::* fields[] = { &SThievesGuildInfo::numOfTowns, &SThievesGuildInfo::numOfHeroes, &SThievesGuildInfo::gold, &SThievesGuildInfo::woodOre, &SThievesGuildInfo::mercSulfCrystGems, &SThievesGuildInfo::obelisks, &SThievesGuildInfo::artifacts, &SThievesGuildInfo::army, &SThievesGuildInfo::income }; @@ -1564,7 +1648,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): counter = 0; for(auto & it : tgi.bestCreature) { - if(it.second >= 0) + if(it.second != CreatureID::NONE) bestCreatures.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), it.second+2, 0, 255 + 66 * counter, 479)); counter++; } @@ -1590,11 +1674,13 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): } CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::string _text) - : CIntObject(LCLICK | DOUBLECLICK), + : CIntObject(LCLICK | DOUBLECLICK | RCLICK_POPUP), parent(_parent), index(_id) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if(parent->images.size() > index) + icon = std::make_shared(parent->images[index], Point(1, 1)); border = std::make_shared(ImagePath::builtin("TPGATES")); pos = border->pos; @@ -1617,6 +1703,9 @@ void CObjectListWindow::CItem::select(bool on) void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition) { parent->changeSelection(index); + + if(parent->onClicked) + parent->onClicked(index); } void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) @@ -1624,10 +1713,17 @@ void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) parent->elementSelected(); } -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) +void CObjectListWindow::CItem::showPopupWindow(const Point & cursorPosition) +{ + if(parent->onPopup) + parent->onPopup(index); +} + +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), onSelect(Callback), - selected(initialSelection) + selected(initialSelection), + images(images) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); items.reserve(_items.size()); @@ -1640,10 +1736,11 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, std::share init(titleWidget_, _title, _descr); } -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection, std::vector> images) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), onSelect(Callback), - selected(initialSelection) + selected(initialSelection), + images(images) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); items.reserve(_items.size()); @@ -1721,7 +1818,7 @@ void CObjectListWindow::changeSelection(size_t which) selected = which; } -void CObjectListWindow::keyPressed (EShortcut key) +void CObjectListWindow::keyPressed(EShortcut key) { int sel = static_cast(selected); diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 5580c5a36..d118e8203 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -35,6 +35,10 @@ class CGStatusBar; class CTextBox; class CGarrisonInt; class CGarrisonSlot; +class CHeroArea; +class CAnimImage; +class CFilledTexture; +class IImage; enum class EUserEvent; @@ -129,7 +133,7 @@ public: /// Raised up level window where you can select one out of two skills class CLevelWindow : public CWindowObject { - std::shared_ptr portrait; + std::shared_ptr portrait; std::shared_ptr ok; std::shared_ptr mainTitle; std::shared_ptr levelTitle; @@ -154,6 +158,7 @@ class CObjectListWindow : public CWindowObject CObjectListWindow * parent; std::shared_ptr text; std::shared_ptr border; + std::shared_ptr icon; public: const size_t index; CItem(CObjectListWindow * parent, size_t id, std::string text); @@ -161,12 +166,14 @@ class CObjectListWindow : public CWindowObject void select(bool on); void clickPressed(const Point & cursorPosition) override; void clickDouble(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; }; std::function onSelect;//called when OK button is pressed, returns id of selected item. std::shared_ptr titleWidget; std::shared_ptr title; std::shared_ptr descr; + std::vector> images; std::shared_ptr list; std::shared_ptr ok; @@ -180,12 +187,14 @@ public: size_t selected;//index of currently selected item std::function onExit;//optional exit callback + std::function onPopup;//optional popup callback + std::function onClicked;//optional if clicked on item callback /// Callback will be called when OK button is pressed, returns id of selected item. initState = initially selected item /// Image can be nullptr ///item names will be taken from map objects - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0, std::vector> images = {}); std::shared_ptr genItem(size_t index); void elementSelected();//call callback and close this window @@ -205,10 +214,13 @@ public: std::string description; // "XXX is a level Y ZZZ with N artifacts" const CGHeroInstance * h; + std::function onChoose; + void clickPressed(const Point & cursorPosition) override; + void clickDouble(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; void hover (bool on) override; - HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H); + HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H, std::function OnChoose = nullptr); private: int *_sel; @@ -217,6 +229,21 @@ public: std::shared_ptr portrait; }; + class HeroSelector : public CWindowObject + { + public: + std::shared_ptr background; + + HeroSelector(std::map InviteableHeroes, std::function OnChoose); + + private: + std::map inviteableHeroes; + std::function onChoose; + + std::vector> portraits; + std::vector> portraitAreas; + }; + //recruitable heroes std::shared_ptr h1; std::shared_ptr h2; //recruitable heroes @@ -236,6 +263,13 @@ public: std::shared_ptr heroDescription; std::shared_ptr rumor; + + std::shared_ptr inviteHero; + std::shared_ptr inviteHeroImage; + std::shared_ptr inviteHeroImageArea; + std::map inviteableHeroes; + CGHeroInstance* heroToInvite; + void addInvite(); CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); ~CTavernWindow(); @@ -290,6 +324,7 @@ public: std::array, 2> artifs; void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void questlog(int whichHero); //questlog button callback; whichHero: 0 - left, 1 - right @@ -362,6 +397,7 @@ public: void addAll(); void close() override; void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); }; @@ -375,7 +411,7 @@ class CUniversityWindow : public CStatusbarWindow std::shared_ptr name; std::shared_ptr level; public: - int ID;//id of selected skill + SecondarySkill ID;//id of selected skill CUniversityWindow * parent; void showAll(Canvas & to) override; @@ -403,7 +439,7 @@ class CUniversityWindow : public CStatusbarWindow public: CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed); - void makeDeal(int skill); + void makeDeal(SecondarySkill skill); void close(); }; @@ -422,10 +458,10 @@ class CUnivConfirmWindow : public CStatusbarWindow std::shared_ptr costIcon; std::shared_ptr cost; - void makeDeal(int skill); + void makeDeal(SecondarySkill skill); public: - CUnivConfirmWindow(CUniversityWindow * PARENT, int SKILL, bool available); + CUnivConfirmWindow(CUniversityWindow * PARENT, SecondarySkill SKILL, bool available); }; /// Garrison window where you can take creatures out of the hero to place it on the garrison @@ -443,6 +479,7 @@ public: CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; }; /// Hill fort is the building where you can upgrade units @@ -482,6 +519,7 @@ private: public: CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object); void updateGarrisons() override;//update buttons after garrison changes + bool holdsGarrison(const CArmedInstance * army) override; }; class CThievesGuildWindow : public CStatusbarWindow diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 320aabef8..59e95ea21 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -11,83 +11,51 @@ #include "InfoWindows.h" #include "../CGameInfo.h" -#include "../PlayerLocalState.h" #include "../CPlayerInterface.h" -#include "../CMusicHandler.h" +#include "../PlayerLocalState.h" -#include "../widgets/CComponent.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/Buttons.h" -#include "../widgets/TextControls.h" -#include "../gui/CGuiHandler.h" -#include "../gui/WindowHandler.h" -#include "../battle/BattleInterface.h" -#include "../battle/BattleInterfaceClasses.h" #include "../adventureMap/AdventureMapInterface.h" -#include "../windows/CMessage.h" -#include "../render/Canvas.h" -#include "../renderSDL/SDL_Extensions.h" +#include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" #include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/CComponent.h" +#include "../widgets/Images.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/TextControls.h" +#include "../windows/CMessage.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CondSh.h" -#include "../../lib/CGeneralTextHandler.h" //for Unicode related stuff +#include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/mapObjects/CGCreature.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/gameState/InfoAboutArmy.h" -#include +CSelWindow::CSelWindow( const std::string & Text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector>> & Buttons, QueryID askID) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); -void CSimpleWindow::show(Canvas & to) -{ - if(bitmap) - CSDL_Ext::blitAt(bitmap, pos.x, pos.y, to.getInternalSurface()); -} -CSimpleWindow::~CSimpleWindow() -{ - if (bitmap) - { - SDL_FreeSurface(bitmap); - bitmap=nullptr; - } -} + backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); -void CSelWindow::selectionChange(unsigned to) -{ - for (unsigned i=0;i(components[i]); - if (!pom) - continue; - pom->select(i==to); - } - redraw(); -} - -CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); ID = askID; - for (int i = 0; i < Buttons.size(); i++) + for(int i = 0; i < Buttons.size(); i++) { buttons.push_back(std::make_shared(Point(0, 0), Buttons[i].first, CButton::tooltip(), Buttons[i].second)); - if (!i && askID.getNum() >= 0) + if(!i && askID.getNum() >= 0) buttons.back()->addCallback(std::bind(&CSelWindow::madeChoice, this)); buttons[i]->addCallback(std::bind(&CInfoWindow::close, this)); //each button will close the window apart from call-defined actions } text = std::make_shared(Text, Rect(0, 0, 250, 100), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - if (buttons.size() > 1 && askID.getNum() >= 0) //cancel button functionality + if(buttons.size() > 1 && askID.getNum() >= 0) //cancel button functionality { - buttons.back()->addCallback([askID]() { - LOCPLINT->cb.get()->selectionMade(0, askID); - }); + buttons.back()->addCallback([askID](){LOCPLINT->cb->selectionMade(0, askID);}); //buttons.back()->addCallback(std::bind(&CCallback::selectionMade, LOCPLINT->cb.get(), 0, askID)); } @@ -100,16 +68,9 @@ CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperl buttons.back()->assignedKey = EShortcut::GLOBAL_CANCEL; } - for(int i=0;irecActions = 255-DISPOSE; - addChild(comps[i].get()); - components.push_back(comps[i]); - comps[i]->onSelect = std::bind(&CSelWindow::selectionChange,this,i); - comps[i]->onChoose = std::bind(&CSelWindow::madeChoiceAndClose,this); - if(i<8) - comps[i]->assignedKey = vstd::next(EShortcut::SELECT_INDEX_1,i); - } + if(!comps.empty()) + components = std::make_shared(comps, Rect(0,0,0,0)); + CMessage::drawIWindow(this, Text, player); } @@ -118,14 +79,10 @@ void CSelWindow::madeChoice() if(ID.getNum() < 0) return; int ret = -1; - for (int i=0;i(components[i])->selected) - { - ret = i; - } - } - LOCPLINT->cb->selectionMade(ret+1,ID); + if(components) + ret = components->selectedIndex(); + + LOCPLINT->cb->selectionMade(ret + 1, ID); } void CSelWindow::madeChoiceAndClose() @@ -134,14 +91,16 @@ void CSelWindow::madeChoiceAndClose() close(); } -CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo & comps, const TButtonsInfo & Buttons) +CInfoWindow::CInfoWindow(const std::string & Text, PlayerColor player, const TCompsInfo & comps, const TButtonsInfo & Buttons) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); ID = QueryID(-1); - for(auto & Button : Buttons) + for(const auto & Button : Buttons) { - std::shared_ptr button = std::make_shared(Point(0,0), Button.first, CButton::tooltip(), std::bind(&CInfoWindow::close, this)); + auto button = std::make_shared(Point(0, 0), Button.first, CButton::tooltip(), std::bind(&CInfoWindow::close, this)); button->setBorderColor(Colors::METALLIC_GOLD); button->addCallback(Button.second); //each button will close the window apart from call-defined actions buttons.push_back(button); @@ -150,7 +109,9 @@ CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo text = std::make_shared(Text, Rect(0, 0, 250, 100), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); if(!text->slider) { - text->resize(text->label->textSize); + int finalWidth = std::min(250, text->label->textSize.x + 32); + int finalHeight = text->label->textSize.y; + text->resize(Point(finalWidth, finalHeight)); } if(buttons.size() == 1) @@ -162,15 +123,10 @@ CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo buttons.back()->assignedKey = EShortcut::GLOBAL_CANCEL; } - for(auto & comp : comps) - { - comp->recActions = 0xff & ~DISPOSE; - addChild(comp.get()); - comp->recActions &= ~(SHOWALL | UPDATE); - components.push_back(comp); - } + if(!comps.empty()) + components = std::make_shared(comps, Rect(0,0,0,0)); - CMessage::drawIWindow(this,Text,player); + CMessage::drawIWindow(this, Text, player); } CInfoWindow::CInfoWindow() @@ -186,128 +142,45 @@ void CInfoWindow::close() LOCPLINT->showingDialog->setn(false); } -void CInfoWindow::show(Canvas & to) +void CInfoWindow::showAll(Canvas & to) { - CIntObject::show(to); + CIntObject::showAll(to); + CMessage::drawBorder(LOCPLINT ? LOCPLINT->playerID : PlayerColor(1), to, pos.w+28, pos.h+29, pos.x-14, pos.y-15); } CInfoWindow::~CInfoWindow() = default; -void CInfoWindow::showAll(Canvas & to) -{ - CSimpleWindow::show(to); - CIntObject::showAll(to); -} - -void CInfoWindow::showInfoDialog(const std::string &text, const TCompsInfo & components, PlayerColor player) +void CInfoWindow::showInfoDialog(const std::string & text, const TCompsInfo & components, PlayerColor player) { GH.windows().pushWindow(CInfoWindow::create(text, player, components)); } -void CInfoWindow::showYesNoDialog(const std::string & text, const TCompsInfo & components, const CFunctionList &onYes, const CFunctionList &onNo, PlayerColor player) +void CInfoWindow::showYesNoDialog(const std::string & text, const TCompsInfo & components, const CFunctionList & onYes, const CFunctionList & onNo, PlayerColor player) { assert(!LOCPLINT || LOCPLINT->showingDialog->get()); - std::vector > > pom; - pom.push_back( { AnimationPath::builtin("IOKAY.DEF"), 0 }); - pom.push_back( { AnimationPath::builtin("ICANCEL.DEF"), 0 }); - std::shared_ptr temp = std::make_shared(text, player, components, pom); + std::vector>> pom; + pom.emplace_back(AnimationPath::builtin("IOKAY.DEF"), nullptr); + pom.emplace_back(AnimationPath::builtin("ICANCEL.DEF"), nullptr); + auto temp = std::make_shared(text, player, components, pom); - temp->buttons[0]->addCallback( onYes ); - temp->buttons[1]->addCallback( onNo ); + temp->buttons[0]->addCallback(onYes); + temp->buttons[1]->addCallback(onNo); GH.windows().pushWindow(temp); } -std::shared_ptr CInfoWindow::create(const std::string &text, PlayerColor playerID, const TCompsInfo & components) +std::shared_ptr CInfoWindow::create(const std::string & text, PlayerColor playerID, const TCompsInfo & components) { - std::vector > > pom; - pom.push_back({AnimationPath::builtin("IOKAY.DEF"), 0}); + std::vector>> pom; + pom.emplace_back(AnimationPath::builtin("IOKAY.DEF"), nullptr); return std::make_shared(text, playerID, components, pom); } -std::string CInfoWindow::genText(std::string title, std::string description) +std::string CInfoWindow::genText(const std::string & title, const std::string & description) { return std::string("{") + title + "}" + "\n\n" + description; } -CInfoPopup::CInfoPopup(SDL_Surface * Bitmap, int x, int y, bool Free) - :free(Free),bitmap(Bitmap) -{ - init(x, y); -} - - -CInfoPopup::CInfoPopup(SDL_Surface * Bitmap, const Point &p, ETextAlignment alignment, bool Free) - : free(Free),bitmap(Bitmap) -{ - switch(alignment) - { - case ETextAlignment::BOTTOMRIGHT: - init(p.x - Bitmap->w, p.y - Bitmap->h); - break; - case ETextAlignment::CENTER: - init(p.x - Bitmap->w/2, p.y - Bitmap->h/2); - break; - case ETextAlignment::TOPLEFT: - init(p.x, p.y); - break; - case ETextAlignment::TOPCENTER: - init(p.x - Bitmap->w/2, p.y); - break; - default: - assert(0); //not implemented - } -} - -CInfoPopup::CInfoPopup(SDL_Surface *Bitmap, bool Free) -{ - CCS->curh->hide(); - - free=Free; - bitmap=Bitmap; - - if(bitmap) - { - pos.x = GH.screenDimensions().x / 2 - bitmap->w / 2; - pos.y = GH.screenDimensions().y / 2 - bitmap->h / 2; - pos.h = bitmap->h; - pos.w = bitmap->w; - } -} - -void CInfoPopup::close() -{ - if(free) - SDL_FreeSurface(bitmap); - WindowBase::close(); -} - -void CInfoPopup::show(Canvas & to) -{ - CSDL_Ext::blitAt(bitmap,pos.x,pos.y,to.getInternalSurface()); -} - -CInfoPopup::~CInfoPopup() -{ - CCS->curh->show(); -} - -void CInfoPopup::init(int x, int y) -{ - CCS->curh->hide(); - - pos.x = x; - pos.y = y; - pos.h = bitmap->h; - pos.w = bitmap->w; - - // Put the window back on screen if necessary - vstd::amax(pos.x, 0); - vstd::amax(pos.y, 0); - vstd::amin(pos.x, GH.screenDimensions().x - bitmap->w); - vstd::amin(pos.y, GH.screenDimensions().y - bitmap->h); -} - bool CRClickPopup::isPopupWindow() const { return true; @@ -318,10 +191,10 @@ void CRClickPopup::close() WindowBase::close(); } -void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCompsInfo &comps) +void CRClickPopup::createAndPush(const std::string & txt, const CInfoWindow::TCompsInfo & comps) { PlayerColor player = LOCPLINT ? LOCPLINT->playerID : PlayerColor(1); //if no player, then use blue - if(settings["session"]["spectate"].Bool())//TODO: there must be better way to implement this + if(settings["session"]["spectate"].Bool()) //TODO: there must be better way to implement this player = PlayerColor(1); auto temp = std::make_shared(txt, player, comps); @@ -334,7 +207,7 @@ void CRClickPopup::createAndPush(const std::string &txt, const CInfoWindow::TCom GH.windows().createAndPushWindow(temp); } -void CRClickPopup::createAndPush(const std::string & txt, std::shared_ptr component) +void CRClickPopup::createAndPush(const std::string & txt, const std::shared_ptr & component) { CInfoWindow::TCompsInfo intComps; intComps.push_back(component); @@ -352,7 +225,7 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, else { std::vector components; - if (settings["general"]["enableUiEnhancements"].Bool()) + if(settings["general"]["enableUiEnhancements"].Bool()) { if(LOCPLINT->localState->getCurrentHero()) components = obj->getPopupComponents(LOCPLINT->localState->getCurrentHero()); @@ -361,7 +234,7 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, } std::vector> guiComponents; - for (auto & component : components) + for(auto & component : components) guiComponents.push_back(std::make_shared(component)); if(LOCPLINT->localState->getCurrentHero()) @@ -371,7 +244,7 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, } } -CRClickPopupInt::CRClickPopupInt(std::shared_ptr our) +CRClickPopupInt::CRClickPopupInt(const std::shared_ptr & our) { CCS->curh->hide(); defActions = SHOWALL | UPDATE; @@ -401,7 +274,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) InfoAboutTown iah; LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentTown()); //todo: should this be nearest hero? - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); tooltip = std::make_shared(Point(9, 10), iah); } @@ -409,9 +282,9 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), toScreen(position)) { InfoAboutHero iah; - LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentHero());//todo: should this be nearest hero? + LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentHero()); //todo: should this be nearest hero? - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); tooltip = std::make_shared(Point(9, 10), iah); } @@ -421,18 +294,19 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) InfoAboutTown iah; LOCPLINT->cb->getTownInfo(garr, iah); - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); tooltip = std::make_shared(Point(9, 10), iah); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature) - : CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), toScreen(position)) + : CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), toScreen(position)) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); tooltip = std::make_shared(Point(9, 10), creature); } -std::shared_ptr CRClickPopup::createCustomInfoWindow(Point position, const CGObjectInstance * specific) //specific=0 => draws info about selected town/hero +std::shared_ptr +CRClickPopup::createCustomInfoWindow(Point position, const CGObjectInstance * specific) //specific=0 => draws info about selected town/hero { if(nullptr == specific) specific = LOCPLINT->localState->getCurrentArmy(); @@ -445,16 +319,16 @@ std::shared_ptr CRClickPopup::createCustomInfoWindow(Point position, switch(specific->ID) { - case Obj::HERO: - return std::make_shared(position, dynamic_cast(specific)); - case Obj::TOWN: - return std::make_shared(position, dynamic_cast(specific)); - case Obj::MONSTER: - return std::make_shared(position, dynamic_cast(specific)); - case Obj::GARRISON: - case Obj::GARRISON2: - return std::make_shared(position, dynamic_cast(specific)); - default: - return std::shared_ptr(); + case Obj::HERO: + return std::make_shared(position, dynamic_cast(specific)); + case Obj::TOWN: + return std::make_shared(position, dynamic_cast(specific)); + case Obj::MONSTER: + return std::make_shared(position, dynamic_cast(specific)); + case Obj::GARRISON: + case Obj::GARRISON2: + return std::make_shared(position, dynamic_cast(specific)); + default: + return std::shared_ptr(); } } diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index eea665326..5da10014b 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -20,71 +20,56 @@ class CGTownInstance; class CGHeroInstance; class CGGarrison; class CGCreature; -class Rect; VCMI_LIB_NAMESPACE_END -struct SDL_Surface; -class CAnimImage; -class CLabel; -class CAnimation; class CComponent; +class CComponentBox; class CSelectableComponent; class CTextBox; class CButton; -class CSlider; -class CArmyTooltip; - -// Window GUI class -class CSimpleWindow : public WindowBase -{ -public: - SDL_Surface * bitmap; //background - void show(Canvas & to) override; - CSimpleWindow():bitmap(nullptr){}; - virtual ~CSimpleWindow(); -}; +class CFilledTexture; /// text + comp. + ok button -class CInfoWindow : public CSimpleWindow +class CInfoWindow : public WindowBase { public: using TButtonsInfo = std::vector>>; using TCompsInfo = std::vector>; QueryID ID; //for identification + std::shared_ptr backgroundTexture; std::shared_ptr text; + std::shared_ptr components; std::vector> buttons; - TCompsInfo components; void close() override; - - void show(Canvas & to) override; void showAll(Canvas & to) override; + void sliderMoved(int to); - CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo & comps = TCompsInfo(), const TButtonsInfo & Buttons = TButtonsInfo()); + CInfoWindow(const std::string & Text, PlayerColor player, const TCompsInfo & comps = TCompsInfo(), const TButtonsInfo & Buttons = TButtonsInfo()); CInfoWindow(); ~CInfoWindow(); //use only before the game starts! (showYesNoDialog in LOCPLINT must be used then) - static void showInfoDialog( const std::string & text, const TCompsInfo & components, PlayerColor player = PlayerColor(1)); - static void showYesNoDialog( const std::string & text, const TCompsInfo & components, const CFunctionList & onYes, const CFunctionList & onNo, PlayerColor player = PlayerColor(1)); + static void showInfoDialog(const std::string & text, const TCompsInfo & components, PlayerColor player = PlayerColor(1)); + static void showYesNoDialog(const std::string & text, const TCompsInfo & components, const CFunctionList & onYes, const CFunctionList & onNo, PlayerColor player = PlayerColor(1)); static std::shared_ptr create(const std::string & text, PlayerColor playerID = PlayerColor(1), const TCompsInfo & components = TCompsInfo()); /// create text from title and description: {title}\n\n description - static std::string genText(std::string title, std::string description); + static std::string genText(const std::string & title, const std::string & description); }; /// popup displayed on R-click class CRClickPopup : public WindowBase { public: - virtual void close() override; + void close() override; bool isPopupWindow() const override; static std::shared_ptr createCustomInfoWindow(Point position, const CGObjectInstance * specific); - static void createAndPush(const std::string & txt, const CInfoWindow::TCompsInfo &comps = CInfoWindow::TCompsInfo()); - static void createAndPush(const std::string & txt, std::shared_ptr component); + static void createAndPush(const std::string & txt, const CInfoWindow::TCompsInfo & comps = CInfoWindow::TCompsInfo()); + static void createAndPush(const std::string & txt, const std::shared_ptr & component); static void createAndPush(const CGObjectInstance * obj, const Point & p, ETextAlignment alignment = ETextAlignment::BOTTOMRIGHT); }; @@ -92,24 +77,10 @@ public: class CRClickPopupInt : public CRClickPopup { std::shared_ptr inner; -public: - CRClickPopupInt(std::shared_ptr our); - virtual ~CRClickPopupInt(); -}; -class CInfoPopup : public CRClickPopup -{ public: - bool free; //TODO: comment me - SDL_Surface * bitmap; //popup background - void close() override; - void show(Canvas & to) override; - CInfoPopup(SDL_Surface * Bitmap, int x, int y, bool Free=false); - CInfoPopup(SDL_Surface * Bitmap, const Point &p, ETextAlignment alignment, bool Free=false); - CInfoPopup(SDL_Surface * Bitmap = nullptr, bool Free = false); - - void init(int x, int y); - ~CInfoPopup(); + CRClickPopupInt(const std::shared_ptr & our); + ~CRClickPopupInt(); }; /// popup on adventure map for town\hero and other objects with customized popup content @@ -117,6 +88,7 @@ class CInfoBoxPopup : public CWindowObject { std::shared_ptr tooltip; Point toScreen(Point pos); + public: CInfoBoxPopup(Point position, const CGTownInstance * town); CInfoBoxPopup(Point position, const CGHeroInstance * hero); @@ -128,10 +100,7 @@ public: class CSelWindow : public CInfoWindow { public: - void selectionChange(unsigned to); void madeChoice(); //looks for selected component and calls callback void madeChoiceAndClose(); CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID); - - //notification - this class inherits important destructor from CInfoWindow }; diff --git a/client/windows/QuickRecruitmentWindow.h b/client/windows/QuickRecruitmentWindow.h index c8aba9fed..1d5a2bacd 100644 --- a/client/windows/QuickRecruitmentWindow.h +++ b/client/windows/QuickRecruitmentWindow.h @@ -42,7 +42,9 @@ private: void purchaseUnits(); const CGTownInstance * town; - std::shared_ptr maxButton, buyButton, cancelButton; + std::shared_ptr maxButton; + std::shared_ptr buyButton; + std::shared_ptr cancelButton; std::shared_ptr totalCost; std::vector> cards; std::shared_ptr backgroundTexture; diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index 90db19c93..f514f33f1 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -126,6 +126,18 @@ AdventureOptionsTab::AdventureOptionsTab() { return setBoolSetting("adventure", "leftButtonDrag", value); }); + addCallback("smoothDraggingChanged", [](bool value) + { + return setBoolSetting("adventure", "smoothDragging", value); + }); + addCallback("skipAdventureMapAnimationsChanged", [](bool value) + { + return setBoolSetting("gameTweaks", "skipAdventureMapAnimations", value); + }); + addCallback("hideBackgroundChanged", [](bool value) + { + return setBoolSetting("adventure", "hideBackground", value); + }); build(config); std::shared_ptr playerHeroSpeedToggle = widget("heroMovementSpeedPicker"); @@ -164,4 +176,14 @@ AdventureOptionsTab::AdventureOptionsTab() std::shared_ptr leftButtonDragCheckbox = widget("leftButtonDragCheckbox"); if (leftButtonDragCheckbox) leftButtonDragCheckbox->setSelected(settings["adventure"]["leftButtonDrag"].Bool()); + + std::shared_ptr smoothDraggingCheckbox = widget("smoothDraggingCheckbox"); + if (smoothDraggingCheckbox) + smoothDraggingCheckbox->setSelected(settings["adventure"]["smoothDragging"].Bool()); + + std::shared_ptr skipAdventureMapAnimationsCheckbox = widget("skipAdventureMapAnimationsCheckbox"); + skipAdventureMapAnimationsCheckbox->setSelected(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool()); + + std::shared_ptr hideBackgroundCheckbox = widget("hideBackgroundCheckbox"); + hideBackgroundCheckbox->setSelected(settings["adventure"]["hideBackground"].Bool()); } diff --git a/client/windows/settings/BattleOptionsTab.cpp b/client/windows/settings/BattleOptionsTab.cpp index 35abf73cb..7c803614e 100644 --- a/client/windows/settings/BattleOptionsTab.cpp +++ b/client/windows/settings/BattleOptionsTab.cpp @@ -68,6 +68,10 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) { enableAutocombatSpellsChangedCallback(value); }); + addCallback("endWithAutocombatChanged", [this](bool value) + { + endWithAutocombatChangedCallback(value); + }); build(config); std::shared_ptr animationSpeedToggle = widget("animationSpeedPicker"); @@ -99,6 +103,9 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) std::shared_ptr enableAutocombatSpellsCheckbox = widget("enableAutocombatSpellsCheckbox"); enableAutocombatSpellsCheckbox->setSelected(settings["battle"]["enableAutocombatSpells"].Bool()); + + std::shared_ptr endWithAutocombatCheckbox = widget("endWithAutocombatCheckbox"); + endWithAutocombatCheckbox->setSelected(settings["battle"]["endWithAutocombat"].Bool()); } int BattleOptionsTab::getAnimSpeed() const @@ -248,3 +255,8 @@ void BattleOptionsTab::enableAutocombatSpellsChangedCallback(bool value) enableAutocombatSpells->Bool() = value; } +void BattleOptionsTab::endWithAutocombatChangedCallback(bool value) +{ + Settings endWithAutocombat = settings.write["battle"]["endWithAutocombat"]; + endWithAutocombat->Bool() = value; +} diff --git a/client/windows/settings/BattleOptionsTab.h b/client/windows/settings/BattleOptionsTab.h index 43d4d7e50..633726b2c 100644 --- a/client/windows/settings/BattleOptionsTab.h +++ b/client/windows/settings/BattleOptionsTab.h @@ -33,6 +33,7 @@ private: void skipBattleIntroMusicChangedCallback(bool value); void showStickyHeroWindowsChangedCallback(bool value, BattleInterface * parentBattleInterface); void enableAutocombatSpellsChangedCallback(bool value); + void endWithAutocombatChangedCallback(bool value); public: BattleOptionsTab(BattleInterface * owner = nullptr); }; diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 49e490249..2058b664c 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -162,6 +162,22 @@ GeneralOptionsTab::GeneralOptionsTab() setBoolSetting("general", "enableUiEnhancements", value); }); + addCallback("enableLargeSpellbookChanged", [this](bool value) + { + setBoolSetting("gameTweaks", "enableLargeSpellbook", value); + std::shared_ptr spellbookAnimationCheckbox = widget("spellbookAnimationCheckbox"); + if(value) + spellbookAnimationCheckbox->disable(); + else + spellbookAnimationCheckbox->enable(); + redraw(); + }); + + addCallback("audioMuteFocusChanged", [](bool value) + { + setBoolSetting("general", "audioMuteFocus", value); + }); + //moved from "other" tab that is disabled for now to avoid excessible tabs with barely any content addCallback("availableCreaturesAsDwellingChanged", [=](int value) { @@ -186,6 +202,10 @@ GeneralOptionsTab::GeneralOptionsTab() std::shared_ptr spellbookAnimationCheckbox = widget("spellbookAnimationCheckbox"); spellbookAnimationCheckbox->setSelected(settings["video"]["spellbookAnimation"].Bool()); + if(settings["gameTweaks"]["enableLargeSpellbook"].Bool()) + spellbookAnimationCheckbox->disable(); + else + spellbookAnimationCheckbox->enable(); std::shared_ptr fullscreenBorderlessCheckbox = widget("fullscreenBorderlessCheckbox"); if (fullscreenBorderlessCheckbox) @@ -206,6 +226,14 @@ GeneralOptionsTab::GeneralOptionsTab() if (enableUiEnhancementsCheckbox) enableUiEnhancementsCheckbox->setSelected(settings["general"]["enableUiEnhancements"].Bool()); + std::shared_ptr enableLargeSpellbookCheckbox = widget("enableLargeSpellbookCheckbox"); + if (enableLargeSpellbookCheckbox) + enableLargeSpellbookCheckbox->setSelected(settings["gameTweaks"]["enableLargeSpellbook"].Bool()); + + std::shared_ptr audioMuteFocusCheckbox = widget("audioMuteFocusCheckbox"); + if (audioMuteFocusCheckbox) + audioMuteFocusCheckbox->setSelected(settings["general"]["audioMuteFocus"].Bool()); + std::shared_ptr musicSlider = widget("musicSlider"); musicSlider->scrollTo(CCS->musich->getVolume()); @@ -289,7 +317,7 @@ void GeneralOptionsTab::setGameResolution(int index) widget("resolutionLabel")->setText(resolutionToLabelString(resolution.x, resolution.y)); GH.dispatchMainThread([](){ - GH.onScreenResize(); + GH.onScreenResize(true); }); } @@ -313,7 +341,7 @@ void GeneralOptionsTab::setFullscreenMode(bool on, bool exclusive) updateResolutionSelector(); GH.dispatchMainThread([](){ - GH.onScreenResize(); + GH.onScreenResize(true); }); } @@ -372,7 +400,7 @@ void GeneralOptionsTab::setGameScaling(int index) widget("scalingLabel")->setText(scalingToLabelString(scaling)); GH.dispatchMainThread([](){ - GH.onScreenResize(); + GH.onScreenResize(true); }); } diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index d122c8a02..f5c3ab7ac 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -143,12 +143,9 @@ void SettingsMainWindow::mainMenuButtonCallback() [this]() { close(); - GH.dispatchMainThread( []() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("main"); - }); + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("main"); }, 0 ); @@ -188,7 +185,7 @@ void SettingsMainWindow::showAll(Canvas & to) color = PlayerColor(1); // TODO: Spectator shouldn't need special code for UI colors CIntObject::showAll(to); - CMessage::drawBorder(color, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); + CMessage::drawBorder(color, to, pos.w+28, pos.h+29, pos.x-14, pos.y-15); } void SettingsMainWindow::onScreenResize() diff --git a/cmake_modules/FindSDL2.cmake b/cmake_modules/FindSDL2.cmake index c3c537a42..4228a3c0b 100644 --- a/cmake_modules/FindSDL2.cmake +++ b/cmake_modules/FindSDL2.cmake @@ -314,13 +314,6 @@ FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2 REQUIRED_VARS SDL2_LIBRARY SDL2_INCLUDE_DIR VERSION_VAR SDL2_VERSION_STRING) -if(SDL2MAIN_LIBRARY) - FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2main - REQUIRED_VARS SDL2MAIN_LIBRARY SDL2_INCLUDE_DIR - VERSION_VAR SDL2_VERSION_STRING) -endif() - - mark_as_advanced(SDL2_PATH SDL2_NO_DEFAULT_PATH SDL2_LIBRARY diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake deleted file mode 100644 index 4654082d6..000000000 --- a/cmake_modules/VCMI_lib.cmake +++ /dev/null @@ -1,740 +0,0 @@ -macro(add_main_lib TARGET_NAME LIBRARY_TYPE) - if(NOT DEFINED MAIN_LIB_DIR) - set(MAIN_LIB_DIR "${CMAKE_SOURCE_DIR}/lib") - endif() - - set(lib_SRCS - ${MAIN_LIB_DIR}/StdInc.cpp - - ${MAIN_LIB_DIR}/battle/AccessibilityInfo.cpp - ${MAIN_LIB_DIR}/battle/BattleAction.cpp - ${MAIN_LIB_DIR}/battle/BattleAttackInfo.cpp - ${MAIN_LIB_DIR}/battle/BattleHex.cpp - ${MAIN_LIB_DIR}/battle/BattleInfo.cpp - ${MAIN_LIB_DIR}/battle/BattleProxy.cpp - ${MAIN_LIB_DIR}/battle/BattleStateInfoForRetreat.cpp - ${MAIN_LIB_DIR}/battle/CBattleInfoCallback.cpp - ${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.cpp - ${MAIN_LIB_DIR}/battle/CObstacleInstance.cpp - ${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.cpp - ${MAIN_LIB_DIR}/battle/CUnitState.cpp - ${MAIN_LIB_DIR}/battle/DamageCalculator.cpp - ${MAIN_LIB_DIR}/battle/Destination.cpp - ${MAIN_LIB_DIR}/battle/IBattleState.cpp - ${MAIN_LIB_DIR}/battle/ReachabilityInfo.cpp - ${MAIN_LIB_DIR}/battle/SideInBattle.cpp - ${MAIN_LIB_DIR}/battle/SiegeInfo.cpp - ${MAIN_LIB_DIR}/battle/Unit.cpp - - ${MAIN_LIB_DIR}/bonuses/Bonus.cpp - ${MAIN_LIB_DIR}/bonuses/BonusEnum.cpp - ${MAIN_LIB_DIR}/bonuses/BonusList.cpp - ${MAIN_LIB_DIR}/bonuses/BonusParams.cpp - ${MAIN_LIB_DIR}/bonuses/BonusSelector.cpp - ${MAIN_LIB_DIR}/bonuses/BonusCustomTypes.cpp - ${MAIN_LIB_DIR}/bonuses/CBonusProxy.cpp - ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.cpp - ${MAIN_LIB_DIR}/bonuses/IBonusBearer.cpp - ${MAIN_LIB_DIR}/bonuses/Limiters.cpp - ${MAIN_LIB_DIR}/bonuses/Propagators.cpp - ${MAIN_LIB_DIR}/bonuses/Updaters.cpp - - ${MAIN_LIB_DIR}/campaign/CampaignHandler.cpp - ${MAIN_LIB_DIR}/campaign/CampaignState.cpp - - ${MAIN_LIB_DIR}/constants/EntityIdentifiers.cpp - - ${MAIN_LIB_DIR}/events/ApplyDamage.cpp - ${MAIN_LIB_DIR}/events/GameResumed.cpp - ${MAIN_LIB_DIR}/events/ObjectVisitEnded.cpp - ${MAIN_LIB_DIR}/events/ObjectVisitStarted.cpp - ${MAIN_LIB_DIR}/events/PlayerGotTurn.cpp - ${MAIN_LIB_DIR}/events/TurnStarted.cpp - - ${MAIN_LIB_DIR}/filesystem/AdapterLoaders.cpp - ${MAIN_LIB_DIR}/filesystem/CArchiveLoader.cpp - ${MAIN_LIB_DIR}/filesystem/CBinaryReader.cpp - ${MAIN_LIB_DIR}/filesystem/CCompressedStream.cpp - ${MAIN_LIB_DIR}/filesystem/CFileInputStream.cpp - ${MAIN_LIB_DIR}/filesystem/CFilesystemLoader.cpp - ${MAIN_LIB_DIR}/filesystem/CMemoryBuffer.cpp - ${MAIN_LIB_DIR}/filesystem/CMemoryStream.cpp - ${MAIN_LIB_DIR}/filesystem/CZipLoader.cpp - ${MAIN_LIB_DIR}/filesystem/CZipSaver.cpp - ${MAIN_LIB_DIR}/filesystem/FileInfo.cpp - ${MAIN_LIB_DIR}/filesystem/Filesystem.cpp - ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.cpp - ${MAIN_LIB_DIR}/filesystem/ResourcePath.cpp - - ${MAIN_LIB_DIR}/gameState/CGameState.cpp - ${MAIN_LIB_DIR}/gameState/CGameStateCampaign.cpp - ${MAIN_LIB_DIR}/gameState/InfoAboutArmy.cpp - ${MAIN_LIB_DIR}/gameState/TavernHeroesPool.cpp - - ${MAIN_LIB_DIR}/logging/CBasicLogConfigurator.cpp - ${MAIN_LIB_DIR}/logging/CLogger.cpp - - ${MAIN_LIB_DIR}/mapObjectConstructors/AObjectTypeHandler.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/CBankInstanceConstructor.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/CObjectClassesHandler.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/CommonConstructors.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/CRewardableConstructor.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/DwellingInstanceConstructor.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/HillFortInstanceConstructor.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.cpp - - ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.cpp - ${MAIN_LIB_DIR}/mapObjects/CBank.cpp - ${MAIN_LIB_DIR}/mapObjects/CGCreature.cpp - ${MAIN_LIB_DIR}/mapObjects/CGDwelling.cpp - ${MAIN_LIB_DIR}/mapObjects/CGHeroInstance.cpp - ${MAIN_LIB_DIR}/mapObjects/CGMarket.cpp - ${MAIN_LIB_DIR}/mapObjects/CGObjectInstance.cpp - ${MAIN_LIB_DIR}/mapObjects/CGPandoraBox.cpp - ${MAIN_LIB_DIR}/mapObjects/CGTownBuilding.cpp - ${MAIN_LIB_DIR}/mapObjects/CGTownInstance.cpp - ${MAIN_LIB_DIR}/mapObjects/CObjectHandler.cpp - ${MAIN_LIB_DIR}/mapObjects/CQuest.cpp - ${MAIN_LIB_DIR}/mapObjects/CRewardableObject.cpp - ${MAIN_LIB_DIR}/mapObjects/IMarket.cpp - ${MAIN_LIB_DIR}/mapObjects/IObjectInterface.cpp - ${MAIN_LIB_DIR}/mapObjects/MiscObjects.cpp - ${MAIN_LIB_DIR}/mapObjects/ObjectTemplate.cpp - - ${MAIN_LIB_DIR}/mapping/CDrawRoadsOperation.cpp - ${MAIN_LIB_DIR}/mapping/CMap.cpp - ${MAIN_LIB_DIR}/mapping/CMapHeader.cpp - ${MAIN_LIB_DIR}/mapping/CMapEditManager.cpp - ${MAIN_LIB_DIR}/mapping/CMapInfo.cpp - ${MAIN_LIB_DIR}/mapping/CMapOperation.cpp - ${MAIN_LIB_DIR}/mapping/CMapService.cpp - ${MAIN_LIB_DIR}/mapping/MapEditUtils.cpp - ${MAIN_LIB_DIR}/mapping/MapIdentifiersH3M.cpp - ${MAIN_LIB_DIR}/mapping/MapFeaturesH3M.cpp - ${MAIN_LIB_DIR}/mapping/MapFormatH3M.cpp - ${MAIN_LIB_DIR}/mapping/MapReaderH3M.cpp - ${MAIN_LIB_DIR}/mapping/MapFormatJson.cpp - ${MAIN_LIB_DIR}/mapping/ObstacleProxy.cpp - - ${MAIN_LIB_DIR}/modding/CModHandler.cpp - ${MAIN_LIB_DIR}/modding/CModInfo.cpp - ${MAIN_LIB_DIR}/modding/CModVersion.cpp - ${MAIN_LIB_DIR}/modding/ContentTypeHandler.cpp - ${MAIN_LIB_DIR}/modding/IdentifierStorage.cpp - ${MAIN_LIB_DIR}/modding/ModUtility.cpp - - ${MAIN_LIB_DIR}/networkPacks/NetPacksLib.cpp - - ${MAIN_LIB_DIR}/pathfinder/CGPathNode.cpp - ${MAIN_LIB_DIR}/pathfinder/CPathfinder.cpp - ${MAIN_LIB_DIR}/pathfinder/NodeStorage.cpp - ${MAIN_LIB_DIR}/pathfinder/PathfinderOptions.cpp - ${MAIN_LIB_DIR}/pathfinder/PathfindingRules.cpp - ${MAIN_LIB_DIR}/pathfinder/TurnInfo.cpp - - ${MAIN_LIB_DIR}/registerTypes/RegisterTypes.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesClientPacks1.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesClientPacks2.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects1.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects2.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects3.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesLobbyPacks.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesServerPacks.cpp - - ${MAIN_LIB_DIR}/rewardable/Configuration.cpp - ${MAIN_LIB_DIR}/rewardable/Info.cpp - ${MAIN_LIB_DIR}/rewardable/Interface.cpp - ${MAIN_LIB_DIR}/rewardable/Limiter.cpp - ${MAIN_LIB_DIR}/rewardable/Reward.cpp - - ${MAIN_LIB_DIR}/rmg/RmgArea.cpp - ${MAIN_LIB_DIR}/rmg/RmgObject.cpp - ${MAIN_LIB_DIR}/rmg/RmgPath.cpp - ${MAIN_LIB_DIR}/rmg/CMapGenerator.cpp - ${MAIN_LIB_DIR}/rmg/CMapGenOptions.cpp - ${MAIN_LIB_DIR}/rmg/CRmgTemplate.cpp - ${MAIN_LIB_DIR}/rmg/CRmgTemplateStorage.cpp - ${MAIN_LIB_DIR}/rmg/CZonePlacer.cpp - ${MAIN_LIB_DIR}/rmg/TileInfo.cpp - ${MAIN_LIB_DIR}/rmg/Zone.cpp - ${MAIN_LIB_DIR}/rmg/Functions.cpp - ${MAIN_LIB_DIR}/rmg/RmgMap.cpp - ${MAIN_LIB_DIR}/rmg/modificators/Modificator.cpp - ${MAIN_LIB_DIR}/rmg/modificators/ObjectManager.cpp - ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.cpp - ${MAIN_LIB_DIR}/rmg/modificators/RoadPlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/TreasurePlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/QuestArtifactPlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/ConnectionsPlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/WaterAdopter.cpp - ${MAIN_LIB_DIR}/rmg/modificators/MinePlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/TownPlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/WaterProxy.cpp - ${MAIN_LIB_DIR}/rmg/modificators/WaterRoutes.cpp - ${MAIN_LIB_DIR}/rmg/modificators/RockPlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/RockFiller.cpp - ${MAIN_LIB_DIR}/rmg/modificators/ObstaclePlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/RiverPlacer.cpp - ${MAIN_LIB_DIR}/rmg/modificators/TerrainPainter.cpp - ${MAIN_LIB_DIR}/rmg/threadpool/MapProxy.cpp - - ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.cpp - ${MAIN_LIB_DIR}/serializer/BinarySerializer.cpp - ${MAIN_LIB_DIR}/serializer/CLoadIntegrityValidator.cpp - ${MAIN_LIB_DIR}/serializer/CMemorySerializer.cpp - ${MAIN_LIB_DIR}/serializer/Connection.cpp - ${MAIN_LIB_DIR}/serializer/CSerializer.cpp - ${MAIN_LIB_DIR}/serializer/CTypeList.cpp - ${MAIN_LIB_DIR}/serializer/JsonDeserializer.cpp - ${MAIN_LIB_DIR}/serializer/JsonSerializeFormat.cpp - ${MAIN_LIB_DIR}/serializer/JsonSerializer.cpp - ${MAIN_LIB_DIR}/serializer/JsonUpdater.cpp - ${MAIN_LIB_DIR}/serializer/ILICReader.cpp - - ${MAIN_LIB_DIR}/spells/AbilityCaster.cpp - ${MAIN_LIB_DIR}/spells/AdventureSpellMechanics.cpp - ${MAIN_LIB_DIR}/spells/BattleSpellMechanics.cpp - ${MAIN_LIB_DIR}/spells/BonusCaster.cpp - ${MAIN_LIB_DIR}/spells/CSpellHandler.cpp - ${MAIN_LIB_DIR}/spells/ExternalCaster.cpp - ${MAIN_LIB_DIR}/spells/ISpellMechanics.cpp - ${MAIN_LIB_DIR}/spells/ObstacleCasterProxy.cpp - ${MAIN_LIB_DIR}/spells/Problem.cpp - ${MAIN_LIB_DIR}/spells/ProxyCaster.cpp - ${MAIN_LIB_DIR}/spells/TargetCondition.cpp - ${MAIN_LIB_DIR}/spells/ViewSpellInt.cpp - - ${MAIN_LIB_DIR}/spells/effects/Catapult.cpp - ${MAIN_LIB_DIR}/spells/effects/Clone.cpp - ${MAIN_LIB_DIR}/spells/effects/Damage.cpp - ${MAIN_LIB_DIR}/spells/effects/DemonSummon.cpp - ${MAIN_LIB_DIR}/spells/effects/Dispel.cpp - ${MAIN_LIB_DIR}/spells/effects/Effect.cpp - ${MAIN_LIB_DIR}/spells/effects/Effects.cpp - ${MAIN_LIB_DIR}/spells/effects/Heal.cpp - ${MAIN_LIB_DIR}/spells/effects/LocationEffect.cpp - ${MAIN_LIB_DIR}/spells/effects/Moat.cpp - ${MAIN_LIB_DIR}/spells/effects/Obstacle.cpp - ${MAIN_LIB_DIR}/spells/effects/Registry.cpp - ${MAIN_LIB_DIR}/spells/effects/UnitEffect.cpp - ${MAIN_LIB_DIR}/spells/effects/Summon.cpp - ${MAIN_LIB_DIR}/spells/effects/Teleport.cpp - ${MAIN_LIB_DIR}/spells/effects/Timed.cpp - ${MAIN_LIB_DIR}/spells/effects/RemoveObstacle.cpp - ${MAIN_LIB_DIR}/spells/effects/Sacrifice.cpp - - ${MAIN_LIB_DIR}/vstd/DateUtils.cpp - ${MAIN_LIB_DIR}/vstd/StringUtils.cpp - - ${MAIN_LIB_DIR}/ArtifactUtils.cpp - ${MAIN_LIB_DIR}/BasicTypes.cpp - ${MAIN_LIB_DIR}/BattleFieldHandler.cpp - ${MAIN_LIB_DIR}/CAndroidVMHelper.cpp - ${MAIN_LIB_DIR}/CArtHandler.cpp - ${MAIN_LIB_DIR}/CArtifactInstance.cpp - ${MAIN_LIB_DIR}/CBonusTypeHandler.cpp - ${MAIN_LIB_DIR}/CBuildingHandler.cpp - ${MAIN_LIB_DIR}/CConfigHandler.cpp - ${MAIN_LIB_DIR}/CConsoleHandler.cpp - ${MAIN_LIB_DIR}/CCreatureHandler.cpp - ${MAIN_LIB_DIR}/CCreatureSet.cpp - ${MAIN_LIB_DIR}/CGameInfoCallback.cpp - ${MAIN_LIB_DIR}/CGameInterface.cpp - ${MAIN_LIB_DIR}/CGeneralTextHandler.cpp - ${MAIN_LIB_DIR}/CHeroHandler.cpp - ${MAIN_LIB_DIR}/CPlayerState.cpp - ${MAIN_LIB_DIR}/CRandomGenerator.cpp - ${MAIN_LIB_DIR}/CScriptingModule.cpp - ${MAIN_LIB_DIR}/CSkillHandler.cpp - ${MAIN_LIB_DIR}/CStack.cpp - ${MAIN_LIB_DIR}/CThreadHelper.cpp - ${MAIN_LIB_DIR}/CTownHandler.cpp - ${MAIN_LIB_DIR}/GameSettings.cpp - ${MAIN_LIB_DIR}/IGameCallback.cpp - ${MAIN_LIB_DIR}/IHandlerBase.cpp - ${MAIN_LIB_DIR}/JsonDetail.cpp - ${MAIN_LIB_DIR}/JsonNode.cpp - ${MAIN_LIB_DIR}/JsonRandom.cpp - ${MAIN_LIB_DIR}/LoadProgress.cpp - ${MAIN_LIB_DIR}/LogicalExpression.cpp - ${MAIN_LIB_DIR}/MetaString.cpp - ${MAIN_LIB_DIR}/ObstacleHandler.cpp - ${MAIN_LIB_DIR}/StartInfo.cpp - ${MAIN_LIB_DIR}/ResourceSet.cpp - ${MAIN_LIB_DIR}/RiverHandler.cpp - ${MAIN_LIB_DIR}/RoadHandler.cpp - ${MAIN_LIB_DIR}/ScriptHandler.cpp - ${MAIN_LIB_DIR}/TerrainHandler.cpp - ${MAIN_LIB_DIR}/TextOperations.cpp - ${MAIN_LIB_DIR}/TurnTimerInfo.cpp - ${MAIN_LIB_DIR}/VCMIDirs.cpp - ${MAIN_LIB_DIR}/VCMI_Lib.cpp - ) - - # Version.cpp is a generated file - if(ENABLE_GITVERSION) - list(APPEND lib_SRCS ${CMAKE_BINARY_DIR}/Version.cpp) - set_source_files_properties(${CMAKE_BINARY_DIR}/Version.cpp - PROPERTIES GENERATED TRUE - ) - endif() - - set(lib_HEADERS - ${MAIN_LIB_DIR}/../include/vstd/CLoggerBase.h - ${MAIN_LIB_DIR}/../Global.h - ${MAIN_LIB_DIR}/../AUTHORS.h - ${MAIN_LIB_DIR}/StdInc.h - - ${MAIN_LIB_DIR}/../include/vstd/ContainerUtils.h - ${MAIN_LIB_DIR}/../include/vstd/RNG.h - ${MAIN_LIB_DIR}/../include/vstd/DateUtils.h - ${MAIN_LIB_DIR}/../include/vstd/StringUtils.h - - ${MAIN_LIB_DIR}/../include/vcmi/events/AdventureEvents.h - ${MAIN_LIB_DIR}/../include/vcmi/events/ApplyDamage.h - ${MAIN_LIB_DIR}/../include/vcmi/events/BattleEvents.h - ${MAIN_LIB_DIR}/../include/vcmi/events/Event.h - ${MAIN_LIB_DIR}/../include/vcmi/events/EventBus.h - ${MAIN_LIB_DIR}/../include/vcmi/events/GameResumed.h - ${MAIN_LIB_DIR}/../include/vcmi/events/GenericEvents.h - ${MAIN_LIB_DIR}/../include/vcmi/events/ObjectVisitEnded.h - ${MAIN_LIB_DIR}/../include/vcmi/events/ObjectVisitStarted.h - ${MAIN_LIB_DIR}/../include/vcmi/events/PlayerGotTurn.h - ${MAIN_LIB_DIR}/../include/vcmi/events/SubscriptionRegistry.h - ${MAIN_LIB_DIR}/../include/vcmi/events/TurnStarted.h - - ${MAIN_LIB_DIR}/../include/vcmi/scripting/Service.h - - ${MAIN_LIB_DIR}/../include/vcmi/spells/Caster.h - ${MAIN_LIB_DIR}/../include/vcmi/spells/Magic.h - ${MAIN_LIB_DIR}/../include/vcmi/spells/Service.h - ${MAIN_LIB_DIR}/../include/vcmi/spells/Spell.h - - ${MAIN_LIB_DIR}/../include/vcmi/Artifact.h - ${MAIN_LIB_DIR}/../include/vcmi/ArtifactService.h - ${MAIN_LIB_DIR}/../include/vcmi/Creature.h - ${MAIN_LIB_DIR}/../include/vcmi/CreatureService.h - ${MAIN_LIB_DIR}/../include/vcmi/Entity.h - ${MAIN_LIB_DIR}/../include/vcmi/Environment.h - ${MAIN_LIB_DIR}/../include/vcmi/Faction.h - ${MAIN_LIB_DIR}/../include/vcmi/FactionService.h - ${MAIN_LIB_DIR}/../include/vcmi/HeroClass.h - ${MAIN_LIB_DIR}/../include/vcmi/HeroClassService.h - ${MAIN_LIB_DIR}/../include/vcmi/HeroType.h - ${MAIN_LIB_DIR}/../include/vcmi/HeroTypeService.h - ${MAIN_LIB_DIR}/../include/vcmi/Metatype.h - ${MAIN_LIB_DIR}/../include/vcmi/Player.h - ${MAIN_LIB_DIR}/../include/vcmi/ServerCallback.h - ${MAIN_LIB_DIR}/../include/vcmi/Services.h - ${MAIN_LIB_DIR}/../include/vcmi/Skill.h - ${MAIN_LIB_DIR}/../include/vcmi/SkillService.h - ${MAIN_LIB_DIR}/../include/vcmi/Team.h - - ${MAIN_LIB_DIR}/battle/AccessibilityInfo.h - ${MAIN_LIB_DIR}/battle/AutocombatPreferences.h - ${MAIN_LIB_DIR}/battle/BattleAction.h - ${MAIN_LIB_DIR}/battle/BattleAttackInfo.h - ${MAIN_LIB_DIR}/battle/BattleHex.h - ${MAIN_LIB_DIR}/battle/BattleInfo.h - ${MAIN_LIB_DIR}/battle/BattleStateInfoForRetreat.h - ${MAIN_LIB_DIR}/battle/BattleProxy.h - ${MAIN_LIB_DIR}/battle/CBattleInfoCallback.h - ${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.h - ${MAIN_LIB_DIR}/battle/CObstacleInstance.h - ${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.h - ${MAIN_LIB_DIR}/battle/CUnitState.h - ${MAIN_LIB_DIR}/battle/DamageCalculator.h - ${MAIN_LIB_DIR}/battle/Destination.h - ${MAIN_LIB_DIR}/battle/IBattleInfoCallback.h - ${MAIN_LIB_DIR}/battle/IBattleState.h - ${MAIN_LIB_DIR}/battle/IUnitInfo.h - ${MAIN_LIB_DIR}/battle/PossiblePlayerBattleAction.h - ${MAIN_LIB_DIR}/battle/ReachabilityInfo.h - ${MAIN_LIB_DIR}/battle/SideInBattle.h - ${MAIN_LIB_DIR}/battle/SiegeInfo.h - ${MAIN_LIB_DIR}/battle/Unit.h - - ${MAIN_LIB_DIR}/bonuses/Bonus.h - ${MAIN_LIB_DIR}/bonuses/BonusEnum.h - ${MAIN_LIB_DIR}/bonuses/BonusList.h - ${MAIN_LIB_DIR}/bonuses/BonusParams.h - ${MAIN_LIB_DIR}/bonuses/BonusSelector.h - ${MAIN_LIB_DIR}/bonuses/BonusCustomTypes.h - ${MAIN_LIB_DIR}/bonuses/CBonusProxy.h - ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.h - ${MAIN_LIB_DIR}/bonuses/IBonusBearer.h - ${MAIN_LIB_DIR}/bonuses/Limiters.h - ${MAIN_LIB_DIR}/bonuses/Propagators.h - ${MAIN_LIB_DIR}/bonuses/Updaters.h - - ${MAIN_LIB_DIR}/campaign/CampaignConstants.h - ${MAIN_LIB_DIR}/campaign/CampaignHandler.h - ${MAIN_LIB_DIR}/campaign/CampaignScenarioPrologEpilog.h - ${MAIN_LIB_DIR}/campaign/CampaignState.h - - ${MAIN_LIB_DIR}/constants/EntityIdentifiers.h - ${MAIN_LIB_DIR}/constants/Enumerations.h - ${MAIN_LIB_DIR}/constants/IdentifierBase.h - ${MAIN_LIB_DIR}/constants/VariantIdentifier.h - ${MAIN_LIB_DIR}/constants/NumericConstants.h - ${MAIN_LIB_DIR}/constants/StringConstants.h - - ${MAIN_LIB_DIR}/events/ApplyDamage.h - ${MAIN_LIB_DIR}/events/GameResumed.h - ${MAIN_LIB_DIR}/events/ObjectVisitEnded.h - ${MAIN_LIB_DIR}/events/ObjectVisitStarted.h - ${MAIN_LIB_DIR}/events/PlayerGotTurn.h - ${MAIN_LIB_DIR}/events/TurnStarted.h - - ${MAIN_LIB_DIR}/filesystem/AdapterLoaders.h - ${MAIN_LIB_DIR}/filesystem/CArchiveLoader.h - ${MAIN_LIB_DIR}/filesystem/CBinaryReader.h - ${MAIN_LIB_DIR}/filesystem/CCompressedStream.h - ${MAIN_LIB_DIR}/filesystem/CFileInputStream.h - ${MAIN_LIB_DIR}/filesystem/CFilesystemLoader.h - ${MAIN_LIB_DIR}/filesystem/CInputOutputStream.h - ${MAIN_LIB_DIR}/filesystem/CInputStream.h - ${MAIN_LIB_DIR}/filesystem/CMemoryBuffer.h - ${MAIN_LIB_DIR}/filesystem/CMemoryStream.h - ${MAIN_LIB_DIR}/filesystem/COutputStream.h - ${MAIN_LIB_DIR}/filesystem/CStream.h - ${MAIN_LIB_DIR}/filesystem/CZipLoader.h - ${MAIN_LIB_DIR}/filesystem/CZipSaver.h - ${MAIN_LIB_DIR}/filesystem/FileInfo.h - ${MAIN_LIB_DIR}/filesystem/Filesystem.h - ${MAIN_LIB_DIR}/filesystem/ISimpleResourceLoader.h - ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.h - ${MAIN_LIB_DIR}/filesystem/ResourcePath.h - - ${MAIN_LIB_DIR}/gameState/CGameState.h - ${MAIN_LIB_DIR}/gameState/CGameStateCampaign.h - ${MAIN_LIB_DIR}/gameState/EVictoryLossCheckResult.h - ${MAIN_LIB_DIR}/gameState/InfoAboutArmy.h - ${MAIN_LIB_DIR}/gameState/SThievesGuildInfo.h - ${MAIN_LIB_DIR}/gameState/TavernHeroesPool.h - ${MAIN_LIB_DIR}/gameState/TavernSlot.h - ${MAIN_LIB_DIR}/gameState/QuestInfo.h - - ${MAIN_LIB_DIR}/logging/CBasicLogConfigurator.h - ${MAIN_LIB_DIR}/logging/CLogger.h - - ${MAIN_LIB_DIR}/mapObjectConstructors/AObjectTypeHandler.h - ${MAIN_LIB_DIR}/mapObjectConstructors/CBankInstanceConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/CDefaultObjectTypeHandler.h - ${MAIN_LIB_DIR}/mapObjectConstructors/CObjectClassesHandler.h - ${MAIN_LIB_DIR}/mapObjectConstructors/CommonConstructors.h - ${MAIN_LIB_DIR}/mapObjectConstructors/CRewardableConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/DwellingInstanceConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/HillFortInstanceConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/IObjectInfo.h - ${MAIN_LIB_DIR}/mapObjectConstructors/RandomMapInfo.h - ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/SObjectSounds.h - - ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.h - ${MAIN_LIB_DIR}/mapObjects/CBank.h - ${MAIN_LIB_DIR}/mapObjects/CGCreature.h - ${MAIN_LIB_DIR}/mapObjects/CGDwelling.h - ${MAIN_LIB_DIR}/mapObjects/CGHeroInstance.h - ${MAIN_LIB_DIR}/mapObjects/CGMarket.h - ${MAIN_LIB_DIR}/mapObjects/CGObjectInstance.h - ${MAIN_LIB_DIR}/mapObjects/CGPandoraBox.h - ${MAIN_LIB_DIR}/mapObjects/CGTownBuilding.h - ${MAIN_LIB_DIR}/mapObjects/CGTownInstance.h - ${MAIN_LIB_DIR}/mapObjects/CObjectHandler.h - ${MAIN_LIB_DIR}/mapObjects/CQuest.h - ${MAIN_LIB_DIR}/mapObjects/CRewardableObject.h - ${MAIN_LIB_DIR}/mapObjects/IMarket.h - ${MAIN_LIB_DIR}/mapObjects/IObjectInterface.h - ${MAIN_LIB_DIR}/mapObjects/MapObjects.h - ${MAIN_LIB_DIR}/mapObjects/MiscObjects.h - ${MAIN_LIB_DIR}/mapObjects/ObjectTemplate.h - - ${MAIN_LIB_DIR}/mapping/CDrawRoadsOperation.h - ${MAIN_LIB_DIR}/mapping/CMapDefines.h - ${MAIN_LIB_DIR}/mapping/CMapEditManager.h - ${MAIN_LIB_DIR}/mapping/CMapHeader.h - ${MAIN_LIB_DIR}/mapping/CMap.h - ${MAIN_LIB_DIR}/mapping/CMapInfo.h - ${MAIN_LIB_DIR}/mapping/CMapOperation.h - ${MAIN_LIB_DIR}/mapping/CMapService.h - ${MAIN_LIB_DIR}/mapping/MapEditUtils.h - ${MAIN_LIB_DIR}/mapping/MapIdentifiersH3M.h - ${MAIN_LIB_DIR}/mapping/MapFeaturesH3M.h - ${MAIN_LIB_DIR}/mapping/MapFormatH3M.h - ${MAIN_LIB_DIR}/mapping/MapFormat.h - ${MAIN_LIB_DIR}/mapping/MapReaderH3M.h - ${MAIN_LIB_DIR}/mapping/MapFormatJson.h - ${MAIN_LIB_DIR}/mapping/ObstacleProxy.h - - ${MAIN_LIB_DIR}/modding/CModHandler.h - ${MAIN_LIB_DIR}/modding/CModInfo.h - ${MAIN_LIB_DIR}/modding/CModVersion.h - ${MAIN_LIB_DIR}/modding/ContentTypeHandler.h - ${MAIN_LIB_DIR}/modding/IdentifierStorage.h - ${MAIN_LIB_DIR}/modding/ModIncompatibility.h - ${MAIN_LIB_DIR}/modding/ModScope.h - ${MAIN_LIB_DIR}/modding/ModUtility.h - - ${MAIN_LIB_DIR}/networkPacks/ArtifactLocation.h - ${MAIN_LIB_DIR}/networkPacks/BattleChanges.h - ${MAIN_LIB_DIR}/networkPacks/Component.h - ${MAIN_LIB_DIR}/networkPacks/EInfoWindowMode.h - ${MAIN_LIB_DIR}/networkPacks/EntityChanges.h - ${MAIN_LIB_DIR}/networkPacks/EOpenWindowMode.h - ${MAIN_LIB_DIR}/networkPacks/NetPacksBase.h - ${MAIN_LIB_DIR}/networkPacks/NetPackVisitor.h - ${MAIN_LIB_DIR}/networkPacks/PacksForClient.h - ${MAIN_LIB_DIR}/networkPacks/PacksForClientBattle.h - ${MAIN_LIB_DIR}/networkPacks/PacksForLobby.h - ${MAIN_LIB_DIR}/networkPacks/PacksForServer.h - ${MAIN_LIB_DIR}/networkPacks/SetStackEffect.h - ${MAIN_LIB_DIR}/networkPacks/StackLocation.h - - ${MAIN_LIB_DIR}/pathfinder/INodeStorage.h - ${MAIN_LIB_DIR}/pathfinder/CGPathNode.h - ${MAIN_LIB_DIR}/pathfinder/CPathfinder.h - ${MAIN_LIB_DIR}/pathfinder/NodeStorage.h - ${MAIN_LIB_DIR}/pathfinder/PathfinderOptions.h - ${MAIN_LIB_DIR}/pathfinder/PathfinderUtil.h - ${MAIN_LIB_DIR}/pathfinder/PathfindingRules.h - ${MAIN_LIB_DIR}/pathfinder/TurnInfo.h - - ${MAIN_LIB_DIR}/registerTypes/RegisterTypes.h - - ${MAIN_LIB_DIR}/rewardable/Configuration.h - ${MAIN_LIB_DIR}/rewardable/Info.h - ${MAIN_LIB_DIR}/rewardable/Interface.h - ${MAIN_LIB_DIR}/rewardable/Limiter.h - ${MAIN_LIB_DIR}/rewardable/Reward.h - - ${MAIN_LIB_DIR}/rmg/RmgArea.h - ${MAIN_LIB_DIR}/rmg/RmgObject.h - ${MAIN_LIB_DIR}/rmg/RmgPath.h - ${MAIN_LIB_DIR}/rmg/CMapGenerator.h - ${MAIN_LIB_DIR}/rmg/CMapGenOptions.h - ${MAIN_LIB_DIR}/rmg/CRmgTemplate.h - ${MAIN_LIB_DIR}/rmg/CRmgTemplateStorage.h - ${MAIN_LIB_DIR}/rmg/CZonePlacer.h - ${MAIN_LIB_DIR}/rmg/TileInfo.h - ${MAIN_LIB_DIR}/rmg/Zone.h - ${MAIN_LIB_DIR}/rmg/RmgMap.h - ${MAIN_LIB_DIR}/rmg/float3.h - ${MAIN_LIB_DIR}/rmg/Functions.h - ${MAIN_LIB_DIR}/rmg/modificators/Modificator.h - ${MAIN_LIB_DIR}/rmg/modificators/ObjectManager.h - ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.h - ${MAIN_LIB_DIR}/rmg/modificators/RoadPlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/TreasurePlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/QuestArtifactPlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/ConnectionsPlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/WaterAdopter.h - ${MAIN_LIB_DIR}/rmg/modificators/MinePlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/TownPlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/WaterProxy.h - ${MAIN_LIB_DIR}/rmg/modificators/WaterRoutes.h - ${MAIN_LIB_DIR}/rmg/modificators/RockPlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/RockFiller.h - ${MAIN_LIB_DIR}/rmg/modificators/ObstaclePlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/RiverPlacer.h - ${MAIN_LIB_DIR}/rmg/modificators/TerrainPainter.h - ${MAIN_LIB_DIR}/rmg/threadpool/BlockingQueue.h - ${MAIN_LIB_DIR}/rmg/threadpool/ThreadPool.h - ${MAIN_LIB_DIR}/rmg/threadpool/MapProxy.h - - ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.h - ${MAIN_LIB_DIR}/serializer/BinarySerializer.h - ${MAIN_LIB_DIR}/serializer/CLoadIntegrityValidator.h - ${MAIN_LIB_DIR}/serializer/CMemorySerializer.h - ${MAIN_LIB_DIR}/serializer/Connection.h - ${MAIN_LIB_DIR}/serializer/CSerializer.h - ${MAIN_LIB_DIR}/serializer/CTypeList.h - ${MAIN_LIB_DIR}/serializer/JsonDeserializer.h - ${MAIN_LIB_DIR}/serializer/JsonSerializeFormat.h - ${MAIN_LIB_DIR}/serializer/JsonSerializer.h - ${MAIN_LIB_DIR}/serializer/JsonUpdater.h - ${MAIN_LIB_DIR}/serializer/ILICReader.h - ${MAIN_LIB_DIR}/serializer/Cast.h - - ${MAIN_LIB_DIR}/spells/AbilityCaster.h - ${MAIN_LIB_DIR}/spells/AdventureSpellMechanics.h - ${MAIN_LIB_DIR}/spells/BattleSpellMechanics.h - ${MAIN_LIB_DIR}/spells/BonusCaster.h - ${MAIN_LIB_DIR}/spells/CSpellHandler.h - ${MAIN_LIB_DIR}/spells/ExternalCaster.h - ${MAIN_LIB_DIR}/spells/ISpellMechanics.h - ${MAIN_LIB_DIR}/spells/ObstacleCasterProxy.h - ${MAIN_LIB_DIR}/spells/Problem.h - ${MAIN_LIB_DIR}/spells/ProxyCaster.h - ${MAIN_LIB_DIR}/spells/TargetCondition.h - ${MAIN_LIB_DIR}/spells/ViewSpellInt.h - - ${MAIN_LIB_DIR}/spells/effects/Catapult.h - ${MAIN_LIB_DIR}/spells/effects/Clone.h - ${MAIN_LIB_DIR}/spells/effects/Damage.h - ${MAIN_LIB_DIR}/spells/effects/DemonSummon.h - ${MAIN_LIB_DIR}/spells/effects/Dispel.h - ${MAIN_LIB_DIR}/spells/effects/Effect.h - ${MAIN_LIB_DIR}/spells/effects/Effects.h - ${MAIN_LIB_DIR}/spells/effects/EffectsFwd.h - ${MAIN_LIB_DIR}/spells/effects/Heal.h - ${MAIN_LIB_DIR}/spells/effects/LocationEffect.h - ${MAIN_LIB_DIR}/spells/effects/Obstacle.h - ${MAIN_LIB_DIR}/spells/effects/Registry.h - ${MAIN_LIB_DIR}/spells/effects/UnitEffect.h - ${MAIN_LIB_DIR}/spells/effects/Summon.h - ${MAIN_LIB_DIR}/spells/effects/Teleport.h - ${MAIN_LIB_DIR}/spells/effects/Timed.h - ${MAIN_LIB_DIR}/spells/effects/RemoveObstacle.h - ${MAIN_LIB_DIR}/spells/effects/Sacrifice.h - - ${MAIN_LIB_DIR}/AI_Base.h - ${MAIN_LIB_DIR}/ArtifactUtils.h - ${MAIN_LIB_DIR}/BattleFieldHandler.h - ${MAIN_LIB_DIR}/CAndroidVMHelper.h - ${MAIN_LIB_DIR}/CArtHandler.h - ${MAIN_LIB_DIR}/CArtifactInstance.h - ${MAIN_LIB_DIR}/CBonusTypeHandler.h - ${MAIN_LIB_DIR}/CBuildingHandler.h - ${MAIN_LIB_DIR}/CConfigHandler.h - ${MAIN_LIB_DIR}/CConsoleHandler.h - ${MAIN_LIB_DIR}/CCreatureHandler.h - ${MAIN_LIB_DIR}/CCreatureSet.h - ${MAIN_LIB_DIR}/CGameInfoCallback.h - ${MAIN_LIB_DIR}/CGameInterface.h - ${MAIN_LIB_DIR}/CGeneralTextHandler.h - ${MAIN_LIB_DIR}/CHeroHandler.h - ${MAIN_LIB_DIR}/CondSh.h - ${MAIN_LIB_DIR}/ConstTransitivePtr.h - ${MAIN_LIB_DIR}/Color.h - ${MAIN_LIB_DIR}/CPlayerState.h - ${MAIN_LIB_DIR}/CRandomGenerator.h - ${MAIN_LIB_DIR}/CScriptingModule.h - ${MAIN_LIB_DIR}/CSkillHandler.h - ${MAIN_LIB_DIR}/CSoundBase.h - ${MAIN_LIB_DIR}/CStack.h - ${MAIN_LIB_DIR}/CStopWatch.h - ${MAIN_LIB_DIR}/CThreadHelper.h - ${MAIN_LIB_DIR}/CTownHandler.h - ${MAIN_LIB_DIR}/FunctionList.h - ${MAIN_LIB_DIR}/GameConstants.h - ${MAIN_LIB_DIR}/GameSettings.h - ${MAIN_LIB_DIR}/IBonusTypeHandler.h - ${MAIN_LIB_DIR}/IGameCallback.h - ${MAIN_LIB_DIR}/IGameEventsReceiver.h - ${MAIN_LIB_DIR}/IHandlerBase.h - ${MAIN_LIB_DIR}/int3.h - ${MAIN_LIB_DIR}/JsonDetail.h - ${MAIN_LIB_DIR}/JsonNode.h - ${MAIN_LIB_DIR}/JsonRandom.h - ${MAIN_LIB_DIR}/Languages.h - ${MAIN_LIB_DIR}/LoadProgress.h - ${MAIN_LIB_DIR}/LogicalExpression.h - ${MAIN_LIB_DIR}/MetaString.h - ${MAIN_LIB_DIR}/ObstacleHandler.h - ${MAIN_LIB_DIR}/Point.h - ${MAIN_LIB_DIR}/Rect.h - ${MAIN_LIB_DIR}/Rect.cpp - ${MAIN_LIB_DIR}/ResourceSet.h - ${MAIN_LIB_DIR}/RiverHandler.h - ${MAIN_LIB_DIR}/RoadHandler.h - ${MAIN_LIB_DIR}/ScriptHandler.h - ${MAIN_LIB_DIR}/ScopeGuard.h - ${MAIN_LIB_DIR}/StartInfo.h - ${MAIN_LIB_DIR}/TerrainHandler.h - ${MAIN_LIB_DIR}/TextOperations.h - ${MAIN_LIB_DIR}/TurnTimerInfo.h - ${MAIN_LIB_DIR}/UnlockGuard.h - ${MAIN_LIB_DIR}/VCMIDirs.h - ${MAIN_LIB_DIR}/vcmi_endian.h - ${MAIN_LIB_DIR}/VCMI_Lib.h - ) - - assign_source_group(${lib_SRCS} ${lib_HEADERS}) - - add_library(${TARGET_NAME} ${LIBRARY_TYPE} ${lib_SRCS} ${lib_HEADERS}) - set_target_properties(${TARGET_NAME} PROPERTIES COMPILE_DEFINITIONS "VCMI_DLL=1") - target_link_libraries(${TARGET_NAME} PUBLIC - minizip::minizip ZLIB::ZLIB - ${SYSTEM_LIBS} Boost::boost Boost::thread Boost::filesystem Boost::program_options Boost::locale Boost::date_time - ) - if(APPLE_IOS) - target_link_libraries(${TARGET_NAME} PUBLIC iOS_utils) - endif() - - target_include_directories(${TARGET_NAME} - PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} - PUBLIC ${MAIN_LIB_DIR}/.. - PUBLIC ${MAIN_LIB_DIR}/../include - PUBLIC ${MAIN_LIB_DIR} - ) - - if(WIN32) - set_target_properties(${TARGET_NAME} - PROPERTIES - OUTPUT_NAME "VCMI_lib" - PROJECT_LABEL "VCMI_lib" - ) - endif() - - vcmi_set_output_dir(${TARGET_NAME} "") - - enable_pch(${TARGET_NAME}) - - # We want to deploy assets into build directory for easier debugging without install - if(COPY_CONFIG_ON_BUILD) - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config - COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods - COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config - COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods - ) - endif() - - # Update version before vcmi compiling - if(TARGET update_version) - add_dependencies(${TARGET_NAME} update_version) - endif() - - if("${LIBRARY_TYPE}" STREQUAL SHARED) - install(TARGETS ${TARGET_NAME} RUNTIME DESTINATION ${LIB_DIR} LIBRARY DESTINATION ${LIB_DIR}) - endif() - if(APPLE_IOS AND NOT USING_CONAN) - get_target_property(LINKED_LIBS ${TARGET_NAME} LINK_LIBRARIES) - foreach(LINKED_LIB IN LISTS LINKED_LIBS) - if(NOT TARGET ${LINKED_LIB}) - if(LINKED_LIB MATCHES "\\${CMAKE_SHARED_LIBRARY_SUFFIX}$") - install(FILES ${LINKED_LIB} DESTINATION ${LIB_DIR}) - endif() - continue() - endif() - - get_target_property(LIB_TYPE ${LINKED_LIB} TYPE) - if(NOT LIB_TYPE STREQUAL "SHARED_LIBRARY") - continue() - endif() - - get_target_property(_aliased ${LINKED_LIB} ALIASED_TARGET) - if(_aliased) - set(LINKED_LIB_REAL ${_aliased}) - else() - set(LINKED_LIB_REAL ${LINKED_LIB}) - endif() - - get_target_property(_imported ${LINKED_LIB_REAL} IMPORTED) - if(_imported) - set(INSTALL_TYPE IMPORTED_RUNTIME_ARTIFACTS) - get_target_property(BOOST_DEPENDENCIES ${LINKED_LIB_REAL} INTERFACE_LINK_LIBRARIES) - foreach(BOOST_DEPENDENCY IN LISTS BOOST_DEPENDENCIES) - get_target_property(BOOST_DEPENDENCY_TYPE ${BOOST_DEPENDENCY} TYPE) - if(BOOST_DEPENDENCY_TYPE STREQUAL "SHARED_LIBRARY") - install(IMPORTED_RUNTIME_ARTIFACTS ${BOOST_DEPENDENCY} LIBRARY DESTINATION ${LIB_DIR}) - endif() - endforeach() - else() - set(INSTALL_TYPE TARGETS) - endif() - install(${INSTALL_TYPE} ${LINKED_LIB_REAL} LIBRARY DESTINATION ${LIB_DIR}) - endforeach() - endif() -endmacro() diff --git a/cmake_modules/VersionDefinition.cmake b/cmake_modules/VersionDefinition.cmake index f9fdb6992..f3db8fd89 100644 --- a/cmake_modules/VersionDefinition.cmake +++ b/cmake_modules/VersionDefinition.cmake @@ -1,5 +1,5 @@ set(VCMI_VERSION_MAJOR 1) -set(VCMI_VERSION_MINOR 4) +set(VCMI_VERSION_MINOR 5) set(VCMI_VERSION_PATCH 0) add_definitions( -DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR} diff --git a/cmake_modules/create_link.cmake b/cmake_modules/create_link.cmake new file mode 100644 index 000000000..c5ea762ad --- /dev/null +++ b/cmake_modules/create_link.cmake @@ -0,0 +1,14 @@ + +#message(${CMAKE_ARGV0}) # cmake.exe +#message(${CMAKE_ARGV1}) # -P +#message(${CMAKE_ARGV2}) # thisfilename +#message(${CMAKE_ARGV3}) # existing +#message(${CMAKE_ARGV4}) # linkname +if (WIN32) + file(TO_NATIVE_PATH ${CMAKE_ARGV3} existing_native) + file(TO_NATIVE_PATH ${CMAKE_ARGV4} linkname_native) + execute_process(COMMAND cmd.exe /c RD /Q "${linkname_native}") + execute_process(COMMAND cmd.exe /c mklink /J "${linkname_native}" "${existing_native}") +else() + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_ARGV3} ${CMAKE_ARGV4}) +endif() diff --git a/config/ai/nkai/nkai-settings.json b/config/ai/nkai/nkai-settings.json new file mode 100644 index 000000000..a77362420 --- /dev/null +++ b/config/ai/nkai/nkai-settings.json @@ -0,0 +1,7 @@ +{ + "maxRoamingHeroes" : 8, + "maxpass" : 30, + "mainHeroTurnDistanceLimit" : 10, + "scoutHeroTurnDistanceLimit" : 5, + "maxGoldPreasure" : 0.3 +} \ No newline at end of file diff --git a/config/ai/object-priorities.txt b/config/ai/nkai/object-priorities.txt similarity index 100% rename from config/ai/object-priorities.txt rename to config/ai/nkai/object-priorities.txt diff --git a/config/artifacts.json b/config/artifacts.json index c58da9263..ae5501f30 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -1730,7 +1730,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel1", + "subtype" : "creatureLevel2", "val" : 5, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1743,7 +1743,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel2", + "subtype" : "creatureLevel3", "val" : 4, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1756,7 +1756,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel3", + "subtype" : "creatureLevel4", "val" : 3, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1769,7 +1769,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel4", + "subtype" : "creatureLevel5", "val" : 2, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1782,7 +1782,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel5", + "subtype" : "creatureLevel6", "val" : 1, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1873,6 +1873,7 @@ } ], "index" : 127, + "class" : "SPECIAL", "type" : ["HERO"] }, "armageddonsBlade": @@ -1917,6 +1918,7 @@ } ], "index" : 128, + "class" : "SPECIAL", "type" : ["HERO"] }, "angelicAlliance": diff --git a/config/bonuses.json b/config/bonuses.json index 877161cf8..b6d3ca125 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -145,6 +145,14 @@ } }, + "ENEMY_ATTACK_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RATT" + } + }, + "ENEMY_DEFENCE_REDUCTION": { "graphics": @@ -185,6 +193,14 @@ } }, + "FEROCITY": + { + "graphics": + { + "icon": "zvs/Lib1.res/Ferocity" + } + }, + "FLYING": { "graphics": @@ -428,6 +444,14 @@ } }, + "REVENGE": + { + "graphics": + { + "icon": "zvs/Lib1.res/Revenge" + } + }, + "SHOOTER": { "graphics": diff --git a/config/commanders.json b/config/commanders.json index 447a25570..d1267f47c 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -3,9 +3,9 @@ //Commander receives these bonuses on level-up "bonusPerLevel": [ - ["CREATURE_DAMAGE", 1, "creatureDamageMin", 0 ], //+1 minimum damage - ["CREATURE_DAMAGE", 2, "creatureDamageMax", 0 ], //+2 maximum damage - ["STACK_HEALTH", 5, null, 0 ] //+5 hp + ["CREATURE_DAMAGE", 2, "creatureDamageMin", 0 ], //+2 minimum damage + ["CREATURE_DAMAGE", 4, "creatureDamageMax", 0 ], //+4 maximum damage + ["STACK_HEALTH", 20, null, 0 ] //+20 hp ], //Value of bonuses given by each skill level "skillLevels": diff --git a/config/creatures/dungeon.json b/config/creatures/dungeon.json index ef0d4e368..e3e03e9b1 100644 --- a/config/creatures/dungeon.json +++ b/config/creatures/dungeon.json @@ -411,12 +411,6 @@ "type" : "LEVEL_SPELL_IMMUNITY", "val" : 5 }, - "hateGiants" : - { - "type" : "HATE", - "subtype" : "creature.giant", - "val" : 50 - }, "hateTitans" : { "type" : "HATE", diff --git a/config/difficulty.json b/config/difficulty.json index a3e8beffa..50f93d545 100644 --- a/config/difficulty.json +++ b/config/difficulty.json @@ -28,7 +28,7 @@ }, "king": { - "resources": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, + "resources": { "wood" : 0, "mercury": 0, "ore": 0, "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, "globalBonuses": [], "battleBonuses": [] } diff --git a/config/filesystem.json b/config/filesystem.json index 0a388c31b..e57599a78 100644 --- a/config/filesystem.json +++ b/config/filesystem.json @@ -28,7 +28,7 @@ {"type" : "snd", "path" : "Data/H3ab_ahd.snd"}, {"type" : "snd", "path" : "Data/Heroes3.snd"}, {"type" : "snd", "path" : "Data/Heroes3-cd2.snd"}, - //WoG have overriden sounds with .82m extension in Data + //WoG have overridden sounds with .82m extension in Data {"type" : "dir", "path" : "Data", "depth": 0} ], "MUSIC/": diff --git a/config/gameConfig.json b/config/gameConfig.json index 5b6e17b5c..5c13bb56b 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -289,7 +289,9 @@ // Chances for a hero with default army to receive corresponding stack out of his predefined starting troops "startingStackChances": [ 100, 88, 25], // number of artifacts that can fit in a backpack. -1 is unlimited. - "backpackSize" : -1 + "backpackSize" : -1, + // if heroes are invitable in tavern + "tavernInvite" : false }, "towns": @@ -371,6 +373,8 @@ "pathfinder" : { + // if enabled, pathfinder will build path through locations guarded by wandering monsters + "ignoreGuards" : false, // if enabled, pathfinder will take use of any available boats "useBoat" : true, // if enabled, pathfinder will take use of any bidirectional monoliths @@ -380,7 +384,9 @@ // if enabled, pathfinder will take use of one-way monoliths with multiple exits. "useMonolithOneWayRandom" : false, // if enabled and hero has whirlpool protection effect, pathfinder will take use of whirpools - "useWhirlpool" : true + "useWhirlpool" : true, + // if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied + "originalFlyRules" : false }, "bonuses" : diff --git a/config/heroClasses.json b/config/heroClasses.json index cf4f65d01..6fa79b64a 100644 --- a/config/heroClasses.json +++ b/config/heroClasses.json @@ -99,22 +99,22 @@ "mapObject" : { "templates" : { "default" : { "animation" : "AH09_.def", "editorAnimation": "AH09_E.def" } } }, "animation": { "battle" : { "male" : "CH08.DEF", "female" : "CH09.DEF" } } }, - "warlock" : + "overlord" : { "index": 10, "faction" : "dungeon", "defaultTavern" : 5, - "affinity" : "magic", + "affinity" : "might", "commander" : "medusaQueen", "mapObject" : { "templates" : { "default" : { "animation" : "AH11_.def", "editorAnimation": "AH11_E.def" } } }, "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } } }, - "overlord" : + "warlock" : { "index": 11, "faction" : "dungeon", "defaultTavern" : 5, - "affinity" : "might", + "affinity" : "magic", "commander" : "medusaQueen", "mapObject" : { "templates" : { "default" : { "animation" : "AH10_.def", "editorAnimation": "AH10_E.def" } } }, "animation": { "battle" : { "male" : "CH010.DEF", "female" : "CH11.DEF" } } diff --git a/config/heroes/dungeon.json b/config/heroes/dungeon.json index e08959d35..04fb2d3c4 100644 --- a/config/heroes/dungeon.json +++ b/config/heroes/dungeon.json @@ -2,7 +2,7 @@ "lorelei": { "index": 80, - "class" : "warlock", + "class" : "overlord", "female": true, "skills": [ @@ -16,7 +16,7 @@ "arlach": { "index": 81, - "class" : "warlock", + "class" : "overlord", "female": false, "skills": [ @@ -30,7 +30,7 @@ "dace": { "index": 82, - "class" : "warlock", + "class" : "overlord", "female": false, "skills": [ @@ -44,7 +44,7 @@ "ajit": { "index": 83, - "class" : "warlock", + "class" : "overlord", "female": false, "skills": [ @@ -58,7 +58,7 @@ "damacon": { "index": 84, - "class" : "warlock", + "class" : "overlord", "female": false, "skills": [ @@ -77,7 +77,7 @@ "gunnar": { "index": 85, - "class" : "warlock", + "class" : "overlord", "female": false, "skills": [ @@ -100,7 +100,7 @@ "synca": { "index": 86, - "class" : "warlock", + "class" : "overlord", "female": true, "skills": [ @@ -114,7 +114,7 @@ "shakti": { "index": 87, - "class" : "warlock", + "class" : "overlord", "female": false, "skills": [ @@ -128,7 +128,7 @@ "alamar": { "index": 88, - "class" : "overlord", + "class" : "warlock", "spellbook": [ "resurrection" ], "female": false, "skills": @@ -150,7 +150,7 @@ "jaegar": { "index": 89, - "class" : "overlord", + "class" : "warlock", "female": false, "spellbook": [ "shield" ], "skills": @@ -173,7 +173,7 @@ "malekith": { "index": 90, - "class" : "overlord", + "class" : "warlock", "female": false, "spellbook": [ "bloodlust" ], "skills": @@ -197,7 +197,7 @@ "jeddite": { "index": 91, - "class" : "overlord", + "class" : "warlock", "female": true, "spellbook": [ "resurrection" ], "skills": @@ -218,7 +218,7 @@ "geon": { "index": 92, - "class" : "overlord", + "class" : "warlock", "female": false, "spellbook": [ "slow" ], "skills": @@ -241,7 +241,7 @@ "deemer": { "index": 93, - "class" : "overlord", + "class" : "warlock", "female": false, "spellbook": [ "meteorShower" ], "skills": @@ -263,7 +263,7 @@ "sephinroth": { "index": 94, - "class" : "overlord", + "class" : "warlock", "female": true, "spellbook": [ "protectAir" ], "skills": @@ -284,7 +284,7 @@ "darkstorn": { "index": 95, - "class" : "overlord", + "class" : "warlock", "female": false, "spellbook": [ "stoneSkin" ], "skills": diff --git a/config/heroes/stronghold.json b/config/heroes/stronghold.json index e940ef6a8..13238de3d 100644 --- a/config/heroes/stronghold.json +++ b/config/heroes/stronghold.json @@ -237,13 +237,13 @@ ], "specialty" : { "bonuses" : { - "sorcery" : { - "targetSourceType" : "SECONDARY_SKILL", - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.any", + "offence" : { + "subtype" : "damageTypeMelee", + "type" : "PERCENTAGE_DAMAGE_BOOST", "updater" : "TIMES_HERO_LEVEL", "val" : 5, - "valueType" : "PERCENT_TO_TARGET_TYPE" + "valueType" : "PERCENT_TO_TARGET_TYPE", + "targetSourceType" : "SECONDARY_SKILL" } } } diff --git a/config/heroes/tower.json b/config/heroes/tower.json index 0fecfc54b..4ddc15e86 100644 --- a/config/heroes/tower.json +++ b/config/heroes/tower.json @@ -70,6 +70,7 @@ "torosar": { "index": 36, + "compatibilityIdentifiers" : [ "torosar " ], "class" : "alchemist", "female": false, "spellbook": [ "magicArrow" ], diff --git a/config/mapOverrides.json b/config/mapOverrides.json index 9a520fe7c..142c87307 100644 --- a/config/mapOverrides.json +++ b/config/mapOverrides.json @@ -7,7 +7,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 17 } ] + [ "control", { "type" : "creatureGeneratorCommon" } ] ], "effect" : { "messageToSend" : "core.genrltxt.289", @@ -35,7 +35,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 9, 64, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 9, 64, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -47,7 +47,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 13, 63, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 13, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -95,7 +95,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 16, 23, 0 ], "type" : 84 } ] + [ "transport", { "position" : [ 16, 23, 0 ], "type" : "artifact.spiritOfOppression" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -135,7 +135,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 2 } ] + [ "haveArtifact", { "type" : "artifact.grail" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -163,7 +163,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 63, 65, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 63, 65, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -205,7 +205,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 102, 18, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 102, 18, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -217,7 +217,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 128 } ] + [ "haveArtifact", { "type" : "artifact.armageddonsBlade" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -246,7 +246,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 7, 66, 0 ], "type" : 34 } ] // Catherine + [ "control", { "position" : [ 7, 66, 0 ], "type" : "hero" } ] // Catherine ] ], "effect" : { @@ -260,7 +260,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 18, 68, 0 ], "type" : 34 } ] // Roland + [ "control", { "position" : [ 18, 68, 0 ], "type" : "hero" } ] // Roland ] ], "effect" : { @@ -274,7 +274,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 58, 12, 0 ], "type" : 34 } ] //Gelu + [ "control", { "position" : [ 58, 12, 0 ], "type" : "hero" } ] //Gelu ] ], "effect" : { @@ -287,7 +287,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 33, 37, 0 ], "type" : 34 } ] + [ "destroy", { "position" : [ 33, 37, 0 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -316,7 +316,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 4, 3, 0 ], "type" : 34 } ] // Gelu + [ "control", { "position" : [ 4, 3, 0 ], "type" : "hero" } ] // Gelu ] ], "effect" : { @@ -330,7 +330,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 12, 63, 0 ], "type" : 34 } ] // Catherine + [ "control", { "position" : [ 12, 63, 0 ], "type" : "hero" } ] // Catherine ] ], "effect" : { @@ -344,7 +344,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 63, 51, 0 ], "type" : 34 } ] // Roland + [ "control", { "position" : [ 63, 51, 0 ], "type" : "hero" } ] // Roland ] ], "effect" : { @@ -369,7 +369,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 35, 36, 0 ], "type" : 128 } ] + [ "transport", { "position" : [ 35, 36, 0 ], "type" : "artifact.armageddonsBlade" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -397,7 +397,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 99, 101, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 99, 101, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -421,7 +421,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 53 } ] + [ "control", { "type" : "mine" } ] ], "effect" : { "messageToSend" : "core.genrltxt.291", @@ -449,7 +449,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 104, 61, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 104, 61, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -470,7 +470,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 6, 49, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 6, 49, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -497,7 +497,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 88, 82, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 88, 82, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -518,7 +518,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 107, 3, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 107, 3, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -545,7 +545,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 23, 1, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 23, 1, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -566,7 +566,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 57, 61, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 57, 61, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -594,7 +594,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 10, 59, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 10, 59, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -608,7 +608,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 4, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 4, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -622,7 +622,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 15, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 15, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -636,7 +636,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 10, 66, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 10, 66, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -686,7 +686,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 94, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 94, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -700,7 +700,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 93, 51, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 93, 51, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -714,7 +714,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 85, 64, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 85, 64, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -728,7 +728,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [100, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [100, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -766,7 +766,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 34, 38, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 34, 38, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -780,7 +780,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 32, 7, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 32, 7, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -794,7 +794,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 8, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 8, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -808,7 +808,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -838,7 +838,7 @@ "message" : "core.genrltxt.7" }, "specialVictory" : { - "condition" : [ "destroy", { "type" : 54} ], + "condition" : [ "destroy", { "type" : "monster"} ], "effect" : { "messageToSend" : "vcmi.map.victoryCondition.eliminateMonsters.toOthers", "type" : "victory" @@ -857,7 +857,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 1, 3, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 1, 3, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -878,7 +878,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 62, 5, 1 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 62, 5, 1 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -905,7 +905,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 69, 69, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 69, 69, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -933,7 +933,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "type" : 54} ] + [ "destroy", { "type" : "monster"} ] ], "effect" : { "messageToSend" : "vcmi.map.victoryCondition.eliminateMonsters.toOthers", @@ -954,7 +954,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 17 } ] + [ "control", { "type" : "creatureGeneratorCommon" } ] ], "effect" : { "messageToSend" : "core.genrltxt.289", @@ -994,7 +994,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 60, 45, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 60, 45, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1044,7 +1044,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 19, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 19, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1058,7 +1058,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 17, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 17, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1068,7 +1068,7 @@ "message" : "core.genrltxt.253" }, "specialVictory" : { - "condition" : [ "haveArtifact", { "type" : 54 } ], + "condition" : [ "haveArtifact", { "type" : "artifact.amuletOfTheUndertaker" } ], "effect" : { "messageToSend" : "core.genrltxt.281", "type" : "victory" @@ -1096,7 +1096,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 30, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 30, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1110,7 +1110,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 30, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 30, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1123,7 +1123,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 9, 11, 0 ], "type" : 55 } ] + [ "transport", { "position" : [ 9, 11, 0 ], "type" : "artifact.vampiresCowl" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -1152,7 +1152,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 55, 17, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 55, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1166,7 +1166,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 53, 17, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 53, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1179,7 +1179,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 53, 16, 0 ], "type" : 56 } ] + [ "transport", { "position" : [ 53, 16, 0 ], "type" : "artifact.deadMansBoots" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -1207,7 +1207,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 58, 70, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 58, 70, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1219,7 +1219,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 20 } ] + [ "haveArtifact", { "type" : "artifact.skullHelmet" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1247,7 +1247,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 69, 2, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 69, 2, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1259,7 +1259,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 8 } ] + [ "haveArtifact", { "type" : "artifact.blackshardOfTheDeadKnight" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1287,7 +1287,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 2, 1, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 2, 1, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1299,7 +1299,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 26 } ] + [ "haveArtifact", { "type" : "artifact.ribCage" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1327,7 +1327,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 10, 11, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 10, 11, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1339,7 +1339,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 14 } ] + [ "haveArtifact", { "type" : "artifact.shieldOfTheYawningDead" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1368,7 +1368,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 56, 54, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 56, 54, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1382,7 +1382,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 53, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 53, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1420,7 +1420,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 25, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 25, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1434,7 +1434,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 67, 25, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 67, 25, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1471,7 +1471,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 71, 14, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 71, 14, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1495,7 +1495,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 68, 4, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 68, 4, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -1515,6 +1515,217 @@ "victoryIconIndex" : 6, "victoryString" : "core.vcdesc.7" }, + "data/yog:1" : { // The Meeting + "defeatIconIndex" : 1, + "defeatString" : "core.lcdesc.3", + "triggeredEvents" : { + "heroMustSurvive" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "noneOf", [ "control", { "position" : [ 31, 32, 0 ], "type" : "hero" } ] ] // yog + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.253" + }, + "specialVictory" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "control", { "position" : [ 14, 16, 0 ], "type" : "town" } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.250", + "type" : "victory" + }, + "message" : "core.genrltxt.249" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 6, + "victoryString" : "core.vcdesc.7" + }, + "data/yog:2" : { // A Tough Start + "defeatIconIndex" : 1, + "defeatString" : "core.lcdesc.3", + "triggeredEvents" : { + "heroMustSurvive" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "noneOf", [ "control", { "position" : [ 33, 36, 0 ], "type" : "hero" } ] ] // yog + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.253" + }, + "specialVictory" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.281", + "type" : "victory" + }, + "message" : "core.genrltxt.280" + }, + "specialDefeat" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "anyOf", + [ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.armorOfWonder" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.sandalsOfTheSaint" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.celestialNecklaceOfBliss" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.lionsShieldOfCourage" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.swordOfJudgement" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.angelicAlliance" } ], [ "haveArtifact", { "type" : "artifact.helmOfHeavenlyEnlightenment" } ] ] + ], + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 0, + "victoryString" : "core.vcdesc.1" + }, + "data/yog:3" : { // Falor and Terwen + "defeatIconIndex" : 1, + "defeatString" : "core.lcdesc.3", + "triggeredEvents" : { + "heroMustSurvive" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "noneOf", [ "control", { "position" : [ 3, 5, 0 ], "type" : "hero" } ] ] // yog + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.253" + }, + "specialVictory" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.281", + "type" : "victory" + }, + "message" : "core.genrltxt.280" + }, + "specialDefeat" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "anyOf", + [ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.celestialNecklaceOfBliss" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.lionsShieldOfCourage" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.swordOfJudgement" } ] ], + [ "noneOf", [ "haveArtifact", { "type" : "artifact.helmOfHeavenlyEnlightenment" } ] ] + ], + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 0, + "victoryString" : "core.vcdesc.1" + }, + "data/yog:4" : { // Returning to Bracada + "defeatIconIndex" : 1, + "defeatString" : "core.lcdesc.3", + "triggeredEvents" : { + "heroMustSurvive" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "noneOf", [ "control", { "position" : [ 32, 5, 0 ], "type" : "hero" } ] ] // yog + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.253" + }, + "specialVictory" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.281", + "type" : "victory" + }, + "message" : "core.genrltxt.280" + }, + "specialDefeat" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "anyOf", + [ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.swordOfJudgement" } ] ], + //[ "noneOf", [ "haveArtifact", { "type" : "artifact.pendantOfCourage" } ], [ "haveArtifact", { "type" : "artifact.helmOfHeavenlyEnlightenment" } ] ] + ], + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 0, + "victoryString" : "core.vcdesc.1" + }, "data/final:3" : { // Final Peace "defeatIconIndex" : 1, "defeatString" : "core.lcdesc.2", @@ -1524,7 +1735,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 57, 12, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 57, 12, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1538,7 +1749,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 11, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 11, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1551,7 +1762,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 8, 29, 1 ], "type" : 34 } ] + [ "destroy", { "position" : [ 8, 29, 1 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -1580,7 +1791,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 54, 6, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 54, 6, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1594,7 +1805,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 13, 6, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 13, 6, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1632,7 +1843,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 34, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 34, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1646,7 +1857,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 36, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 36, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1659,7 +1870,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 36, 67, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 36, 67, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -1688,7 +1899,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 21, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 21, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1702,7 +1913,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 44, 9, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 44, 9, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1746,7 +1957,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 66, 70, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 66, 70, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1760,7 +1971,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 70, 66, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 70, 66, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1804,7 +2015,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 7, 13, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 7, 13, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1818,7 +2029,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 9, 15, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 9, 15, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1832,7 +2043,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 6, 103, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 6, 103, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1846,7 +2057,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 9, 105, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 9, 105, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1889,7 +2100,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 14, 53, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 14, 53, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1903,7 +2114,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 21, 69, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 21, 69, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1917,7 +2128,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 38, 59, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 38, 59, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1931,7 +2142,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 66, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 66, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1980,7 +2191,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 68, 13, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 68, 13, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2004,7 +2215,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 4, 67, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 4, 67, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -2032,7 +2243,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 66, 17, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 66, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2044,7 +2255,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveResources", { "type" : 6, "value" : 100000 } ] + [ "haveResources", { "type" : "gold", "value" : 100000 } ] ], "effect" : { "messageToSend" : "core.genrltxt.279", @@ -2072,7 +2283,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 6, 8, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 6, 8, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2096,7 +2307,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 31, 26, 0 ], "type" : 34 } ] + [ "destroy", { "position" : [ 31, 26, 0 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -2124,7 +2335,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 24, 7, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 24, 7, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index 2e117356c..cfcaf2a7a 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -1055,9 +1055,8 @@ { "chance": 100, "guards": [ - { "amount": 20, "type": "goldGolem" }, + { "amount": 40, "type": "goldGolem" }, { "amount": 10, "type": "diamondGolem" }, - { "amount": 20, "type": "goldGolem" }, { "amount": 10, "type": "diamondGolem" } ], "combat_value": 786, diff --git a/config/objects/generic.json b/config/objects/generic.json index 396ddc374..23240bbd5 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -10,7 +10,11 @@ } }, "types" : { - "prison" : { "index" : 0, "aiValue" : 5000 } + "prison" : { + "index" : 0, + "aiValue" : 5000, + "removable": true + } } }, @@ -136,6 +140,7 @@ "object" : { "index" : 0, "aiValue" : 10000, + "removable": true, "templates" : { "normal" : { "animation" : "ava0128.def", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } }, @@ -150,6 +155,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { } } @@ -313,6 +319,7 @@ "object" : { "index" : 0, "aiValue" : 0, + "removable": true, "rmg" : { } } @@ -444,6 +451,7 @@ "object" : { "index" : 0, "aiValue" : 10000, + "removable": true, "rmg" : { } } @@ -495,6 +503,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 2000, "rarity" : 150 @@ -511,6 +520,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 5000, "rarity" : 150 @@ -527,6 +537,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 10000, "rarity" : 150 @@ -543,6 +554,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 20000, "rarity" : 150 @@ -572,6 +584,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon1", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -584,6 +597,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon2", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -596,6 +610,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon3", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -608,6 +623,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon4", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -632,6 +648,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon6", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -644,6 +661,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon7", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -687,6 +705,7 @@ "object" : { "index" : 0, "aiValue" : 0, + "removable": true, "rmg" : { } } @@ -723,6 +742,7 @@ "index" :95, "handler": "generic", "base" : { + "blockVisit": true, "sounds" : { "ambient" : ["LOOPTAV"], "visit" : ["STORE"] @@ -915,5 +935,16 @@ "grassHills" : { "index" :208, "handler": "static", "types" : { "object" : { "index" : 0} } }, "roughHills" : { "index" :209, "handler": "static", "types" : { "object" : { "index" : 0} } }, "subterraneanRocks" : { "index" :210, "handler": "static", "types" : { "object" : { "index" : 0} } }, - "swampFoliage" : { "index" :211, "handler": "static", "types" : { "object" : { "index" : 0} } } + "swampFoliage" : { "index" :211, "handler": "static", "types" : { "object" : { "index" : 0} } }, + + /// special object to handle invalid / unknown objects on some user-made maps + "nothing" : { + "index" : 0, + "handler": "generic", + "types" : { + "nothing" : { + "index" : 0 + } + } + } } diff --git a/config/objects/moddables.json b/config/objects/moddables.json index 2a2b31371..b97606eb9 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -8,6 +8,7 @@ "index" :5, "handler": "artifact", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] @@ -25,6 +26,7 @@ "handler": "hero", "base" : { "aiValue" : 7500, + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VVV", "VAV"] @@ -40,6 +42,7 @@ "index" :54, "handler": "monster", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] @@ -55,6 +58,7 @@ "index" :76, "handler": "randomResource", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VA" ] @@ -85,6 +89,7 @@ "handler": "resource", "lastReservedIndex" : 6, "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VA" ] @@ -138,6 +143,7 @@ "lastReservedIndex" : 2, "base" : { "aiValue" : 0, + "removable": true, "layer" : "sail", "onboardAssaultAllowed" : true, "onboardVisitAllowed" : true, @@ -175,6 +181,7 @@ "lastReservedIndex" : 7, "base" : { "aiValue" : 0, + "removable": true, "sounds" : { "visit" : ["CAVEHEAD"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] diff --git a/config/objects/rewardableOncePerHero.json b/config/objects/rewardableOncePerHero.json index 43065ed8d..779575307 100644 --- a/config/objects/rewardableOncePerHero.json +++ b/config/objects/rewardableOncePerHero.json @@ -249,7 +249,7 @@ "description" : "@core.arraytxt.202", "message" : 148, "appearChance" : { "max" : 34 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, { "description" : "@core.arraytxt.203", @@ -257,7 +257,7 @@ "appearChance" : { "min" : 34, "max" : 67 }, "limiter" : { "resources" : { "gold" : 2000 } }, "resources" : { "gold" : -2000 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, { "description" : "@core.arraytxt.204", @@ -265,7 +265,7 @@ "appearChance" : { "min" : 67 }, "limiter" : { "resources" : { "gems" : 10 } }, "resources" : { "gems" : -10 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, ] } diff --git a/config/objects/rewardableOnceVisitable.json b/config/objects/rewardableOnceVisitable.json index 353b2d666..5999d2cb5 100644 --- a/config/objects/rewardableOnceVisitable.json +++ b/config/objects/rewardableOnceVisitable.json @@ -41,6 +41,7 @@ "index" : 22, "handler": "configurable", "base" : { + "blockedVisitable" : true, "sounds" : { "visit" : ["MYSTERY"] } @@ -54,9 +55,7 @@ "rarity" : 100 }, "compatibilityIdentifiers" : [ "object" ], - "onVisitedMessage" : 38, - "blockedVisitable" : true, "visitMode" : "once", "selectMode" : "selectFirst", "rewards" : [ @@ -154,7 +153,7 @@ "onVisited" : [ { "message" : 163, - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] } ], "rewards" : [ @@ -162,25 +161,25 @@ "appearChance" : { "max" : 30 }, "message" : 162, "artifacts" : [ { "class" : "TREASURE" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 30, "max" : 80 }, "message" : 162, "artifacts" : [ { "class" : "MINOR" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 80, "max" : 95 }, "message" : 162, "artifacts" : [ { "class" : "MAJOR" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 95 }, "message" : 162, "artifacts" : [ { "class" : "RELIC" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] } ] } diff --git a/config/objects/rewardablePickable.json b/config/objects/rewardablePickable.json index 6b01cf3d6..764065a30 100644 --- a/config/objects/rewardablePickable.json +++ b/config/objects/rewardablePickable.json @@ -5,6 +5,8 @@ "index" : 12, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "ambient" : ["LOOPCAMP"], "visit" : ["EXPERNCE"], @@ -20,8 +22,6 @@ "rarity" : 500 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -48,6 +48,8 @@ "index" : 29, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["GENIE"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -62,15 +64,13 @@ "rarity" : 100 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ { "message" : 51, "appearChance" : { "max" : 25 }, - "removeObject" : true, + "removeObject" : true }, { "message" : 52, @@ -106,6 +106,8 @@ "index" : 82, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["CHEST"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -120,8 +122,6 @@ "rarity" : 500 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -157,6 +157,8 @@ "index" : 86, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["TREASURE"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -171,8 +173,6 @@ "rarity" : 50 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -208,6 +208,7 @@ "index" : 101, "handler": "configurable", "base" : { + "removable": true, "sounds" : { "visit" : ["CHEST"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] diff --git a/config/randomMap.json b/config/randomMap.json index 800a2f893..70a4d0dd6 100644 --- a/config/randomMap.json +++ b/config/randomMap.json @@ -3,8 +3,9 @@ { "treasure" : [ - { "min" : 2000, "max" : 6000, "density" : 1 }, - { "min" : 100, "max" : 1000, "density" : 5 } + { "min" : 4000, "max" : 12000, "density" : 1 }, + { "min" : 1000, "max" : 4000, "density" : 3 }, + { "min" : 100, "max" : 1000, "density" : 6 } ], "shipyard" : { diff --git a/config/schemas/artifact.json b/config/schemas/artifact.json index 5a7aed863..ab335a5ca 100644 --- a/config/schemas/artifact.json +++ b/config/schemas/artifact.json @@ -93,7 +93,7 @@ "map" : { "type" : "string", "description" : ".def file for adventure map", - "format" : "defFile" + "format" : "animationFile" } } }, diff --git a/config/schemas/creature.json b/config/schemas/creature.json index d5bc82167..c32fb8fcb 100644 --- a/config/schemas/creature.json +++ b/config/schemas/creature.json @@ -133,12 +133,12 @@ "animation" : { "type" : "string", "description" : "File with animation of this creature in battles", - "format" : "defFile" + "format" : "animationFile" }, "map" : { "type" : "string", "description" : "File with animation of this creature on adventure map", - "format" : "defFile" + "format" : "animationFile" }, "mapMask" : { "type" : "array", @@ -184,7 +184,7 @@ "projectile" : { "type" : "string", "description" : "Path to projectile animation", - "format" : "defFile" + "format" : "animationFile" }, "ray" : { "type" : "array", diff --git a/config/schemas/hero.json b/config/schemas/hero.json index 419836386..8d533387b 100644 --- a/config/schemas/hero.json +++ b/config/schemas/hero.json @@ -29,7 +29,14 @@ "battleImage" : { "type" : "string", "description" : "Custom animation to be used on battle, overrides hero class property", - "format" : "defFile" + "format" : "animationFile" + }, + "compatibilityIdentifiers" : { + "type" : "array", + "items" : { + "type" : "string", + }, + "description" : "Additional identifiers that may refer to this object, to provide compatibility after object has been renamed" }, "images" : { "type" : "object", diff --git a/config/schemas/heroClass.json b/config/schemas/heroClass.json index 9efdddaca..b6f5db342 100644 --- a/config/schemas/heroClass.json +++ b/config/schemas/heroClass.json @@ -42,12 +42,12 @@ "female" : { "type" : "string", "description" : "Female version", - "format" : "defFile" + "format" : "animationFile" }, "male" : { "type" : "string", "description" : "Male version", - "format" : "defFile" + "format" : "animationFile" } } } diff --git a/config/schemas/objectTemplate.json b/config/schemas/objectTemplate.json index 9450f5af8..d98f8e8c4 100644 --- a/config/schemas/objectTemplate.json +++ b/config/schemas/objectTemplate.json @@ -9,12 +9,12 @@ "animation" : { "type" : "string", "description" : "Path to def file with animation of this object", - "format" : "defFile" + "format" : "animationFile" }, "editorAnimation" : { "type" : "string", "description" : "Optional path to def file with animation of this object to use in map editor", - "format" : "defFile" + "format" : "animationFile" }, "visitableFrom" : { "type" : "array", diff --git a/config/schemas/obstacle.json b/config/schemas/obstacle.json index bbda0e589..9d245b23d 100644 --- a/config/schemas/obstacle.json +++ b/config/schemas/obstacle.json @@ -45,7 +45,7 @@ "type" : "string", "description" : "Image resource", "anyOf" : [ - { "format" : "defFile" }, + { "format" : "animationFile" }, { "format" : "imageFile" } ] }, diff --git a/config/schemas/river.json b/config/schemas/river.json index db88affaf..0c79b14da 100644 --- a/config/schemas/river.json +++ b/config/schemas/river.json @@ -20,7 +20,7 @@ { "type" : "string", "description" : "Name of file with river graphics", - "format" : "defFile" + "format" : "animationFile" }, "delta" : { diff --git a/config/schemas/road.json b/config/schemas/road.json index 623899ce5..bf9d045da 100644 --- a/config/schemas/road.json +++ b/config/schemas/road.json @@ -20,7 +20,7 @@ { "type" : "string", "description" : "Name of file with road graphics", - "format" : "defFile" + "format" : "animationFile" }, "moveCost" : { diff --git a/config/schemas/settings.json b/config/schemas/settings.json index d0dedb7fe..890b90dfa 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -39,7 +39,8 @@ "useSavePrefix", "savePrefix", "startTurnAutosave", - "enableUiEnhancements" + "enableUiEnhancements", + "audioMuteFocus" ], "properties" : { "playerName" : { @@ -131,6 +132,10 @@ "enableUiEnhancements" : { "type": "boolean", "default": true + }, + "audioMuteFocus" : { + "type": "boolean", + "default": false } } }, @@ -194,6 +199,7 @@ }, "driver" : { "type" : "string", + "defaultWindows" : "", "default" : "opengl", "description" : "preferred graphics backend driver name for SDL2" }, @@ -235,7 +241,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground" ], "properties" : { "heroMoveTime" : { "type" : "number", @@ -255,7 +261,7 @@ }, "quickCombat" : { "type" : "boolean", - "default" : true + "default" : false }, "objectAnimation" : { "type" : "boolean", @@ -288,13 +294,17 @@ "type" : "number", "default" : 128 }, + "hideBackground" : { + "type" : "boolean", + "default" : false + } } }, "battle" : { "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells" ], + "required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat", "queueSmallSlots", "queueSmallOutside" ], "properties" : { "speedFactor" : { "type" : "number", @@ -340,6 +350,18 @@ "enableAutocombatSpells" : { "type": "boolean", "default": true + }, + "endWithAutocombat" : { + "type": "boolean", + "default": false + }, + "queueSmallSlots" : { + "type": "number", + "default": 10 + }, + "queueSmallOutside" : { + "type": "boolean", + "default": false } } }, @@ -347,19 +369,23 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "server", "port", "localInformation", "playerAI", "alliedAI", "friendlyAI", "neutralAI", "enemyAI", "reconnect", "uuid", "names" ], + "required" : [ "localHostname", "localPort", "remoteHostname", "remotePort", "playerAI", "alliedAI", "friendlyAI", "neutralAI", "enemyAI" ], "properties" : { - "server" : { + "localHostname" : { "type" : "string", "default" : "127.0.0.1" }, - "port" : { + "localPort" : { "type" : "number", "default" : 3030 }, - "localInformation" : { + "remoteHostname" : { + "type" : "string", + "default" : "" + }, + "remotePort" : { "type" : "number", - "default" : 2 + "default" : 3030 }, "playerAI" : { "type" : "string", @@ -380,23 +406,6 @@ "enemyAI" : { "type" : "string", "default" : "BattleAI" - }, - "reconnect" : { - "type" : "boolean", - "default" : false - }, - "uuid" : { - "type" : "string", - "default" : "" - }, - "names" : { - "type" : "array", - "default" : [], - "items" : - { - "type" : "string", - "default" : "" - } } } }, @@ -454,7 +463,7 @@ "properties" : { "format" : { "type" : "string", - "default" : "[%c] %l %n - %m" + "default" : "[%c] %l [%t] %n - %m" } } }, @@ -556,11 +565,47 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "mapPreview" ], + "required" : [ "mapPreview", "accountID", "accountCookie", "displayName", "hostname", "port", "roomPlayerLimit", "roomType", "roomMode", "roomID" ], "properties" : { "mapPreview" : { "type" : "boolean", "default" : true + }, + "accountID" : { + "type" : "string", + "default" : "" + }, + "accountCookie" : { + "type" : "string", + "default" : "" + }, + "displayName" : { + "type" : "string", + "default" : "" + }, + "hostname" : { + "type" : "string", + "default" : "127.0.0.1" + }, + "port" : { + "type" : "number", + "default" : 30303 + }, + "roomPlayerLimit" : { + "type" : "number", + "default" : 2 + }, + "roomType" : { + "type" : "number", + "default" : 0 + }, + "roomMode" : { + "type" : "number", + "default" : 0 + }, + "roomID" : { + "type" : "string", + "default" : "" } } }, @@ -576,7 +621,9 @@ "compactTownCreatureInfo", "infoBarPick", "skipBattleIntroMusic", - "infoBarCreatureManagement" + "infoBarCreatureManagement", + "enableLargeSpellbook", + "skipAdventureMapAnimations" ], "properties" : { "showGrid" : { @@ -609,7 +656,15 @@ }, "infoBarCreatureManagement": { "type" : "boolean", - "default" : false + "default" : true + }, + "enableLargeSpellbook" : { + "type": "boolean", + "default": true + }, + "skipAdventureMapAnimations": { + "type": "boolean", + "default": false } } } diff --git a/config/schemas/spell.json b/config/schemas/spell.json index 8a29cd88b..dff3bc2df 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -15,13 +15,13 @@ { //assumed verticalPosition: top "type" : "string", - "format" : "defFile" + "format" : "animationFile" }, { "type" : "object", "properties" : { "verticalPosition" : {"type" : "string", "enum" :["top","bottom"]}, - "defName" : {"type" : "string", "format" : "defFile"}, + "defName" : {"type" : "string", "format" : "animationFile"}, "effectName" : { "type" : "string" } }, "additionalProperties" : false @@ -41,7 +41,7 @@ "items" : { "type" : "object", "properties" : { - "defName" : {"type" : "string", "format" : "defFile"}, + "defName" : {"type" : "string", "format" : "animationFile"}, "minimumAngle" : {"type" : "number", "minimum" : 0} }, "additionalProperties" : false @@ -166,7 +166,10 @@ "defaultGainChance" : { "type" : "number", "description" : "Gain chance by default for all factions" - + }, + "canCastOnSelf" : { + "type" : "boolean", + "description" : "If used as creature spell, unit can cast this spell on itself" }, "gainChance" : { "type" : "object", diff --git a/config/schemas/template.json b/config/schemas/template.json index c91f3bd4d..a1a353a6d 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -119,7 +119,7 @@ "description" : "Number of players that will be present on map (human or AI)", "type": "string" }, - "cpu" : { + "humans" : { "description" : "Optional, number of AI-only players", "type": "string" }, @@ -135,6 +135,10 @@ "description" : "Optional name - useful to have several template variations with same name", "type": "string" }, + "description" : { + "description" : "Optional info about template, author or special rules", + "type": "string" + }, "zones" : { "description" : "List of named zones", "type" : "object", diff --git a/config/schemas/terrain.json b/config/schemas/terrain.json index bc518106b..23aa598a3 100644 --- a/config/schemas/terrain.json +++ b/config/schemas/terrain.json @@ -35,7 +35,7 @@ { "type" : "string", "description" : "Name of file with graphicks", - "format" : "defFile" + "format" : "animationFile" }, "rockTerrain" : { diff --git a/config/spells/timed.json b/config/spells/timed.json index 0817cbad1..77edbabcd 100644 --- a/config/spells/timed.json +++ b/config/spells/timed.json @@ -1153,7 +1153,10 @@ }, "targetCondition" : { "noneOf" : { - "bonus.SIEGE_WEAPON" : "absolute" + "bonus.MIND_IMMUNITY" : "absolute", + "bonus.NON_LIVING" : "absolute", + "bonus.SIEGE_WEAPON" : "absolute", + "bonus.UNDEAD" : "absolute" } } }, @@ -1221,7 +1224,7 @@ "effects" : { "attacksNearestCreature" : { "type" : "ATTACKS_NEAREST_CREATURE", - "duration" : "UNTIL_ATTACK" + "duration" : "UNTIL_OWN_ATTACK" } } }, diff --git a/config/terrainViewPatterns.json b/config/terrainViewPatterns.json index 571227ccf..783edd3c2 100644 --- a/config/terrainViewPatterns.json +++ b/config/terrainViewPatterns.json @@ -85,7 +85,8 @@ "N", "N", "N", "N", "N", "N" ], - "mapping" : { "normal" : "49-72", "dirt" : "21-44", "sand" : "0-23", "water" : "20-32", "rock": "0-7", "hota" : "77-117" } + "decoration" : true, + "mapping" : { "normal" : "49-56,57-72", "dirt" : "21-28,29-44", "sand" : "0-11,12-23", "water" : "20-32", "rock": "0-7", "hota" : "77-101,102-117" } }, // Mixed transitions { diff --git a/config/widgets/optionsTab.json b/config/widgets/advancedOptionsTab.json similarity index 69% rename from config/widgets/optionsTab.json rename to config/widgets/advancedOptionsTab.json index bd8c76126..1175ce8b1 100644 --- a/config/widgets/optionsTab.json +++ b/config/widgets/advancedOptionsTab.json @@ -73,41 +73,16 @@ "adoptHeight": true }, + // timer { - "name": "simturnsDuration", - "type": "slider", - "orientation": "horizontal", - "position": {"x": 55, "y": 537}, - "size": 194, - "callback": "setSimturnDuration", - "itemsVisible": 1, - "itemsTotal": 28, - "selected": 0, - "style": "blue", - "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, - "panningStep": 20 - }, - - { - "name": "labelSimturnsDurationValue", "type": "label", "font": "small", "alignment": "center", - "color": "white", - "text": "", - "position": {"x": 319, "y": 545} + "color": "yellow", + "text": "core.genrltxt.521", + "position": {"x": 222, "y": 544} }, - // timer - //{ - // "type": "label", - // "font": "small", - // "alignment": "center", - // "color": "yellow", - // "text": "core.genrltxt.521", - // "position": {"x": 222, "y": 544} - //}, - { "name": "labelTurnDurationValue", "type": "label", @@ -129,8 +104,7 @@ "itemsTotal": 11, "selected": 11, "style": "blue", - "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, - //"scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, + "scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, "panningStep": 20 }, ], @@ -139,17 +113,17 @@ { "timerPresets" : [ - [0, 60, 0, 0], - [0, 120, 0, 0], - [0, 240, 0, 0], - [0, 360, 0, 0], - [0, 480, 0, 0], - [0, 600, 0, 0], - [0, 900, 0, 0], - [0, 1200, 0, 0], - [0, 1500, 0, 0], - [0, 1800, 0, 0], - [0, 0, 0, 0], + [0, 60, 0, 0, false, false], + [0, 120, 0, 0, false, false], + [0, 240, 0, 0, false, false], + [0, 360, 0, 0, false, false], + [0, 480, 0, 0, false, false], + [0, 600, 0, 0, false, false], + [0, 900, 0, 0, false, false], + [0, 1200, 0, 0, false, false], + [0, 1500, 0, 0, false, false], + [0, 1800, 0, 0, false, false], + [0, 0, 0, 0, false, false], ] } } diff --git a/config/widgets/buttons/campaignBonusSelection.json b/config/widgets/buttons/campaignBonusSelection.json new file mode 100644 index 000000000..325609785 --- /dev/null +++ b/config/widgets/buttons/campaignBonusSelection.json @@ -0,0 +1,22 @@ +{ + "normal" : { + "width": 58, + "height": 64, + "items" : [] + }, + "pressed" : { + "width": 58, + "height": 64, + "items" : [] + }, + "blocked" : { + "width": 58, + "height": 64, + "items" : [] + }, + "highlighted" : { + "width": 58, + "height": 64, + "items" : [] + }, +} diff --git a/config/widgets/buttons/castleInterfaceQuickAccess.json b/config/widgets/buttons/castleInterfaceQuickAccess.json new file mode 100644 index 000000000..d5d5f2c5c --- /dev/null +++ b/config/widgets/buttons/castleInterfaceQuickAccess.json @@ -0,0 +1,22 @@ +{ + "normal" : { + "width": 38, + "height": 38, + "items" : [] + }, + "pressed" : { + "width": 38, + "height": 38, + "items" : [] + }, + "blocked" : { + "width": 38, + "height": 38, + "items" : [] + }, + "highlighted" : { + "width": 38, + "height": 38, + "items" : [] + }, +} diff --git a/config/widgets/buttons/heroBackpack.json b/config/widgets/buttons/heroBackpack.json new file mode 100644 index 000000000..60b622da2 --- /dev/null +++ b/config/widgets/buttons/heroBackpack.json @@ -0,0 +1,114 @@ +{ + "normal" : { + "width": 52, + "height": 36, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 52, "h": 36} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 52, "h": 36}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "pressed" : { + "width": 52, + "height": 36, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 1, "y": 1, "w": 51, "h": 35} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 52, "h": 36}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] }, + + { "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] }, + + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] }, + ] + } + ] + }, + "blocked" : { + "width": 52, + "height": 36, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 52, "h": 36} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 52, "h": 36}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "highlighted" : { + "width": 52, + "height": 36, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 52, "h": 36} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 52, "h": 36}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + ] + } + ] + }, +} diff --git a/config/widgets/buttons/heroCommander.json b/config/widgets/buttons/heroCommander.json new file mode 100644 index 000000000..76c2a5257 --- /dev/null +++ b/config/widgets/buttons/heroCommander.json @@ -0,0 +1,114 @@ +{ + "normal" : { + "width": 92, + "height": 38, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 92, "h": 38} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 92, "h": 38}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "pressed" : { + "width": 92, + "height": 38, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 1, "y": 1, "w": 91, "h": 37} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 92, "h": 38}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] }, + + { "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] }, + + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] }, + ] + } + ] + }, + "blocked" : { + "width": 92, + "height": 38, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 92, "h": 38} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 92, "h": 38}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "highlighted" : { + "width": 92, + "height": 38, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 92, "h": 38} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 92, "h": 38}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + ] + } + ] + }, +} diff --git a/config/widgets/buttons/selectionTabSortDate.json b/config/widgets/buttons/selectionTabSortDate.json new file mode 100644 index 000000000..b5b3965c3 --- /dev/null +++ b/config/widgets/buttons/selectionTabSortDate.json @@ -0,0 +1,22 @@ +{ + "normal" : { + "width": 18, + "height": 31, + "items" : [] + }, + "pressed" : { + "width": 18, + "height": 31, + "items" : [] + }, + "blocked" : { + "width": 18, + "height": 31, + "items" : [] + }, + "highlighted" : { + "width": 18, + "height": 31, + "items" : [] + }, +} diff --git a/config/widgets/buttons/settingsWindow/button190.json b/config/widgets/buttons/settingsWindow/button190.json new file mode 100644 index 000000000..341d75811 --- /dev/null +++ b/config/widgets/buttons/settingsWindow/button190.json @@ -0,0 +1,114 @@ +{ + "normal" : { + "width": 190, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 190, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 190, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "pressed" : { + "width": 190, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 1, "y": 1, "w": 189, "h": 31} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 190, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] }, + + { "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] }, + + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] }, + ] + } + ] + }, + "blocked" : { + "width": 190, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 190, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 190, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "highlighted" : { + "width": 190, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 190, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 190, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + ] + } + ] + }, +} diff --git a/config/widgets/buttons/settingsWindow/button32.json b/config/widgets/buttons/settingsWindow/button32.json new file mode 100644 index 000000000..88458bd91 --- /dev/null +++ b/config/widgets/buttons/settingsWindow/button32.json @@ -0,0 +1,114 @@ +{ + "normal" : { + "width": 32, + "height": 24, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 32, "h": 24} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 32, "h": 24}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "pressed" : { + "width": 32, + "height": 24, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 1, "y": 1, "w": 31, "h": 23} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 32, "h": 24}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] }, + + { "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] }, + + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] }, + ] + } + ] + }, + "blocked" : { + "width": 32, + "height": 24, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 32, "h": 24} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 32, "h": 24}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "highlighted" : { + "width": 32, + "height": 24, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 32, "h": 24} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 32, "h": 24}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + ] + } + ] + }, +} diff --git a/config/widgets/buttons/settingsWindow/button46.json b/config/widgets/buttons/settingsWindow/button46.json new file mode 100644 index 000000000..a18b6e736 --- /dev/null +++ b/config/widgets/buttons/settingsWindow/button46.json @@ -0,0 +1,114 @@ +{ + "normal" : { + "width": 46, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 46, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 46, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "pressed" : { + "width": 46, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 1, "y": 1, "w": 45, "h": 31} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 46, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] }, + + { "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] }, + + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] }, + ] + } + ] + }, + "blocked" : { + "width": 46, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 46, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 46, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "highlighted" : { + "width": 46, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 46, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 46, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + ] + } + ] + }, +} diff --git a/config/widgets/buttons/settingsWindow/button80.json b/config/widgets/buttons/settingsWindow/button80.json new file mode 100644 index 000000000..17c26605a --- /dev/null +++ b/config/widgets/buttons/settingsWindow/button80.json @@ -0,0 +1,114 @@ +{ + "normal" : { + "width": 80, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 80, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 80, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "pressed" : { + "width": 80, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 1, "y": 1, "w": 79, "h": 31} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 80, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] }, + + { "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] }, + + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] }, + ] + } + ] + }, + "blocked" : { + "width": 80, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 80, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 80, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] }, + + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] }, + ] + } + ] + }, + "highlighted" : { + "width": 80, + "height": 32, + "items" : [ + { + "type": "texture", + "image": "DiBoxBck", + "rect": {"x": 0, "y": 0, "w": 80, "h": 32} + }, + { + "type": "graphicalPrimitive", + "rect": {"x": 0, "y": 0, "w": 80, "h": 32}, + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] }, + + { "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + { "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 255, 255, 255, 255 ] }, + + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] }, + ] + } + ] + }, +} diff --git a/config/widgets/commonPrimitives.json b/config/widgets/commonPrimitives.json new file mode 100644 index 000000000..aab498284 --- /dev/null +++ b/config/widgets/commonPrimitives.json @@ -0,0 +1,61 @@ +{ + "boxWithNoBackground" : { + "type": "graphicalPrimitive", + "primitives" : [ + // Top line + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 32 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 1}, "b" : { "x" : -1, "y" : 1}, "color" : [ 0, 0, 0, 48 ] }, + + // Left line + { "type" : "line", "a" : { "x" : 0, "y" : 1}, "b" : { "x" : 0, "y" : -2}, "color" : [ 255, 255, 255, 32 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 2}, "b" : { "x" : 1, "y" : -2}, "color" : [ 0, 0, 0, 48 ] }, + + // Right line + { "type" : "line", "a" : { "x" : -2, "y" : 2}, "b" : { "x" : -2, "y" : -3}, "color" : [ 255, 255, 255, 24 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 1}, "b" : { "x" : -1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + + // Bottom line + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -1, "y" : -2}, "color" : [ 255, 255, 255, 24 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 255, 255, 255, 48 ] }, + ] + }, + + "horizontalLine" : { + "type": "graphicalPrimitive", + "primitives" : [ + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 1}, "b" : { "x" : -1, "y" : 1}, "color" : [ 0, 0, 0, 64 ] } + ] + }, + + "verticalLine" : { + "type": "graphicalPrimitive", + "primitives" : [ + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 0}, "b" : { "x" : 1, "y" : -1}, "color" : [ 0, 0, 0, 64 ] } + ] + }, + + "boxWithBackground" : { + "type": "graphicalPrimitive", + "primitives" : [ + { "type" : "filledBox", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 75 ] }, + + // Top line + { "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 32 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : 1}, "b" : { "x" : -1, "y" : 1}, "color" : [ 0, 0, 0, 48 ] }, + + // Left line + { "type" : "line", "a" : { "x" : 0, "y" : 1}, "b" : { "x" : 0, "y" : -2}, "color" : [ 255, 255, 255, 32 ] }, + { "type" : "line", "a" : { "x" : 1, "y" : 2}, "b" : { "x" : 1, "y" : -2}, "color" : [ 0, 0, 0, 48 ] }, + + // Right line + { "type" : "line", "a" : { "x" : -2, "y" : 2}, "b" : { "x" : -2, "y" : -3}, "color" : [ 255, 255, 255, 24 ] }, + { "type" : "line", "a" : { "x" : -1, "y" : 1}, "b" : { "x" : -1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] }, + + // Bottom line + { "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -1, "y" : -2}, "color" : [ 255, 255, 255, 24 ] }, + { "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 255, 255, 255, 48 ] } + ] + }, +} diff --git a/config/widgets/extraOptionsTab.json b/config/widgets/extraOptionsTab.json new file mode 100644 index 000000000..f0b939192 --- /dev/null +++ b/config/widgets/extraOptionsTab.json @@ -0,0 +1,110 @@ +{ + "library" : "config/widgets/settings/library.json", + + "items": + [ + { + "name": "background", + "type": "picture", + "image": "ADVOPTBK", + "position": {"x": 0, "y": 6} + }, + { + "name": "textureCampaignOverdraw", + "type": "texture", + "color" : "blue", + "image": "DIBOXBCK", + "rect": {"x": 391, "y": 14, "w": 82, "h": 569} + }, + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "vcmi.optionsTab.extraOptions.hover", + "position": {"x": 222, "y": 36} + }, + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.optionsTab.extraOptions.help", + "rect": {"x": 60, "y": 48, "w": 320, "h": 0}, + "adoptHeight": true + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 54, "y": 127, "w": 335, "h": 2}, + "color": [24, 41, 90, 255] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 159, "y": 90, "w": 2, "h": 38}, + "color": [24, 41, 90, 255] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 235, "y": 90, "w": 2, "h": 38}, + "color": [24, 41, 90, 255] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 311, "y": 90, "w": 2, "h": 38}, + "color": [24, 41, 90, 255] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 55, "y": 556, "w": 335, "h": 19}, + "color": [24, 41, 90, 255] + }, + { + "name": "ExtraOptionsButtons", + "type" : "verticalLayout", + "customType" : "toggleButton", + "position": {"x": 70, "y": 100}, + "items": + [ + { + "name": "buttonCheatAllowed", + "image": "lobby/checkbox", + "callback" : "setCheatAllowed", + "help" : "vcmi.optionsTab.cheatAllowed", + "selected" : true + }, + { + "name": "buttonUnlimitedReplay", + "image": "lobby/checkbox", + "callback" : "setUnlimitedReplay", + "help" : "vcmi.optionsTab.unlimitedReplay", + "selected" : true + } + ] + }, + { + "name": "ExtraOptionsLabels", + "type" : "verticalLayout", + "customType" : "label", + "position": {"x": 110, "y": 103}, + "items": + [ + { + "name": "labelCheatAllowed", + "font": "small", + "alignment": "left", + "color": "yellow", + "text": "vcmi.optionsTab.cheatAllowed.hover" + }, + { + "name": "labelUnlimitedReplay", + "font": "small", + "alignment": "left", + "color": "yellow", + "text": "vcmi.optionsTab.unlimitedReplay.hover" + } + ] + } + ] +} diff --git a/config/widgets/lobbyWindow.json b/config/widgets/lobbyWindow.json new file mode 100644 index 000000000..e8bdc8889 --- /dev/null +++ b/config/widgets/lobbyWindow.json @@ -0,0 +1,199 @@ +{ + "customTypes" : { + "labelTitleMain" : { + "type": "label", + "font": "big", + "alignment": "left", + "color": "yellow" + }, + "labelTitle" : { + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow" + }, + "backgroundTexture" : { + "type": "texture", + "font": "tiny", + "color" : "blue", + "image": "DIBOXBCK" + }, + "areaFilled":{ + "type": "transparentFilledRectangle", + "color": [0, 0, 0, 75], + "colorLine": [64, 80, 128, 255] + } + }, + + + "width": 1024, + "height": 600, + + "items": + [ + { + "type": "backgroundTexture", + "rect": {"w": 1024, "h": 600} + }, + + { + "type": "areaFilled", + "rect": {"x": 5, "y": 5, "w": 250, "h": 40} + }, + { + "name" : "accountNameLabel", + "type": "labelTitleMain", + "position": {"x": 15, "y": 10} + }, + + { + "type": "areaFilled", + "rect": {"x": 5, "y": 50, "w": 250, "h": 500} + }, + { + "type": "labelTitle", + "position": {"x": 15, "y": 53}, + "text" : "Room List" + }, + { + "type" : "roomList", + "name" : "roomList", + "position" : { "x" : 7, "y" : 69 }, + "itemOffset" : { "x" : 0, "y" : 40 }, + "sliderPosition" : { "x" : 230, "y" : 0 }, + "sliderSize" : { "x" : 450, "y" : 450 } + }, + + { + "type": "areaFilled", + "rect": {"x": 270, "y": 50, "w": 150, "h": 540} + }, + { + "type": "labelTitle", + "position": {"x": 280, "y": 53}, + "text" : "Channel List" + }, + + { + "type": "areaFilled", + "rect": {"x": 430, "y": 50, "w": 430, "h": 515} + }, + { + "type": "labelTitle", + "position": {"x": 440, "y": 53}, + "text" : "Game Chat" + }, + { + "type": "textBox", + "name": "gameChat", + "font": "small", + "alignment": "left", + "color": "white", + "rect": {"x": 440, "y": 70, "w": 430, "h": 495} + }, + + { + "type": "areaFilled", + "rect": {"x": 430, "y": 565, "w": 395, "h": 25} + }, + { + "name" : "messageInput", + "type": "textInput", + "alignment" : "left", + "rect": {"x": 440, "y": 568, "w": 375, "h": 20} + }, + + { + "type": "areaFilled", + "rect": {"x": 870, "y": 50, "w": 150, "h": 540} + }, + { + "type": "labelTitle", + "position": {"x": 880, "y": 53}, + "text" : "Account List" + }, + { + "type" : "accountList", + "name" : "accountList", + "position" : { "x" : 872, "y" : 69 }, + "itemOffset" : { "x" : 0, "y" : 40 }, + "sliderPosition" : { "x" : 130, "y" : 0 }, + "sliderSize" : { "x" : 520, "y" : 520 } + }, + + { + "type": "button", + "position": {"x": 840, "y": 10}, + "image": "settingsWindow/button80", + "help": "core.help.288", + "callback": "closeWindow", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "Leave" + } + ] + }, + + { + "type": "button", + "position": {"x": 940, "y": 10}, + "image": "settingsWindow/button80", + "help": "core.help.288", + "callback": "closeWindow", + "hotkey": "globalCancel", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "Close" + } + ] + }, + + { + "type": "button", + "position": {"x": 828, "y": 565}, + "image": "settingsWindow/button32", + "help": "core.help.288", + "callback": "sendMessage", + "hotkey": "globalAccept", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": ">" + } + ] + }, + + { + "type": "button", + "position": {"x": 10, "y": 555}, + "image": "settingsWindow/button190", + "help": "core.help.288", + "callback": "createGameRoom", + "items": + [ + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "Create Room" + } + ] + }, + + ] +} diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index d451afda4..9aff4aab7 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -1,4 +1,6 @@ { + "library" : "config/widgets/commonPrimitives.json", + "items": [ { @@ -8,10 +10,8 @@ "rect": {"w": 428, "h": 379} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 5, "y": 5, "w": 418, "h": 20}, - "color": [0, 0, 0, 75], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 5, "y": 5, "w": 418, "h": 20} }, { "type": "label", @@ -22,10 +22,8 @@ "position": {"x": 214, "y": 15} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 5, "y": 30, "w": 418, "h": 20}, - "color": [0, 0, 0, 75], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 5, "y": 30, "w": 418, "h": 20} }, { "type": "label", @@ -37,10 +35,8 @@ "position": {"x": 214, "y": 40} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 5, "y": 55, "w": 418, "h": 20}, - "color": [0, 0, 0, 75], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 5, "y": 55, "w": 418, "h": 20} }, { "type": "label", @@ -51,10 +47,8 @@ "position": {"x": 214, "y": 65} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 29, "y": 79, "w": 171, "h": 171}, - "color": [0, 0, 0, 255], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 29, "y": 79, "w": 171, "h": 171} }, { "type": "label", @@ -70,10 +64,8 @@ "rect": {"x": 30, "y": 80, "w": 169, "h": 169} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 228, "y": 79, "w": 171, "h": 171}, - "color": [0, 0, 0, 255], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 228, "y": 79, "w": 171, "h": 171} }, { "type": "label", @@ -90,10 +82,8 @@ "rect": {"x": 229, "y": 80, "w": 169, "h": 169} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 5, "y": 254, "w": 418, "h": 20}, - "color": [0, 0, 0, 75], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 5, "y": 254, "w": 418, "h": 20} }, { "type": "label", @@ -104,10 +94,8 @@ "position": {"x": 214, "y": 264} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 5, "y": 279, "w": 418, "h": 20}, - "color": [0, 0, 0, 75], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 5, "y": 279, "w": 418, "h": 20} }, { "type": "label", @@ -119,10 +107,8 @@ "position": {"x": 214, "y": 289} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 5, "y": 304, "w": 418, "h": 20}, - "color": [0, 0, 0, 75], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 5, "y": 304, "w": 418, "h": 20} }, { "type": "label", @@ -133,10 +119,8 @@ "position": {"x": 214, "y": 314} }, { - "type": "transparentFilledRectangle", - "rect": {"x": 5, "y": 329, "w": 418, "h": 45}, - "color": [0, 0, 0, 75], - "colorLine": [128, 100, 75, 255] + "type": "boxWithBackground", + "rect": {"x": 5, "y": 329, "w": 418, "h": 45} }, { "type": "textBox", diff --git a/config/widgets/playerOptionsTab.json b/config/widgets/playerOptionsTab.json new file mode 100644 index 000000000..65a36caa6 --- /dev/null +++ b/config/widgets/playerOptionsTab.json @@ -0,0 +1,121 @@ +{ + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "name": "background", + "type": "picture", + "image": "ADVOPTBK", + "position": {"x": 0, "y": 6} + }, + + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.515", + "position": {"x": 222, "y": 36} + }, + + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "core.genrltxt.516", + "rect": {"x": 60, "y": 50, "w": 320, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelPlayerNameAndHandicap", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.517", + "rect": {"x": 58, "y": 92, "w": 100, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingTown", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.518", + "rect": {"x": 163, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingHero", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.519", + "rect": {"x": 239, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingBonus", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.520", + "rect": {"x": 315, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "type" : "dropDownTimers", + "name": "timerPresetSelector", + "position": {"x": 56, "y": 535}, + "dropDownPosition": {"x": 0, "y": -260} + }, + { + "type" : "dropDownSimturns", + "name": "simturnsPresetSelector", + "position": {"x": 56, "y": 555}, + "dropDownPosition": {"x": 0, "y": -160} + } + ], + + "variables": + { + "timerPresets" : + [ + [ 0, 0, 0, 0, false, false], + [ 0, 60, 0, 0, false, false], + [ 0, 120, 0, 0, false, false], + [ 0, 300, 0, 0, false, false], + [ 0, 600, 0, 0, false, false], + [ 0, 1200, 0, 0, false, false], + [ 0, 1800, 0, 0, false, false], + [ 960, 480, 120, 0, true, false], + [ 960, 480, 75, 0, true, false], + [ 480, 240, 60, 0, true, false], + [ 120, 90, 60, 0, true, false], + [ 120, 60, 15, 0, true, false], + [ 60, 60, 0, 0, true, false] + ], + "simturnsPresets" : + [ + [ 0, 0, false], + [ 999, 0, false], + [ 7, 0, false], + [ 14, 0, false], + [ 28, 0, false], + [ 7, 7, false], + [ 14, 14, false], + [ 28, 28, false] + ] + } +} diff --git a/config/widgets/settings/adventureOptionsTab.json b/config/widgets/settings/adventureOptionsTab.json index cee89b6eb..57886e5e2 100644 --- a/config/widgets/settings/adventureOptionsTab.json +++ b/config/widgets/settings/adventureOptionsTab.json @@ -1,12 +1,14 @@ { - "library" : "config/widgets/settings/library.json", + "library" : [ + "config/widgets/settings/library.json", + "config/widgets/commonPrimitives.json", + ], "items": [ { "name": "lineLabelsEnd", - "type": "texture", - "image": "settingsWindow/lineHorizontal", + "type": "horizontalLine", "rect": { "x" : 5, "y" : 229, "w": 365, "h": 3} }, /////////////////////////////////////// Left section - Hero Speed and Map Scrolling @@ -279,7 +281,7 @@ "callback": "mapScrollSpeedChanged" }, -/////////////////////////////////////// Right section - Original H3 options +/////////////////////////////////////// Right section - Original H3 options + some custom { "type" : "verticalLayout", "customType" : "labelDescription", @@ -294,6 +296,12 @@ }, { "text": "core.genrltxt.574" // quick combat + }, + { + "text": "vcmi.adventureOptions.showGrid.hover" + }, + { + "text": "vcmi.adventureOptions.hideBackground.hover" } ] }, @@ -305,7 +313,7 @@ [ { "name": "showMovePathPlaceholder", - "type": "checkboxFake", + "type": "checkboxFake" }, { "name": "heroReminderCheckbox", @@ -317,6 +325,16 @@ "help": "core.help.362", "callback": "quickCombatChanged" }, + { + "name": "showGridCheckbox", + "help": "vcmi.adventureOptions.showGrid", + "callback": "showGridChanged" + }, + { + "name": "hideBackgroundCheckbox", + "help": "vcmi.adventureOptions.hideBackground", + "callback": "hideBackgroundChanged" + } ] }, /////////////////////////////////////// Bottom section - VCMI Options @@ -333,7 +351,7 @@ "text": "vcmi.adventureOptions.forceMovementInfo.hover" }, { - "text": "vcmi.adventureOptions.showGrid.hover" + "text": "vcmi.adventureOptions.skipAdventureMapAnimations.hover" }, { "text": "vcmi.adventureOptions.infoBarPick.hover" @@ -347,6 +365,9 @@ { "text": "vcmi.adventureOptions.leftButtonDrag.hover", "created" : "desktop" + }, + { + "text": "vcmi.adventureOptions.smoothDragging.hover" } ] }, @@ -367,9 +388,9 @@ "callback": "forceMovementInfoChanged" }, { - "name": "showGridCheckbox", - "help": "vcmi.adventureOptions.showGrid", - "callback": "showGridChanged" + "name": "skipAdventureMapAnimationsCheckbox", + "help": "vcmi.adventureOptions.skipAdventureMapAnimations", + "callback": "skipAdventureMapAnimationsChanged" }, { "name": "infoBarPickCheckbox", @@ -391,6 +412,11 @@ "help": "vcmi.adventureOptions.leftButtonDrag", "callback": "leftButtonDragChanged", "created" : "desktop" + }, + { + "name": "smoothDraggingCheckbox", + "help": "vcmi.adventureOptions.smoothDragging", + "callback": "smoothDraggingChanged" } ] } diff --git a/config/widgets/settings/battleOptionsTab.json b/config/widgets/settings/battleOptionsTab.json index 8f47a3fd5..08480233f 100644 --- a/config/widgets/settings/battleOptionsTab.json +++ b/config/widgets/settings/battleOptionsTab.json @@ -1,18 +1,19 @@ { - "library" : "config/widgets/settings/library.json", + "library" : [ + "config/widgets/settings/library.json", + "config/widgets/commonPrimitives.json", + ], "items": [ { "name": "lineCreatureInfo", - "type": "texture", - "image": "settingsWindow/lineHorizontal", + "type": "horizontalLine", "rect": { "x" : 5, "y" : 289, "w": 365, "h": 3} }, { "name": "lineAnimationSpeed", - "type": "texture", - "image": "settingsWindow/lineHorizontal", + "type": "horizontalLine", "rect": { "x" : 5, "y" : 349, "w": 365, "h": 3} }, { @@ -47,6 +48,9 @@ }, { "text": "core.genrltxt.401" // First Aid Tent + }, + { + "text": "vcmi.battleOptions.endWithAutocombat.hover" } ] }, @@ -69,7 +73,6 @@ [ { "name": "enableAutocombatSpellsCheckbox", - "help": "vcmi.battleOptions.enableAutocombatSpells", "callback": "enableAutocombatSpellsChanged" } ] @@ -87,6 +90,20 @@ {} ] }, + + { + "type" : "verticalLayout", + "customType" : "checkbox", + "position": {"x": 380, "y": 233}, + "items": + [ + { + "help": "vcmi.battleOptions.endWithAutocombat", + "name": "endWithAutocombatCheckbox", + "callback": "endWithAutocombatChanged" + } + ] + }, /////////////////////////////////////// Left section - checkboxes { "name": "creatureInfoLabels", diff --git a/config/widgets/settings/generalOptionsTab.json b/config/widgets/settings/generalOptionsTab.json index e34131c6a..03b744272 100644 --- a/config/widgets/settings/generalOptionsTab.json +++ b/config/widgets/settings/generalOptionsTab.json @@ -1,12 +1,14 @@ { - "library" : "config/widgets/settings/library.json", + "library" : [ + "config/widgets/settings/library.json", + "config/widgets/commonPrimitives.json", + ], "items": [ { "name": "lineLabelsEnd", - "type": "texture", - "image": "settingsWindow/lineHorizontal", + "type": "horizontalLine", "rect": { "x" : 5, "y" : 349, "w": 365, "h": 3} }, { @@ -50,6 +52,9 @@ { "text": "vcmi.systemOptions.framerateButton.hover" }, + { + "text": "vcmi.systemOptions.enableLargeSpellbookButton.hover" + }, { "text": "core.genrltxt.577" }, @@ -67,6 +72,35 @@ } ] }, + { + "type" : "verticalLayout", + "customType" : "checkboxFake", + "position" : {"x": 10, "y": 83}, + "items" : [ + { + "created" : "desktop" + }, + {}, + { + "created" : "desktop" + }, + { + "created" : "desktop" + }, + {}, + {}, + { + "name": "spellbookAnimationCheckboxPlaceholder" + }, + { + "created" : "touchscreen" + }, + { + "created" : "mobile" + }, + {} + ] + }, { "type" : "verticalLayout", "customType" : "checkbox", @@ -102,6 +136,11 @@ "help": "vcmi.systemOptions.framerateButton", "callback": "framerateChanged" }, + { + "name": "enableLargeSpellbookCheckbox", + "help": "vcmi.systemOptions.enableLargeSpellbookButton", + "callback": "enableLargeSpellbookChanged" + }, { "name": "spellbookAnimationCheckbox", "help": "core.help.364", @@ -172,6 +211,28 @@ "type": "labelCentered", "position": {"x": 565, "y": 158} }, + { + "type" : "verticalLayout", + "customType" : "labelDescription", + "position" : {"x": 415, "y": 202}, + "items" : [ + { + "text": "vcmi.systemOptions.audioMuteFocus.hover" + } + ] + }, + { + "type" : "verticalLayout", + "customType" : "checkbox", + "position" : {"x": 380, "y": 200}, + "items" : [ + { + "name": "audioMuteFocusCheckbox", + "help": "vcmi.systemOptions.audioMuteFocus", + "callback": "audioMuteFocusChanged" + } + ] + }, /////////////////////////////////////// Bottom section - Towns Settings { "type" : "verticalLayout", diff --git a/config/widgets/settings/library.json b/config/widgets/settings/library.json index c5ea07f95..21096db72 100644 --- a/config/widgets/settings/library.json +++ b/config/widgets/settings/library.json @@ -35,8 +35,9 @@ ] }, "checkboxFake" : { - "type": "picture", - "image": "settingsWindow/checkBoxEmpty" + "type": "boxWithBackground", + "rect": { "x" : 0, "y" : 0, "w": 32, "h": 24} + }, "audioSlider" : { "type": "slider", diff --git a/config/widgets/settings/settingsMainContainer.json b/config/widgets/settings/settingsMainContainer.json index 0e495d73f..48665a5ab 100644 --- a/config/widgets/settings/settingsMainContainer.json +++ b/config/widgets/settings/settingsMainContainer.json @@ -1,4 +1,8 @@ { + "library" : [ + "config/widgets/commonPrimitives.json" + ], + "items": [ { @@ -9,14 +13,12 @@ }, { "name": "lineTabs", - "type": "texture", - "image": "settingsWindow/lineHorizontal", + "type": "horizontalLine", "rect": { "x" : 10, "y" : 45, "w": 580, "h": 3} }, { "name": "lineColumns", - "type": "texture", - "image": "settingsWindow/lineVertical", + "type": "verticalLine", "rect": { "x" : 370, "y" : 50, "w": 3, "h": 420} }, @@ -91,8 +93,7 @@ { "name": "lineButtons", - "type": "texture", - "image": "settingsWindow/lineHorizontal", + "type": "horizontalLine", "rect": { "x" : 375, "y" : 289, "w": 220, "h": 3} }, { diff --git a/config/widgets/turnOptionsDropdownLibrary.json b/config/widgets/turnOptionsDropdownLibrary.json new file mode 100644 index 000000000..162fbd775 --- /dev/null +++ b/config/widgets/turnOptionsDropdownLibrary.json @@ -0,0 +1,520 @@ +{ + "dropDownBackground" : + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 1, "y": 1, "w": 219, "h": 19}, + "color": [0, 0, 0, 128], + "colorLine": [0, 0, 0, 128] + }, + "dropDownLabel": + { + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "position": {"x": 4, "y": 0} + }, + "dropDownHover": + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 2, "y": 2, "w": 216, "h": 16}, + "color": [0, 0, 0, 0], + "colorLine": [255, 255, 0, 255] + }, + "dropDownTimers" : + { + "name": "timerPresetSelector", + "type": "comboBox", + "image": "lobby/dropdown", + "imageOrder": [0, 0, 0, 0], + "items": + [ + { + "name": "timer", + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.turnTime.select" + } + ], + "dropDown": + { + "items": + [ + { + "type": "texture", + "image": "DiBoxBck", + "color" : "blue", + "rect": {"x": 0, "y": 0, "w": 220, "h": 260} + }, + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 0, "y": 0, "w": 220, "h": 260}, + "color": [0, 0, 0, 0], + "colorLine": [64, 80, 128, 128] + }, + { + "type": "item", + "position": {"x": 0, "y": 0}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.unlimited" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 20}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 40}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 60}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.5" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 80}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.10" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 100}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.20" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 120}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.30" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 140}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.20" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 160}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.16" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 180}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.8" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 200}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.4" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 220}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 240}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + } + ] + } + }, + "dropDownSimturns" : + { + "name": "timerPresetSelector", + "type": "comboBox", + "image": "lobby/dropdown", + "imageOrder": [0, 0, 0, 0], + "items": + [ + { + "name": "timer", + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturns.select" + } + ], + "dropDown": + { + "items": + [ + { + "type": "texture", + "image": "DiBoxBck", + "color" : "blue", + "rect": {"x": 0, "y": 0, "w": 220, "h": 160} + }, + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 0, "y": 0, "w": 220, "h": 160}, + "color": [0, 0, 0, 0], + "colorLine": [64, 80, 128, 128] + }, + { + "type": "item", + "position": {"x": 0, "y": 0}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.none" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 20}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContactMax" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 40}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContact1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 60}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContact2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 80}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContact4" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 100}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.blocked1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 120}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.blocked2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 140}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.blocked4" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + } + ] + } + } +} diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json new file mode 100644 index 000000000..0ba76719a --- /dev/null +++ b/config/widgets/turnOptionsTab.json @@ -0,0 +1,343 @@ +{ + "library" : [ + "config/widgets/turnOptionsDropdownLibrary.json", + "config/widgets/commonPrimitives.json", + ], + + "customTypes" : { + "verticalLayout66" : { + "type" : "layout", + "vertical" : true, + "dynamic" : false, + "distance" : 66 + }, + "labelTitle" : { + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow" + }, + "labelDescription" : { + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 0, "y": 0, "w": 300, "h": 35}, + "adoptHeight": true + }, + "timeInput" : { + "type": "textInput", + "alignment": "center", + "text": "00:00", + "rect": {"x": 0, "y": 0, "w": 86, "h": 23}, + "offset": {"x": 0, "y": 0} + }, + "timeInputBackground" : { + "type": "boxWithBackground", + "rect": {"x": 0, "y": 0, "w": 86, "h": 23} + } + }, + + "items": + [ + { + "name": "background", + "type": "picture", + "image": "RANMAPBK", + "position": {"x": 0, "y": 6} + }, + + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "vcmi.optionsTab.turnOptions.hover", + "position": {"x": 222, "y": 36} + }, + + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.optionsTab.turnOptions.help", + "rect": {"x": 60, "y": 48, "w": 320, "h": 0}, + "adoptHeight": true + }, + + { + "type" : "dropDownTimers", + "name": "timerPresetSelector", + "position": {"x": 160, "y": 78}, + "dropDownPosition": {"x": 0, "y": 20} + }, + { + "type" : "dropDownSimturns", + "name": "simturnsPresetSelector", + "position": {"x": 160, "y": 98}, + "dropDownPosition": {"x": 0, "y": 20} + }, + + { + "type": "texture", + "image": "DiBoxBck", + "color" : "blue", + "rect": {"x" : 64, "y" : 394, "w": 316, "h": 124} + }, + + { + "type": "boxWithNoBackground", + "rect": {"x" : 64, "y" : 394, "w": 316, "h": 124} + }, + { + "type": "horizontalLine", + "rect": {"x" : 65, "y" : 416, "w": 314, "h": 2} + }, + { + "type": "horizontalLine", + "rect": {"x" : 65, "y" : 466, "w": 314, "h": 2} + }, + + { + "type" : "verticalLayout66", + "customType" : "labelTitle", + "position": {"x": 70, "y": 133}, + "items": + [ + { + "text": "vcmi.optionsTab.chessFieldBase.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldTurn.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldBattle.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldUnit.hover" + }, + { + "text": "vcmi.optionsTab.simturnsTitle" + } + ] + }, + + { + "type" : "verticalLayout66", + "customType" : "labelDescription", + "position": {"x": 70, "y": 155}, + "items": + [ + { + "text": "vcmi.optionsTab.chessFieldBase.help" + }, + { + "name": "chessFieldTurnLabel" + }, + { + "text": "vcmi.optionsTab.chessFieldBattle.help" + }, + { + "name": "chessFieldUnitLabel" + } + ] + }, + + { + "name": "buttonTurnTimerAccumulate", + "position": {"x": 160, "y": 195}, + "type": "toggleButton", + "image": "lobby/checkbox", + "callback" : "setTurnTimerAccumulate" + }, + { + "name": "buttonUnitTimerAccumulate", + "position": {"x": 160, "y": 327}, + "type": "toggleButton", + "image": "lobby/checkbox", + "callback" : "setUnitTimerAccumulate" + }, + + { + "type" : "labelTitle", + "position": {"x": 195, "y": 199}, + "text" : "vcmi.optionsTab.accumulate" + }, + { + "type" : "labelTitle", + "position": {"x": 195, "y": 331}, + "text" : "vcmi.optionsTab.accumulate" + }, + + { + "type" : "verticalLayout66", + "customType" : "timeInputBackground", + "position": {"x": 294, "y": 129}, + "items": + [ + {}, + {}, + {}, + {} + ] + }, + + { + "type" : "verticalLayout66", + "customType" : "timeInput", + "position": {"x": 294, "y": 129}, + "items": + [ + { + "name": "chessFieldBase", + "callback": "parseAndSetTimer_base", + "help": "vcmi.optionsTab.chessFieldBase.help" + }, + { + "name": "chessFieldTurn", + "callback": "parseAndSetTimer_turn", + "help": "vcmi.optionsTab.chessFieldTurnAccumulate.help" + }, + { + "name": "chessFieldBattle", + "callback": "parseAndSetTimer_battle", + "help": "vcmi.optionsTab.chessFieldBattle.help" + }, + { + "name": "chessFieldUnit", + "callback": "parseAndSetTimer_unit", + "help": "vcmi.optionsTab.chessFieldUnitAccumulate.help" + } + ] + }, + + { + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturnsMin.hover", + "position": {"x": 70, "y": 420} + }, + { + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturnsMax.hover", + "position": {"x": 70, "y": 470} + }, + + { + "name": "simturnsDurationMin", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 178, "y": 420}, + "size": 200, + "callback": "setSimturnDurationMin", + "items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168 ], + "selected": 0, + "style": "blue", + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + "panningStep": 20 + }, + { + "name": "simturnsDurationMax", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 178, "y": 470}, + "size": 200, + "callback": "setSimturnDurationMax", + "items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168, 1000000 ], + "selected": 0, + "style": "blue", + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + "panningStep": 20 + }, + { + "name": "labelSimturnsDurationValueMin", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 278, "y": 428} + }, + { + "name": "labelSimturnsDurationValueMax", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 278, "y": 478} + }, + { + "text": "vcmi.optionsTab.simturnsMin.help", + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 70, "y": 430, "w": 300, "h": 40} + }, + { + "text": "vcmi.optionsTab.simturnsMax.help", + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 70, "y": 480, "w": 300, "h": 40} + }, + { + "name": "buttonSimturnsAI", + "position": {"x": 70, "y": 535}, + "help" : "vcmi.optionsTab.simturnsAI", + "type": "toggleButton", + "image": "lobby/checkbox", + "callback" : "setSimturnAI" + }, + { + "name": "labelSimturnsAI", + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow", + "text": "vcmi.optionsTab.simturnsAI.hover", + "position": {"x": 110, "y": 538} + } + ], + + "variables": + { + "timerPresets" : + [ + [ 0, 0, 0, 0, false, false], + [ 0, 60, 0, 0, false, false], + [ 0, 120, 0, 0, false, false], + [ 0, 300, 0, 0, false, false], + [ 0, 600, 0, 0, false, false], + [ 0, 1200, 0, 0, false, false], + [ 0, 1800, 0, 0, false, false], + [ 1200, 600, 120, 0, true, false], + [ 960, 480, 90, 0, true, false], + [ 480, 240, 60, 0, true, false], + [ 240, 120, 30, 0, true, false], + [ 120, 60, 15, 0, true, false], + [ 60, 60, 0, 0, true, false] + ], + "simturnsPresets" : + [ + [ 0, 0, false], + [ 999, 0, false], + [ 7, 0, false], + [ 14, 0, false], + [ 28, 0, false], + [ 7, 7, false], + [ 14, 14, false], + [ 28, 28, false] + ] + } +} diff --git a/config/widgets/turnTimer.json b/config/widgets/turnTimer.json deleted file mode 100644 index 5965ed2f0..000000000 --- a/config/widgets/turnTimer.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "items": - [ - { //backgound color - "type": "drawRect", - "rect": {"x": 4, "y": 4, "w": 72, "h": 24}, - "color": [10, 10, 10, 255] - }, - - { //clocks icon - "type": "image", - "image": "VCMI/BATTLEQUEUE/STATESSMALL", - "frame": 1, - "position": {"x": 4, "y": 6} - }, - - { //timer field label - "name": "timer", - "type": "label", - "font": "big", - "alignment": "left", - "color": "yellow", - "text": "", - "position": {"x": 26, "y": 2} - }, - ], - - "variables": - { - "notificationTime": [0, 1, 2, 3, 4, 5, 20], - "notificationSound": "WE5", - "textColorFromPlayerColor": true - } -} diff --git a/debian/changelog b/debian/changelog index 7c5b0b41f..4ba89eca5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,44 @@ +vcmi (1.5.0) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 1 Mar 2024 12:00:00 +0200 + +vcmi (1.4.5) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Tue, 23 Jan 2024 12:00:00 +0200 + +vcmi (1.4.4) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Sat, 20 Jan 2024 12:00:00 +0200 + +vcmi (1.4.3) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 19 Jan 2024 12:00:00 +0200 + +vcmi (1.4.2) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Mon, 25 Dec 2023 16:00:00 +0200 + +vcmi (1.4.1) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Tue, 12 Dec 2023 16:00:00 +0200 + vcmi (1.4.0) jammy; urgency=medium * New upstream release - -- Ivan Savenko Fri, 22 Dec 2023 16:00:00 +0200 + -- Ivan Savenko Fri, 8 Dec 2023 16:00:00 +0200 vcmi (1.3.2) jammy; urgency=medium diff --git a/docs/Readme.md b/docs/Readme.md index 3c630514b..76dc21a97 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,12 +1,21 @@ -[![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.2) +[![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.0) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.1) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.2) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.5/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.5) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) # VCMI Project -VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities. +VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities. + +

+Vanilla town siege in extended window +Vanilla town view with radial menu for touchscreen devices +Large Spellbook with German translation +New widget for Hero selection, featuring Pavillon Town +

+ ## Links @@ -15,6 +24,7 @@ VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it ne * Bugtracker: https://github.com/vcmi/vcmi/issues * Slack: https://slack.vcmi.eu/ * Discord: https://discord.gg/chBT42V + * GPT Store: https://chat.openai.com/g/g-1kNhX0mlO-vcmi-assistant ## Latest release @@ -29,9 +39,15 @@ Please see corresponding installation guide articles for details for your platfo - [Android](players/Installation_Android.md) - [iOS](players/Installation_iOS.md) +

+Forge Town in battle +Asylum town with new creature dialog +Ruins town siege + Map editor +

+ ## Documentation and guidelines for players -- [General information about VCMI Project](players/Manual.md) - [Frequently asked questions](https://vcmi.eu/faq/) (external link) - [Game mechanics](players/Game_Mechanics.md) - [Bug reporting guidelines](players/Bug_Reporting_Guidelines.md) diff --git a/docs/developers/Building_Android.md b/docs/developers/Building_Android.md index 6f04b71a5..fee0024a4 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -62,7 +62,7 @@ Building for Android is a 2-step process. First, native C++ code is compiled to This is a traditional CMake project, you can build it from command line or some IDE. You're not required to pass any custom options (except Conan toolchain file), defaults are already good. If you wish to use your own CMake presets, inherit them from our `build-with-conan` preset. Example: ``` -cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug -D CMAKE_COMPILER_LAUNCHER=ccache --toolchain ... +cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug -D ENABLE_CCACHE:BOOL=ON --toolchain ... cmake --build ../build ``` diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index a1371532c..559527f1b 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -49,12 +49,14 @@ Information about building packages from the Arch User Repository (AUR) can be f # Getting the sources -VCMI is still in development. We recommend the following initial directory structure: +We recommend the following directory structure: . ├── vcmi -> contains sources and is under git control └── build -> contains build output, makefiles, object files,... +Out-of-source builds keep the local repository clean so one doesn't have to manually exclude files generated during the build from commits. + You can get latest sources with: `git clone -b develop --recursive https://github.com/vcmi/vcmi.git` @@ -65,25 +67,25 @@ You can get latest sources with: ```sh mkdir build && cd build -cmake ../vcmi +cmake -S ../vcmi ``` # Additional options that you may want to use: ## To enable debugging: -`cmake ../vcmi -D CMAKE_BUILD_TYPE=Debug` +`cmake -S ../vcmi -D CMAKE_BUILD_TYPE=Debug` **Notice**: The ../vcmi/ is not a typo, it will place makefile scripts into the build dir as the build dir is your working dir when calling CMake. ## To use ccache: -`cmake ../vcmi -D CMAKE_COMPILER_LAUNCHER=ccache` +`cmake -S ../vcmi -D ENABLE_CCACHE:BOOL=ON` ## Trigger build `cmake --build . -- -j2` (-j2 = compile with 2 threads, you can specify any value) -That will generate vcmiclient, vcmiserver, vcmilauncher as well as .so libraries in **build/bin/** directory. +That will generate vcmiclient, vcmiserver, vcmilauncher as well as .so libraries in the **build/bin/** directory. # Package building diff --git a/docs/developers/Building_Windows.md b/docs/developers/Building_Windows.md index 1ba751662..44bdf65ce 100644 --- a/docs/developers/Building_Windows.md +++ b/docs/developers/Building_Windows.md @@ -111,6 +111,14 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi - Right click on `BUILD_ALL` project. This `BUILD_ALL` project should be in `CMakePredefinedTargets` tree in Solution Explorer. - VCMI will be built in `%VCMI_DIR%/build/bin` folder! +## Compile VCMI with MinGW via MSYS2 +- Install MSYS2 from https://www.msys2.org/ +- Start the `MSYS MinGW x64`-shell +- Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static` +- Generate and build solution from VCMI-root dir: `cmake --preset windows-mingw-release && cmake --build --preset windows-mingw-release` + +**NOTE:** This will link Qt5 statically to `VCMI_launcher.exe` and `VCMI_Mapeditor.exe`. See [PR #3421](https://github.com/vcmi/vcmi/pull/3421) for some background. + # Create VCMI installer (This step is not required for just building & development) Make sure NSIS is installed to default directory or have registry entry so CMake can find it. diff --git a/docs/modders/Bonus/Bonus_Duration_Types.md b/docs/modders/Bonus/Bonus_Duration_Types.md index 28056c6b1..1dd7359b7 100644 --- a/docs/modders/Bonus/Bonus_Duration_Types.md +++ b/docs/modders/Bonus/Bonus_Duration_Types.md @@ -13,4 +13,5 @@ Bonus may have any of these durations. They acts in disjunction. - UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack - UNTIL_ATTACK: removed after attack and counterattacks are performed - STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance -- COMMANDER_KILLED \ No newline at end of file +- COMMANDER_KILLED +- UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed \ No newline at end of file diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 027bf2ac0..0df82d18e 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -6,13 +6,13 @@ The bonuses were grouped according to their original purpose. The bonus system a ### NONE -Special bonus that gives no effect +Bonus placeholder that gives no effect ### MORALE Changes morale of affected units -- val: change in morale +- val: change in morale, eg. 1, -2 ### LUCK @@ -29,7 +29,7 @@ Changes mastery level of spells of affected heroes and units. Examples are magic ### DARKNESS -On each turn, hides area in fog of war around affected town for all players other than town owner +On each turn, hides area in fog of war around affected town for all players other than town owner. Currently does not work for any entities other than towns. - val: radius in tiles @@ -40,21 +40,21 @@ On each turn, hides area in fog of war around affected town for all players othe Increases amount of movement points available to affected hero on new turn - subtype: -- - heroMovementLand: only land movement will be affected -- - heroMovementSea: only sea movement will be affected + - heroMovementLand: only land movement will be affected + - heroMovementSea: only sea movement will be affected - val: number of movement points (100 points for a tile) ### WATER_WALKING Allows movement over water for affected heroes -- val: TODO +- val: Penalty to movement, in percent (Basic Water Walk - 40, Advanced Water Walk - 20) ### FLYING_MOVEMENT Allows flying movement for affected heroes -- val: TODO +- val: Penalty to movement, in percent ### NO_TERRAIN_PENALTY @@ -62,11 +62,11 @@ Eliminates terrain penalty on certain terrain types for affected heroes (Nomads Note: to eliminate all terrain penalties see ROUGH_TERRAIN_DISCOUNT bonus -- subtype: type of terrain +- subtype: type of terrain, eg `terrain.sand` ### TERRAIN_NATIVE -Affected units will view any terrain as native +Affected units will view any terrain as native. This means army containing these creature will have no movement penalty, and will be able to see Mines and Quick Sand in battle. ### PRIMARY_SKILL @@ -93,38 +93,40 @@ Restores entire mana pool for affected heroes on new turn ### NONEVIL_ALIGNMENT_MIX -Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale +Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale (Angelic Alliance effect) ### SURRENDER_DISCOUNT Changes surrender cost for affected heroes -- val: decrease in cost, in precentage +- val: decrease in cost, in percentage ### IMPROVED_NECROMANCY -TODO: Determine units which is raised by necromancy skill. +Allows to raise different creatures than Skeletons after battle. - subtype: creature raised - val: Necromancer power -- addInfo: limiter by Necromancer power -- Example (from Necromancy skill): +- addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert) +- Example (from Cloak Of The Undead King): -` "power" : {` -` "type" : "IMPROVED_NECROMANCY",` -` "subtype" : "creature.skeleton",` -` "addInfo" : 0` -` }` +```jsonc +{ + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.walkingDead", + "addInfo" : 1 +} +``` ### LEARN_BATTLE_SPELL_CHANCE -Determines chance for affected heroes to learn spell casted by enemy hero after battle +Determines chance for affected heroes to learn spell cast by enemy hero after battle - val: chance to learn spell, percentage ### LEARN_BATTLE_SPELL_LEVEL_LIMIT -Allows affected heroes to learn spell casted by enemy hero after battle +Allows affected heroes to learn spell cast by enemy hero after battle - val: maximal level of spell that can be learned @@ -207,7 +209,7 @@ Defines maximum level of spells than hero can learn from any source (Wisdom) ### SPECIAL_SPELL_LEV -Gives additional bonus to effect of specific spell based on level of creature it is casted on +Gives additional bonus to effect of specific spell based on level of creature it is cast on - subtype: identifier of affected spell - val: bonus to spell effect, percentage, divided by target creature level @@ -228,25 +230,43 @@ Gives additional bonus to effect of specific spell ### SPECIAL_PECULIAR_ENCHANT -TODO: blesses and curses with id = val dependent on unit's level +Gives creature under effect of this spell additional bonus, which is hardcoded and depends on the creature tier. -- subtype: affected spell identifier +- subtype: affected spell identifier, ie. `spell.haste` ### SPECIAL_ADD_VALUE_ENCHANT -TODO: specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add +Increased effect of spell affecting creature, ie. Aenain makes Disrupting Ray decrease target's defense by additional 2 points: + +```jsonc +"disruptingRay" : { + "addInfo" : -2, + "subtype" : "spell.disruptingRay", + "type" : "SPECIAL_ADD_VALUE_ENCHANT" +} +``` - subtype: affected spell identifier +- additionalInfo: value to add ### SPECIAL_FIXED_VALUE_ENCHANT -TODO: specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix. +Spell affecting creature has fixed effect, eg. hero Melody has constant spell effect of +3: + +```jsonc +"fortune" : { + "addInfo" : 3, + "subtype" : "spell.fortune", + "type" : "SPECIAL_FIXED_VALUE_ENCHANT" +} +``` - subtype: affected spell identifier +- additionalInfo = fixed value ### SPECIAL_UPGRADE -Allows creature upgrade for affected armies +Allows creature being upgraded to another creature (Gelu, Dracon) - subtype: identifier of creature that can being upgraded - addInfo: identifier of creature to which perform an upgrade @@ -255,7 +275,7 @@ Allows creature upgrade for affected armies ### SPELL_DURATION -Changes duration of timed spells casted by affected hero +Changes duration of timed spells cast by affected hero - val: additional duration, turns - subtype: optional, identifier of affected spells, or all if not set @@ -291,25 +311,25 @@ Affected heroes will add specified resources amounts to player treasure on new d Increases weekly growth of creatures in affected towns (Legion artifacts) - value: number of additional weekly creatures -- subtype: dwelling level, in form "creatureLevelX" where X is desired level (1-7) +- subtype: dwelling level, in form `creatureLevelX` where X is desired level (1-7) ### CREATURE_GROWTH_PERCENT -Increases weekly growth of creatures in affected towns +Increases weekly growth of creatures in affected towns (Statue of Legion) - val: additional growth, percentage ### BATTLE_NO_FLEEING -Heroes affected by this bonus can not retreat or surrender in battle +Heroes affected by this bonus can not retreat or surrender in battle (Shackles of War effect) ### NEGATE_ALL_NATURAL_IMMUNITIES Negates all natural immunities for affected stacks. (Orb of Vulnerability) - subtype: -- - immunityBattleWide: Entire battle will be affected by bonus -- - immunityEnemyHero: Only enemy hero will be affected by bonus + - immunityBattleWide: Entire battle will be affected by bonus + - immunityEnemyHero: Only enemy hero will be affected by bonus ### OPENING_BATTLE_SPELL @@ -320,7 +340,7 @@ In battle, army affected by this bonus will cast spell at the very start of the ### FREE_SHIP_BOARDING -Heroes affected by this bonus will keep all their movement points when embarking or disembarking ship +Heroes affected by this bonus will not lose all movement points when embarking or disembarking ship. Movement points are converted depending on max land and water movement range. ### WHIRLPOOL_PROTECTION @@ -345,9 +365,9 @@ Increases movement speed of units in battle Increases base damage of creature in battle - subtype: -- - creatureDamageMin: increases only minimal damage -- - creatureDamageMax: increases only maximal damage -- - creatureDamageBoth: increases both minimal and maximal damage + - creatureDamageMin: increases only minimal damage + - creatureDamageMax: increases only maximal damage + - creatureDamageBoth: increases both minimal and maximal damage - val: additional damage points ### SHOTS @@ -370,11 +390,11 @@ Affected unit is considered to be a gargoyle and not affected by certain spells ### UNDEAD -Affected unit is considered to be undead +Affected unit is considered to be undead, which makes it immune to many effects, and also reduce morale of allied living units. ### SIEGE_WEAPON -Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. +Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. All War Machines should have this bonus. ### DRAGON_NATURE @@ -405,8 +425,8 @@ Affected units can not receive good or bad morale Affected unit can fly on the battlefield - subtype: -- - movementFlying: creature will fly (slowly move across battlefield) -- - movementTeleporting: creature will instantly teleport to destination + - movementFlying: creature will fly (slowly move across battlefield) + - movementTeleporting: creature will instantly teleport to destination, skipping movement animation. ### SHOOTER @@ -414,21 +434,21 @@ Affected unit can shoot ### CHARGE_IMMUNITY -Affected unit is immune to JOUSTING ability +Affected unit is immune to JOUSTING ability of (ie. Champions). ### ADDITIONAL_ATTACK -Affected unit can perform additional attacks. Attacked unit will retaliate after each attack if can +Affected unit can perform additional attacks. Attacked unit will retaliate after each attack if able. - val: number of additional attacks to perform ### UNLIMITED_RETALIATIONS -Affected unit will always retaliate if able +Affected unit will always retaliate if able (Royal Griffin) ### ADDITIONAL_RETALIATION -Affected unit can retaliate multiple times per turn +Affected unit can retaliate multiple times per turn (basic Griffin) - value: number of additional retaliations @@ -442,7 +462,7 @@ Affected unit will deal more damage based on movement distance (Champions) Affected unit will deal more damage when attacking specific creature -- subtype - identifier of hated creature +- subtype - identifier of hated creature, ie. "creature.genie" - val - additional damage, percentage ### SPELL_LIKE_ATTACK @@ -470,9 +490,9 @@ Affected unit can return to his starting location after attack (Harpies) ### ENEMY_DEFENCE_REDUCTION -Affected unit will ignore specified percentage of attacked unit defence (Behemoth) +Affected unit will ignore specified percentage of attacked unit defense (Behemoth) -- val: amount of defence points to ignore, percentage +- val: amount of defense points to ignore, percentage ### GENERAL_DAMAGE_REDUCTION @@ -480,9 +500,9 @@ Affected units will receive reduced damage from attacks by other units - val: damage reduction, percentage - subtype: -- - damageTypeMelee: only melee damage will be reduced -- - damageTypeRanged: only ranged damage will be reduced -- - damageTypeAll: all damage will be reduced + - damageTypeMelee: only melee damage will be reduced + - damageTypeRanged: only ranged damage will be reduced + - damageTypeAll: all damage will be reduced ### PERCENTAGE_DAMAGE_BOOST @@ -490,8 +510,8 @@ Affected units will deal increased damage when attacking other units - val: damage increase, percentage - subtype: -- - damageTypeMelee: only melee damage will increased -- - damageTypeRanged: only ranged damage will increased + - damageTypeMelee: only melee damage will increased + - damageTypeRanged: only ranged damage will increased ### GENERAL_ATTACK_REDUCTION @@ -501,9 +521,9 @@ Affected units will deal reduced damage when attacking other units (Blind or Par ### DEFENSIVE_STANCE -Affected units will receive increased bonus to defence while defending +Affected units will receive increased bonus to defense while defending -- val: additional bonus to defence, in skill points +- val: additional bonus to defense, in skill points ### NO_DISTANCE_PENALTY @@ -519,7 +539,7 @@ Affected unit will deal full damage when shooting over walls in sieges. Does not ### FREE_SHOOTING -Affected unit can use ranged attack even when blocked by enemy unit. (Sharpshooter's Bow) +Affected unit can use ranged attack even when blocked by enemy unit, like with Bow of the Sharpshooter relic ### BLOCKS_RETALIATION @@ -531,8 +551,8 @@ Affected unit will gain new creatures for each enemy killed by this unit - val: number of units gained per enemy killed - subtype: -- - soulStealPermanent: creature will stay after the battle -- - soulStealBattle: creature will be lost after the battle + - soulStealPermanent: creature will stay after the battle + - soulStealBattle: creature will be lost after the battle ### TRANSMUTATION @@ -540,8 +560,8 @@ Affected units have chance to transform attacked unit to other creature type - val: chance for ability to trigger, percentage - subtype: -- - transmutationPerHealth: transformed unit will have same HP pool as original stack, -- - transmutationPerUnit: transformed unit will have same number of units as original stack + - transmutationPerHealth: transformed unit will have same HP pool as original stack, + - transmutationPerUnit: transformed unit will have same number of units as original stack - addInfo: creature to transform to. If not set, creature will transform to same unit as attacker ### SUMMON_GUARDIANS @@ -567,6 +587,11 @@ Affected unit will attack units on all hexes that surround attacked hex Affected unit will retaliate before enemy attacks, if able +- subtype: + - damageTypeMelee: only melee attacks affected + - damageTypeRanged: only ranged attacks affected. Note that unit also requires ability to retaliate in ranged, such as RANGED_RETALIATION bonus + - damageTypeAll: any attacks are affected + ### SHOOTS_ALL_ADJACENT Affected unit will attack units on all hexes that surround attacked hex in ranged attacks @@ -577,8 +602,8 @@ Affected unit will kills additional units after attack - val: chance to trigger, percentage - subtype: -- - destructionKillPercentage: kill percentage of units, -- - destructionKillAmount: kill amount + - destructionKillPercentage: kill percentage of units, + - destructionKillAmount: kill amount - addInfo: amount or percentage to kill ### LIMITED_SHOOTING_RANGE @@ -588,11 +613,30 @@ Affected unit can use ranged attacks only within specified range - val: max shooting range in hexes - addInfo: optional, range at which ranged penalty will trigger (default is 10) +### FEROCITY + +Affected unit will attack additional times if killed creatures in target unit during attacking (including ADDITIONAL_ATTACK bonus attacks) + +- val: amount of additional attacks (negative number will reduce number of unperformed attacks if any left) +- addInfo: optional, amount of creatures needed to kill (default is 1) + +### ENEMY_ATTACK_REDUCTION + +Affected unit will ignore specified percentage of attacked unit attack (Nix) + +- val: amount of attack points to ignore, percentage + +### REVENGE + +Affected unit will deal more damage based on percentage of self health lost compared to amount on start of battle +(formula: `square_root((total_unit_count + 1) * 1_creature_max_health / (current_whole_unit_health + 1_creature_max_health) - 1)`. +Result is then multiplied separately by min and max base damage of unit and result is additive bonus to total damage at end of calculation) + ## Special abilities ### CATAPULT -Affected unit can attack walls during siege battles +Affected unit can attack walls during siege battles (Cyclops) - subtype: spell that describes attack parameters @@ -629,7 +673,7 @@ All units adjacent to affected unit will receive additional spell resistance bon ### HP_REGENERATION -Affected unit will regenerate portion of its health points on its turn +Affected unit will regenerate portion of its health points on its turn. - val: amount of health points gained per round @@ -647,19 +691,19 @@ Affected unit will give his hero specified portion of mana points spent by enemy ### LIFE_DRAIN -Affected unit will heal itself, resurrecting any dead units, by amount of dealt damage +Affected unit will heal itself, resurrecting any dead units, by amount of dealt damage (Vampire Lord) - val: percentage of damage that will be converted into health points ### DOUBLE_DAMAGE_CHANCE -Affected unit has chance to deal double damage on attack +Affected unit has chance to deal double damage on attack (Death Knight) - val: chance to trigger, percentage ### FEAR -If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance. Blocked by FEARLESS bonus. +If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance (Azure Dragon). Blocked by FEARLESS bonus. ### HEALER @@ -688,14 +732,20 @@ Affected unit will deal additional damage after attack ### DEATH_STARE -Affected unit will kill additional units after attack +Affected unit will kill additional units after attack. Used for Death stare (Mighty Gorgon) ability and for Accurate Shot (Pirates, HotA) - subtype: -- - deathStareGorgon: random amount -- - deathStareCommander: fixed amount + - deathStareGorgon: only melee attack, random amount of killed units + - deathStareNoRangePenalty: only ranged attacks without obstacle (walls) or range penalty + - deathStareRangePenalty: only ranged attacks with range penalty + - deathStareObstaclePenalty: only ranged attacks with obstacle (walls) penalty + - deathStareRangeObstaclePenalty: only ranged attacks with both range and obstacle penalty + - deathStareCommander: fixed amount, both melee and ranged attacks - val: -- - for deathStareGorgon: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula -- - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val + - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val + - for all other subtypes: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once, rounded up +- addInfo: + - SpellID to be used as hit effect. If not set - 'deathStare' spell will be used. If set to "accurateShot" battle log messages will use alternative description ### SPECIAL_CRYSTAL_GENERATION @@ -737,53 +787,45 @@ Determines how many times per combat affected creature can cast its targeted spe ### SPELL_AFTER_ATTACK -TODO: - -- subtype - spell id -- value - chance % -- additional info - \[X, Y\] -- X - spell level -- Y = 0 - all attacks, 1 - shot only, 2 - melee only +- subtype - spell id, eg. spell.iceBolt +- value - chance (percent) +- additional info - \[X, Y, Z\] + - X - spell mastery level (1 - Basic, 3 - Expert) + - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. ### SPELL_BEFORE_ATTACK -TODO: - - subtype - spell id - value - chance % -- additional info - \[X, Y\] -- X - spell level -- Y = 0 - all attacks, 1 - shot only, 2 - melee only +- additional info - \[X, Y, Z\] + - X - spell mastery level (1 - Basic, 3 - Expert) + - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. -### SPECIFIC_SPELL_POWER +### SPECIFIC_SPELL_POWER -TODO: - -- value used for Thunderbolt and Resurrection casted by units, also for Healing secondary skill (for core:spell.firstAid used by First Aid tent) +- value: Used for Thunderbolt and Resurrection cast by units (multiplied by stack size). Also used for Healing secondary skill (for core:spell.firstAid used by First Aid tent) - subtype - spell id ### CREATURE_SPELL_POWER -TODO: - -- value per unit, divided by 100 (so faerie Dragons have 500) +- value: Spell Power of offensive spell cast unit, multiplied by 100. ie. Faerie Dragons have value fo 500, which gives them 5 Spell Power for each unit in the stack. ### CREATURE_ENCHANT_POWER -TODO: - -total duration of spells casted by creature + - val: Total duration of spells cast by creature, in turns ### REBIRTH Affected stack will resurrect after death -TODO: recheck math - -- val - percent of total stack HP restored +- val - percent of total stack HP restored, not rounded. For instance, when 4 Phoenixes with Rebirth chance of 20% die, there is 80% chance than one Phoenix will rise. - subtype: -- - rebirthRegular: (Phoenix) -- - rebirthSpecial: at least one unit will always resurrect (sacred Phoenix) + - rebirthRegular: Phoenix, as described above. + - rebirthSpecial: At least one unit will always rise (Sacred Phoenix) ### ENCHANTED @@ -798,9 +840,7 @@ Affected unit is permanently enchanted with a spell, that is cast again every tu Affected unit is immune to all spell with level below or equal to value of this bonus -- val: level up to which this unit is immune to - -TODO: additional info? +- val: level of spell up to which this unit is immune to ### MAGIC_RESISTANCE @@ -846,7 +886,7 @@ Affected unit can be affected by all friendly spells even it would be normally i ### POISON -TODO: describe +Unit affected by poison will lose 10% of max health every combat turn, up to `val`. After that, effect ends. - val - max health penalty from poison possible @@ -886,9 +926,9 @@ Affected unit can not be controlled by player and instead it will attempt to mov ### IN_FRENZY -Affected unit's defence is reduced to 0 and is transferred to attack with specified multiplier +Affected unit's defense is reduced to 0 and is transferred to attack with specified multiplier -- val: multiplier factor with which defence is transferred to attack (percentage) +- val: multiplier factor with which defense is transferred to attack (percentage) ### HYPNOTIZED @@ -934,9 +974,9 @@ Affected heroes will be under effect of Visions spell, revealing information of - val: multiplier to effect range. Information is revealed within (val \* hero spell power) range - subtype: -- - visionsMonsters: reveal information on monsters, -- - visionsHeroes: reveal information on heroes, -- - visionsTowns: reveal information on towns + - visionsMonsters: reveal information on monsters, + - visionsHeroes: reveal information on heroes, + - visionsTowns: reveal information on towns ### BLOCK_MAGIC_BELOW diff --git a/docs/modders/Difficulty.md b/docs/modders/Difficulty.md index 75c44d768..190df9aa8 100644 --- a/docs/modders/Difficulty.md +++ b/docs/modders/Difficulty.md @@ -4,7 +4,7 @@ Since VCMI 1.4.0 there are more capabilities to configure difficulty parameters. It means, that modders can give different bonuses to AI or human players depending on selected difficulty -Difficulty configuration is located in [config/difficulty.json](../config/difficulty.json) file and can be overriden by mods. +Difficulty configuration is located in [config/difficulty.json](../config/difficulty.json) file and can be overridden by mods. ## Format summary diff --git a/docs/modders/Entities_Format/Creature_Format.md b/docs/modders/Entities_Format/Creature_Format.md index 96d9ce1c1..5bff93d9c 100644 --- a/docs/modders/Entities_Format/Creature_Format.md +++ b/docs/modders/Entities_Format/Creature_Format.md @@ -59,8 +59,8 @@ In order to make functional creature you also need: // Basic growth of this creature in town or in external dwellings "growth" : 0, - // Bonus growth of this creature from built horde - "hordeGrowth" : 0, + // Bonus growth of this creature from built horde, if any + "horde" : 0, // Creature stats in battle "attack" : 0, diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index 350851acf..0fa81c50e 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -61,6 +61,10 @@ "positive": true, }, + // If true, then creature capable of casting this spell can cast this spell on itself + // If false, then creature can only cast this spell on other units + "canCastOnSelf" : false, + // If true, spell won't be available on a map without water "onlyOnWaterMap" : true, diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index 5a1df570d..1f7c00802 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -8,6 +8,8 @@ { //Optional name - useful to have several template variations with same name "name" : "Custom template name", + //Any info you want to be displayed in random map menu + "description" : "Detailed info and recommended rules", /// Minimal and maximal size of the map. Possible formats: /// Size code: s, m, l or xl for size with optional suffix "+u" for underground @@ -18,8 +20,8 @@ /// Number of players that will be present on map (human or AI) "players" : "2-4", - /// Optional, number of AI-only players - "cpu" : "2", + /// Since 1.4.0 - Optional, number of human-only players (as in original templates) + "humans" : "1-4", ///Optional parameter allowing to prohibit some water modes. All modes are allowed if parameter is not specified "allowedWaterContent" : ["none", "normal", "islands"] diff --git a/docs/modders/Translations.md b/docs/modders/Translations.md index 23b378797..1428ec28d 100644 --- a/docs/modders/Translations.md +++ b/docs/modders/Translations.md @@ -28,10 +28,12 @@ This is list of all languages that are currently supported by VCMI. If your lang VCMI allows translating game data into languages other than English. In order to translate Heroes III in your language easiest approach is to: -- Copy existing translation, such as English translation from here: https://github.com/vcmi-mods/h3-for-vcmi-englisation +- Copy existing translation, such as English translation from here: https://github.com/vcmi-mods/h3-for-vcmi-englisation (delete sound and video folders) - Rename mod to indicate your language, preferred form is "(language)-translation" - Update mod.json to match your mod - Translate all texts strings from game.json, campaigns.json and maps.json +- Replace images in data and sprites with translated ones (or delete it if you don't want to translate them) +- If unicode characters needed for language: Create a submod with a free font like here: https://github.com/vcmi-mods/vietnamese-translation/tree/vcmi-1.4/vietnamese-translation/mods/VietnameseTrueTypeFonts If you have already existing Heroes III translation you can: @@ -48,6 +50,7 @@ VCMI contains several new strings, to cover functionality not existing in Heroes - In-game texts, most noticeably - in-game settings menu. - Game Launcher - Map Editor +- Linux specific - Android Launcher Before you start, make sure that you have copy of VCMI source code. If you are not familiar with git, you can use Github Desktop to clone VCMI repository. @@ -75,10 +78,25 @@ Translation of Map Editor is identical, except for location of translation files TODO: how to test translation locally -### Translation of Android Launcher +### Translation of Linux specific files +#### Translation of AppStream metainfo -TODO -see https://github.com/vcmi/vcmi/blob/develop/android/vcmi-app/src/main/res/values/strings.xml +The [AppStream](https://freedesktop.org/software/appstream/docs/chap-Metadata.html) [metainfo file](https://github.com/vcmi/vcmi/blob/develop/launcher/eu.vcmi.VCMI.metainfo.xml) is used for Linux software centers. + +It can be translated using a text editor or using [jdAppStreamEdit](https://flathub.org/apps/page.codeberg.JakobDev.jdAppStreamEdit): +- Install jdAppStreamEdit +- Open `/launcher/eu.vcmi.VCMI.metainfo.xml` +- Translate and save the file + +#### Desktop file +- Edit `/launcher/vcmilauncher.desktop` and `/launcher/vcmieditor.desktop` +- Add `GenericName[xyz]` and `Comment[xyz]` with your language code and translation + +### Translation of Android Launcher +- Copy `/android/vcmi-app/src/main/res/values/strings.xml` to `/android/vcmi-app/src/main/res/values-xyz/strings.xml` (`xyz` is your language code) +- Translate this file + +See also here: https://developer.android.com/guide/topics/resources/localization ### Submitting changes @@ -136,4 +154,4 @@ There *may* be a way to do the same via QtCreator UI or via CMake, if you find o Generally, this should be as simple as overwriting old files. Things that may be necessary if translation update is not visible in executable: - Rebuild subproject (map editor/launcher). -- Regenerate translations via `lupdate -no-obsolete * -ts translation/*.ts` \ No newline at end of file +- Regenerate translations via `lupdate -no-obsolete * -ts translation/*.ts` diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index e92f674b6..c7322b930 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -36,7 +36,7 @@ Gives specific creature in every slot, with optional amount. Examples: ### Movement points -`nwcnebuchadnezzar` or `vcminahar` or `vcmimove` - give 1000000 movement points and free ship boarding for 1 day +`nwcnebuchadnezzar` or `vcminahar` or `vcmimove` - give unlimited (or specified amount of) movement points and free ship boarding Alternative usage: `vcmimove ` - gives specified amount of movement points ### Resources @@ -57,10 +57,22 @@ Alternative usage: `vcmilevel ` - advances hero by specified number of l - `vcmiolorin` or `vcmiexp` - gives selected hero 10000 experience Alternative usage: `vcmiexp ` - gives selected hero specified amount of experience +### Luck and morale + +`nwcfollowthewhiterabbit` or `vcmiluck` - the currently selected hero permanently gains maximum luck +`nwcmorpheus` or `vcmimorale` - the currently selected hero permanently gains maximum morale + +### Puzzle map + +`nwcoracle` or `vcmiobelisk` - reveals the puzzle map + ### Finishing the game -`nwcredpill` or `vcmisilmaril` or `vcmiwin` - player wins -`nwcbluepill` or `vcmimelkor` or `vcmilose` - player loses +`nwcredpill` or `vcmisilmaril` or `vcmiwin` - player wins +`nwcbluepill` or `vcmimelkor` or `vcmilose` - player loses + +### Misc +`nwctheone` or `vcmigod` - reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight ## Using cheat codes on other players By default, all cheat codes apply to current player. Alternatively, it is possible to specify player that you want to target: diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index cb16df499..80bbf0838 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -2,23 +2,58 @@ # List of features added in VCMI +## High resolutions + +VCMI supports resolutions higher than original game, which ran only in 800 x 600. It also allows a number of additional features: + +- High-resolution screens of any ascpect ratio are supported. +- In-game GUI can be freely scaled +- Adventure map can be freely zoomed + +Assets from Heroes of Might & Magic III HD - Remake released by Ubisoft in 2015 - are **not** supported. + +## Extended engine limits + Some of game features have already been extended in comparison to Shadow of Death: - Support for 32-bit graphics with alpha channel. Supported formats are .def, .bmp, .png and .tga - Support for maps of any size, including rectangular shapes - No limit of number of map objects, such as dwellings and stat boosters - Hero experience capacity currently at 2^64, which equals 199 levels with typical progression -- Heroes can have primary stats up to 2^16 -- Unlimited backpack -- Support for Stack Experience +- Heroes can have primary stats up to 2^16. +- Unlimited backpack (by default). This can be toggled off to restore original 64-slot backpack limit. The list of implemented cheat codes and console commands is [here](Cheat_codes.md). +# New mechanics (Optional) + +## Stack Experience module + +VCMI natively suppoorts stack experience feature known from WoG. Any creature - old or modded - can get stack experience bonuses. However, this feature needs to be enabled as a part of WoG VCMI submod. + +Stack experience interface has been merged with regular creature window. Among old functionalities, it includes new useful info: + +- Click experience icon to see detailed info about creature rank and experience needed for next level. This window works only if stack experience module is enabled (true by default). +- Abilities description contain information about actual values and types of bonuses received by creature - be it default ability, stack experience, artifact or other effect. These descriptions use custom text files which have not been translated. +- [Stack Artifact](#stack-artifact-module). You can choose enabled artifact with arrow buttons. There is also additional button below to pass currently selected artifact back to hero. + +## Commanders module + +VCMI offers native support for Commanders. Commanders are part of WoG mod for VCMI and require it to be enabled. However, once this is done, any new faction can use its own Commander, too. + +## Mithril module + +VCMI natively supports Mithril resource known from WoG. However, it is not currently used by any mod. + +## Stack Artifact module + +In original WoG, there is one available Stack Artifact - Warlord's Banner, which is related directly to stack experience. VCMI natively supports any number of Stack Artifacts regardless if of Stack Experience module is enabled or not. However, currently no mods make use of this feature and it hasn't been tested for many years. + # List of bugs fixed in VCMI These bugs were present in original Shadow of Death game, however the team decided to fix them to bring back desired behaviour: -# List of game mechanics changes +## List of game mechanics changes Some of H3 mechanics can't be straight considered as bug, but default VCMI behaviour is different: @@ -27,12 +62,48 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav - Battles. Spells from artifacts like AOTD are autocasted on beginning of the battle, not beginning of turn. - Spells. Dimension Door spell doesn't allow to teleport to unexplored tiles. -# List of extended game functionality +# List of extended GUI features ## Adventure map + +### New Shortcuts + +- [LCtrl] + [R] - Quick restart of current scenario. +- [LCtrl] + Arrows - scrolls Adventure Map behind an open window. +- [LCtrl] pressed blocks Adventure Map scrolling (it allows us to leave the application window without losing current focus). +- NumPad 5 - centers view on selected hero. +- NumPad Enter functions same as normal Enter in the game (it didn't in H3). - [LCtrl] + LClick – perform a normal click (same as no hero is selected). This make it possible to select other hero instead of changing path of current hero. -## Quick Army Management +## Pathfinder + +VCMI introduces improved pathfinder, which may find the way on adventure map using ships,Subterranean Gates and Monoliths. Simply click your destination anywhere on adventure map and it will find shortest path, if if target position is reachable. + +### Quest log + +VCMI itroduces custom Quest Log window. It can display info about Seer Hut or Quest Guard mission, but also handle Borderguard and Border Gate missions. When you choose a quest from the list on the left, it's description is shown. Additionally, on inner minimap you can see small icons indicating locations of quest object. Clicking these objects immediately centers adventure map on desired location. + +### Power rating + +When hovering cursor over neutral stack on adventure map, you may notice additional info about relative threat this stack poses to curently selected hero. This feature has been originally introduced in Heroes of Might and Magic V. + +### Minor GUI features + +Some windows and dialogs now display extra info and images to make game more accessible for new players. This can be turned off, if desired. + +## Battles + +### Stack Queue + +Stack queue is a feature coming straight from HoMM5, which allows you to see order of stacks on the battlefield, sorted from left to right. To toggle in on/off, press [Q] during the battle. There is smaller and bigger version of it, the second one is available only in higher resolutions. + +### Attack range + +In combat, some creatures, such as Dragon or Cerberi, may attack enemies on multiple hexes. All such attacked stacks will be highlighted if the attack cursor is hovered over correct destination tile. Whenever battle stack is hovered, its movement range is highlighted in darker shade. This can help when you try to avoid attacks of melee units. + +## Town Screen + +### Quick Army Management - [LShift] + LClick – splits a half units from the selected stack into an empty slot. - [LCtrl] + LClick – splits a single unit from the selected stack into an empty slot. @@ -40,8 +111,9 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav - [Alt] + LClick – merge all splitted single units into one stack - [Alt] + [LCtrl] + LClick - move all units of selected stack to the city's garrison or to the met hero - [Alt] + [LShift] + LClick - dismiss selected stack` +- Directly type numbers in the Split Stack window to split them in any way you wish -## Interface Shourtcuts +### Interface Shortcuts It's now possible to open Tavern (click on town icon), Townhall, Quick Recruitment and Marketplace (click on gold) from various places: @@ -49,15 +121,30 @@ It's now possible to open Tavern (click on town icon), Townhall, Quick Recruitme - Kingdom overview for each town - Infobox (only if info box army management is enabled) -## Quick Recruitment +### Quick Recruitment Mouse click on castle icon in the town screen open quick recruitment window, where we can purhase in fast way units. -## Color support +## Pregame - Scenario / Saved Game list + +- Mouse wheel - scroll through the Scenario list. +- [Home] - move to the top of the list. +- [End] - move to the bottom of the list. +- NumPad keys can be used in the Save Game screen (they didn't work in H3). + +## Fullscreen + +- [F4] - Toggle fullscreen mode on/off. + +## FPS counter + +It's the new feature meant for testing game performance on various platforms. + +## Color support in game text Additional color are supported for text fields (e.g. map description). Uses HTML color syntax (e.g. #abcdef) / HTML predefined colors (e.g. green). -##### Original Heroes III Support +### Original Heroes III Support `This is white` @@ -67,7 +154,7 @@ Additional color are supported for text fields (e.g. map description). Uses HTML This is yellow -##### New +### New `{#ff0000|This is red}` @@ -77,6 +164,41 @@ Additional color are supported for text fields (e.g. map description). Uses HTML This is green +# Multiplayer + +Opening new Turn Option menu in scenario selection dialog allows detailed configuration of turn timers and simultaneous turns + +## Turn Timers + +TODO + +## Simultaneous turns + +Simultaneous turns allow multiple players to act at the same time, speeding up early game phase in multiplayer games. During this phase if different players (allies or not) attempt to interact with each other, such as capture objects owned by other players (mines, dwellings, towns) or attack their heroes, game will block such actions. Interaction with same map objects at the same time, such as attacking same wandering monster is also blocked. + +Following options can be used to configure simultaneous turns: +- Minimal duration (at least for): this is duration during which simultaneous turns will run unconditionally. Until specified number of days have passed, simultaneous turns will never break and game will not attempt to detect contacts. +- Maximal duration (at most for): this is duration after which simultaneous turns will end unconditionally, even if players still have not contacted each other. However if contact detection discovers contact between two players, simultaneous turns between them might end before specified duration. +- Simultaneous turns for AI: If this option is on, AI can act at the same time as human players. Note that AI shares settings for simultaneous turns with human players - if no simultaneous turns have been set up this option has no effect. + +### Contact detection + +While simultaneous turns are active, VCMI tracks contacts for each pair of player separately. + +Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses rules similar to standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as pickable treasures that block path between heroes. Any existing wandering monsters that block path between heroes are ignored for range calculation. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc. + +Once detected, contact can never be "lost". If game detected contact between two players, this contact will remain active till the end of the game, even if their heroes move far enough from each other. + +Game performs contact detection once per turn, at the very start of each in-game day. Once contact detection has been performed, players that are not in contact with each other can start making turn. For example, in game with 4 players: red, blue, brown and green. If game detected contact between red and blue following will happen: +- red, brown and green will all instantly start turn +- once red ends his turn, blue will be able to start his own turn (even if brown or green are still making turn) + +Once maximal duration of simultaneous turns (as specified during scenario setup) has been reached, or if all players are in contact with each other, game will return to standard turn order: red, blue, brown, green... + +### Differences compared to HD Mod version + +- In VCMI, players can see actions of other players immediately (provided that they have revealed fog of war) instead of waiting for next turn +- In VCMI, attempt to attack hero of another player during simultaneous turns will be blocked instead of reloading save from start of turn like in HD Mod # Manuals and guides diff --git a/docs/players/Installation_iOS.md b/docs/players/Installation_iOS.md index 46d88c99c..a57eef464 100644 --- a/docs/players/Installation_iOS.md +++ b/docs/players/Installation_iOS.md @@ -16,6 +16,10 @@ have the following options: - [Create signing assets on macOS from terminal](https://github.com/kambala-decapitator/xcode-auto-signing-assets). In the command replace `your.bundle.id` with something like `com.MY-NAME.vcmi`. After that use the above signer tool. - [Sign from any OS](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing). You'd still need to find a way to create signing assets (private key and provisioning profile) though. +To install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop: + + /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa + ## Step 2: Installing Heroes III data files Note: if you don't need in-game videos, you can omit downloading/copying file VIDEO.VID from data folder - it will save your time and space. The same applies to the Mp3 directory. diff --git a/docs/players/Manual.md b/docs/players/Manual.md deleted file mode 100644 index fb7c3d682..000000000 --- a/docs/players/Manual.md +++ /dev/null @@ -1,168 +0,0 @@ -< [Documentation](../Readme.md) / Manual - -# Introduction - -The purpose of VCMI project is to rewrite entire HoMM3: WoG engine from scratch, giving it new and extended possibilities. We are hoping to support mods and new towns already made by fans, but abandoned because of game code limitations. -VCMI is a fan-made open-source project in progress. We already allow support for maps of any sizes, higher resolutions and extended engine limits. However, although working, the game is not finished. There are still many features and functionalities to add, both old and brand new. - -# Installation - -VCMI requires Heroes of Might & Magic 3 complete installation and will not run properly without their files. - -## Windows - -To install VCMI, simply unzip downloaded archive to main HoMM3 directory. To launch it, click `VCMI_client` icon. Server mode is inactive yet. - -## Linux - -Visit [Installation on Linux](Installation_Linux.md) for Linux packages and installation guidelines. - -# New features - -A number of enchancements had been introduced thorough new versions of VCMI. In this section you can learn about all of them. - -## High resolutions - -VCMI supports resolutions higher than original 800x600. -Switching resolution may not only change visible area of map, but also alters some interface features such as [Stack Queue.](#stack-queue) -To change resolution or full screen mode use System Options menu when in game. Fullscreen mode can be toggled anytime using F4 hotkey. - -## Stack Experience - -In 0.85, new stack experience interface has been merged with regular creature window. Among old functionalities, it includes new useful info: - -- Click experience icon to see detailed info about creature rank and experience needed for next level. This window works only if stack experience module is enabled (true by default). -- Stack Artifact. As yet creature artifacts are not handled, so this place is unused. You can choose enabled artifact with arrow buttons. There is also additional button below to pass currently selected artifact back to hero. -- Abilities description contain information about actual values and types of bonuses received by creature - be it default ability, stack experience, artifact or other effect. These descriptions use custom text files which have not been translated. - -## Commanders - -VCMI offers native support for Commanders. By default, they resemble original WoG behaviour with basic "Commanders" script enabled. - -## Stack Queue - -Stack queue is a feature coming straight from HoMM5, which allows you to see order of stacks on the battlefield, sorted from left to right. To toggle in on/off, press 'Q' during the battle. There is smaller and bigger version of it, the second one is available only in higher resolutions. - -## Pathfinder - -VCMI introduces improved pathfinder, which may find the way on adventure map using ships and subterranean gates. Simply click your destination on another island or level and the proposed path will be displayed. - -## Quest log - -In 0.9 new quest log was introduced. It can display info about Seer Hut or Quest Guard mission, but also handle Borderguard and Border Gate missions. When you choose a quest from the list on the left, it's description is shown. Additionally, on inner minimap you can see small icons indicating locations of quest object. Clicking these objects immediately centers adventure map on desired location. - -## Attack range - -In combat, some creatures, such as Dragon or Cerberi, may attack enemies on multiple hexes. All such attacked stacks will be highlighted if the attack cursor is hovered over correct destination tile. Whenever battle stack is hovered, its movement range is highlighted in darker shade. This can help when you try to avoid attacks of melee units. - -## Power rating - -When hovering cursor over neutral stack on adventure map, you may notice additional info about relative threat this stack poses to selected hero. This feature has been introduced in Heroes of Might and Magic V and is planned to be extended to all kinds of armed objects. - -## FPS counter - -VCMI 0.85 introduces new feature for testing, the FPS counter. - -## Minor improvements - -## New controls - -VCMI introduces several minor improvements and new keybinds in user -interface. - -### Pregame - Scenario / Saved Game list - -- Mouse wheel - scroll through the Scenario list. -- Home - move to the top of the list. -- End - move to the bottom of the list. -- NumPad keys can be used in the Save Game screen (they didn't work in H3). - -### Adventure Map - -- CTRL + R - Quick restart of current scenario. -- CTRL + Arrows - scrolls Adventure Map behind an open window. -- CTRL pressed blocks Adventure Map scrolling (it allows us to leave the application window without losing current focus). -- NumPad 5 - centers view on selected hero. -- NumPad Enter functions same as normal Enter in the game (it didn't in H3). - -### Miscellaneous - -- Numbers for components in selection window - for example Treasure Chest, skill choice dialog and more yet to come. -- Type numbers in the Split Stack screen (for example 25 will split the stacks as such that there are 25 creatures in the second stack). -- 'Q' - Toggles the [Stack Queue](#stack-queue) display (so it can be enabled/disabled with single key press). -- During Tactics phase, click on any of your stack to instantly activate it. No need to scroll trough entire army anymore. - -## Cheat codes - -Following cheat codes have been implemented in VCMI. Type them in console: - -- `vcmiistari` - Gives all spells and 999 mana to currently selected hero -- `vcmiainur` - Gives 5 Archangels to every empty slot of currently selected hero -- `vcmiangband` - Gives 10 Black Knights into each slot -- `vcmiarmenelos` - Build all structures in currently selected town -- `vcminoldor` - All war machines -- `vcminahar` - 1000000 movement points -- `vcmiformenos` - Give resources (100 wood, ore and rare resources and 20000 gold) -- `vcmieagles` - Reveals fog of war -- `vcmiglorfindel` - Advances currently selected hero to the next level -- `vcmisilmaril` - Player wins -- `vcmimelkor` - Player loses -- `vcmiforgeofnoldorking` - Hero gets all artifacts except spell book, spell scrolls and war machines - -# Feedback - -Our project is open and its sources are available for everyone to browse and download. We do our best to inform community of Heroes fans with all the details and development progress. We also look forward to your comments, support and advice.\ -A good place to start is [VCMI Documentation](../Readme.md) which contains all necessary information for developers, testers and the people who would like to get familiar with our project. If you want to report a bug, use [Mantis Bugtracker](http://bugs.vcmi.eu/bug_report_advanced_page.php). -Make sure the issue is not already mentioned on [the list](http://bugs.vcmi.eu/view_all_bug_page.php) unless you can provide additional details for it. Please do not report as bugs features not yet implemented. For proposing new ideas and requests, visit [our board](http://forum.vcmi.eu/index.php). -VCMI comes with its own bug handlers: the console which prints game log `(server_log, VCMI_Client_log, VCMI_Server_log)` and memory dump file (`.dmp`) created on crash on Windows systems. These may be very helpful when the nature of bug is not obvious, please attach them if necessary. -To resolve an issue, we must be able to reproduce it on our computers. Please put down all circumstances in which a bug occurred and what did you do before, especially if it happens rarely or is not clearly visible. The better report, the better chance to track the bug quickly. - -# FAQ - -### What does "VCMI" stand for? - -VCMI is an acronym of the [Quenya](https://en.wikipedia.org/wiki/Quenya) phrase "Vinyar Callor Meletya Ingole", meaning "New Heroes of Might and Magic". ([Source](https://forum.vcmi.eu/t/what-vcmi-stands-for/297/4)) - -## How to turn off creature queue panel in battles? - -Hotkey to switch this panel is "Q" - -### Is it possible to add town X to vcmi? - -This depends on town authors or anyone else willing to port it to vcmi. Aim of VCMI is to provide *support* for such features. - -## When will the final version be released? - -When it is finished, which is another year at least. Exact date is impossible to predict. Development tempo depends mostly on free time of active programmers and community members, there is no exact shedule. You may expect new version every three months. Of course, joining the project will speed things up. - -## Are you going to add / change X? - -VCMI recreates basic H3:SoD engine and does not add new content or modify original mechanics by default. Only engine and interface improvements are likely to be supported now. If you want something specific to be done, please present detailed project on [our board](http://forum.vcmi.eu/index.php). Of course you are free to contribute with anything you can do. - -## Will it be possible to do Y? - -Removing engine restrictions and allowing flexible modding of game is the main aim of the project. As yet modification of game is not supported. - -## The game is not working, it crashes and I get strange console messages. - -Report your bug. Details are described [here](#Feedback). The sooner you tell the team about the problem, the sooner it will be resolved. Many problems come just from improper installation or system settings. - -## What is the current status of the project? - -Check [Documentation](../Readme.md), [release notes](http://forum.vcmi.eu/viewforum.php?f=1) or [changelog](https://github.com/vcmi/vcmi/blob/develop/ChangeLog). The best place to watch current changes as they are committed to the develop branch is the [Github commits page](https://github.com/vcmi/vcmi/commits/develop). The game is quite playable by now, although many important features are missing. - -## I have a great idea! - -Share it on [VCMI forum](http://forum.vcmi.eu/index.php) so all team members can see it and share their thoughts. Remember, brainstorming is good for your health. - -## Are you going to support Horn of The Abyss / Wog 3.59 / Grove Town etc.? - -Yes, of course. VCMI is designed as a base for any further mods and uses own executables, so the compatibility is not an issue. The team is not going to compete, but to cooperate with the community of creative modders. - -## Can I help VCMI Project in any way? - -If you are C++ programmer, graphican, tester or just have tons of ideas, do not hesistate - your help is needed. The game is huge and many different ares of activity are still waiting for someone like you. - -## I would like to join development team. - -You are always welcome. Contact the core team via [our board](http://forum.vcmi.eu/index.php). The usual way to join the team is to post your patch for review on our board. If the patch is positively rated by core team members, you will be given access to SVN repository. diff --git a/include/vcmi/Creature.h b/include/vcmi/Creature.h index 645d144ab..8bff40f86 100644 --- a/include/vcmi/Creature.h +++ b/include/vcmi/Creature.h @@ -23,7 +23,7 @@ class DLL_LINKAGE ACreature: public AFactionMember { public: bool isLiving() const; //non-undead, non-non living or alive - ui32 speed(int turn = 0, bool useBind = false) const; //get speed (in moving tiles) of creature with all modificators + ui32 getMovementRange(int turn = 0) const; //get speed (in moving tiles) of creature with all modificators virtual ui32 getMaxHealth() const; //get max HP of stack with all modifiers }; diff --git a/include/vcmi/Entity.h b/include/vcmi/Entity.h index 34ccc2757..eded843b7 100644 --- a/include/vcmi/Entity.h +++ b/include/vcmi/Entity.h @@ -50,6 +50,8 @@ template class DLL_LINKAGE EntityT : public Entity { public: + using IdentifierType = IdType; + virtual IdType getId() const = 0; }; diff --git a/include/vcmi/spells/Caster.h b/include/vcmi/spells/Caster.h index 0d779a2a4..4158c0c53 100644 --- a/include/vcmi/spells/Caster.h +++ b/include/vcmi/spells/Caster.h @@ -17,6 +17,7 @@ class MetaString; class ServerCallback; class CGHeroInstance; class Spell; +class SpellSchool; namespace battle { @@ -38,7 +39,7 @@ public: /// returns level on which given spell would be cast by this(0 - none, 1 - basic etc); /// caster may not know this spell at all /// optionally returns number of selected school by arg - 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic - virtual int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const = 0; + virtual int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const = 0; ///default spell school level for effect calculation virtual int32_t getEffectLevel(const Spell * spell) const = 0; diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index 60e7b8023..1822639e1 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -44,6 +44,7 @@ public: virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind) virtual bool hasSchool(SpellSchool school) const = 0; + virtual bool canCastOnSelf() const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; diff --git a/include/vstd/CLoggerBase.h b/include/vstd/CLoggerBase.h index 37c9e5b94..546b3d352 100644 --- a/include/vstd/CLoggerBase.h +++ b/include/vstd/CLoggerBase.h @@ -172,10 +172,10 @@ private: /// Macros for tracing the control flow of the application conveniently. If the LOG_TRACE macro is used it should be /// the first statement in the function. Logging traces via this macro have almost no impact when the trace is disabled. /// -#define RAII_TRACE(logger, onEntry, onLeave) \ - std::unique_ptr ctl00; \ - if(logger->isTraceEnabled()) \ - ctl00 = std::make_unique(logger, onEntry, onLeave); +#define RAII_TRACE(logger, onEntry, onLeave) \ + std::unique_ptr ctl00 = logger->isTraceEnabled() ? \ + std::make_unique(logger, onEntry, onLeave) : \ + std::unique_ptr() #define LOG_TRACE(logger) RAII_TRACE(logger, \ boost::str(boost::format("Entering %s.") % BOOST_CURRENT_FUNCTION), \ diff --git a/include/vstd/DateUtils.h b/include/vstd/DateUtils.h index d080e96b1..6d4ae0773 100644 --- a/include/vstd/DateUtils.h +++ b/include/vstd/DateUtils.h @@ -5,7 +5,7 @@ VCMI_LIB_NAMESPACE_BEGIN namespace vstd { - DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt); + DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt, std::string format); DLL_LINKAGE std::string getDateTimeISO8601Basic(std::time_t dt); } diff --git a/include/vstd/RNG.h b/include/vstd/RNG.h index 53c323bcb..60e1dedc0 100644 --- a/include/vstd/RNG.h +++ b/include/vstd/RNG.h @@ -36,17 +36,40 @@ namespace RandomGeneratorUtil template auto nextItem(const Container & container, vstd::RNG & rand) -> decltype(std::begin(container)) { - assert(!container.empty()); + if(container.empty()) + throw std::runtime_error("Unable to select random item from empty container!"); + return std::next(container.begin(), rand.getInt64Range(0, container.size() - 1)()); } template auto nextItem(Container & container, vstd::RNG & rand) -> decltype(std::begin(container)) { - assert(!container.empty()); + if(container.empty()) + throw std::runtime_error("Unable to select random item from empty container!"); + return std::next(container.begin(), rand.getInt64Range(0, container.size() - 1)()); } + template + size_t nextItemWeighted(Container & container, vstd::RNG & rand) + { + assert(!container.empty()); + + int64_t totalWeight = std::accumulate(container.begin(), container.end(), 0); + assert(totalWeight > 0); + + int64_t roll = rand.getInt64Range(0, totalWeight - 1)(); + + for (size_t i = 0; i < container.size(); ++i) + { + roll -= container[i]; + if(roll < 0) + return i; + } + return container.size() - 1; + } + template void randomShuffle(std::vector & container, vstd::RNG & rand) { diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f21ad852b..9622ff6d2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -15,10 +15,6 @@ set(launcher_SRCS launcherdirs.cpp jsonutils.cpp updatedialog_moc.cpp - lobby/lobby.cpp - lobby/lobby_moc.cpp - lobby/lobbyroomrequest_moc.cpp - lobby/chat_moc.cpp ) set(launcher_HEADERS @@ -37,10 +33,6 @@ set(launcher_HEADERS launcherdirs.h jsonutils.h updatedialog_moc.h - lobby/lobby.h - lobby/lobby_moc.h - lobby/lobbyroomrequest_moc.h - lobby/chat_moc.h main.h ) @@ -52,13 +44,11 @@ set(launcher_FORMS firstLaunch/firstlaunch_moc.ui mainwindow_moc.ui updatedialog_moc.ui - lobby/lobby_moc.ui - lobby/lobbyroomrequest_moc.ui - lobby/chat_moc.ui ) set(launcher_TS translation/chinese.ts + translation/czech.ts translation/english.ts translation/french.ts translation/german.ts @@ -145,7 +135,7 @@ if (NOT APPLE_IOS AND NOT ANDROID) target_link_libraries(vcmilauncher SDL2::SDL2) endif() -target_link_libraries(vcmilauncher ${VCMI_LIB_TARGET} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) +target_link_libraries(vcmilauncher vcmi Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) target_include_directories(vcmilauncher PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -174,12 +164,11 @@ if(APPLE_IOS) else() set(RESOURCES_DESTINATION ${DATA_DIR}/launcher) - # Copy to build directory for easier debugging + # Link to build directory for easier debugging add_custom_command(TARGET vcmilauncher POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation - + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation ) install(TARGETS vcmilauncher DESTINATION ${BIN_DIR}) diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 1ef4baf13..f73dc3e01 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -1,56 +1,101 @@ - - + + eu.vcmi.VCMI - CC-BY-SA-4.0 - GPL-2.0-or-later VCMI Open-source game engine for Heroes of Might and Magic III + Herní engine s otevřeným kódem pro Heroes of Might and Magic III + Open-Source-Spielengine für Heroes of Might and Magic III + + VCMI Team + Tým VCMI + + CC-BY-SA-4.0 + GPL-2.0-or-later

VCMI is an open-source engine for Heroes of Might and Magic III with new possibilities. Years of intensive work resulted in an impressive amount of features. Among the current features are:

+

VCMI je engine s otevřeným kódem a novými možnostmi pro Heroes of Might and Magic III. Roky usilovné práce vyústily v úchvatném počtu nových funkcí. Mezi současnými funkcemi jsou:

+

VCMI ist eine Open-Source-Engine für Heroes of Might and Magic III mit neuen Möglichkeiten. Jahrelange intensive Arbeit führte zu einer beeindruckenden Anzahl von Features. Zu den aktuellen Features gehören:

  • Complete gameplay mechanics
  • +
  • Kompletní herní mechaniky
  • +
  • Vollständige Spielmechanik
  • Almost all objects, abilities, spells and other content
  • +
  • Skoro všechny předměty, schopnosti, kouzla a ostatní obsah
  • +
  • Fast alle Objekte, Fähigkeiten, Zaubersprüche und andere Inhalte
  • Basic battle AI and adventure AI
  • +
  • Základní AI boje a mapy světa
  • +
  • Grundlegende Kampf- und Abenteuer-KI
  • Many GUI improvements: high resolutions, stack queue, creature window
  • +
  • Mnoho vylepšení rozhraní: vyšší rozlišení, fronta oddílů a okno bojovníků
  • +
  • Viele GUI-Verbesserungen: Hohe Auflösungen, Truppenwarteschlange, Kreaturenfenster
  • Advanced and easy mod support - add new towns, creatures, heroes, artifacts and spells without limits or conflicts
  • +
  • Pokročilá a jednoduchá podpora modifikací - přidání nových měst, bojovníků, hrdinů, artefaktů a kouzel bez limitů a konfliktů
  • +
  • Erweiterte und einfache Mod-Unterstützung - füge neue Städte, Kreaturen, Helden, Artefakte und Zaubersprüche ohne Einschränkungen oder Konflikte hinzu
  • Launcher for easy configuration - download mods from our server and install them immediately!
  • +
  • Spouštěč pro jednoduché nastavení - stahujte modifikace z našeho serveru a hned je instalujte!
  • +
  • Launcher für einfache Konfiguration - Mods von unserem Server herunterladen und sofort installieren!
  • Random map generator that supports objects added by mods
  • +
  • Náhodný generátor map, který podporuje předměty přidané modifikacemi
  • +
  • Zufallsgenerator für Karten, der von Mods hinzugefügte Objekte unterstützt

Note: In order to play the game using VCMI you need to own data files for Heroes of Might and Magic III: The Shadow of Death.

+

Poznámka: pokud chcete hrát hru přes VCMI, musíte vlastnit datové soubory Heroes of Might and Magic III: The Shadow of Death.

+

Hinweis: Um das Spiel mit VCMI spielen zu können, sind die Originaldateien für Heroes of Might and Magic III: The Shadow of Death erforderlich.

If you want help, please check our forum, bug tracker or GitHub page.

+

Pokud chcete pomoct, prosíme, podívejte se na naše fórum nebo GitHub.

+

Wird Hilfe benötigt, besucht bitte unser Forum, den Bugtracker oder die GitHub-Seite.

- https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440 + https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440 - https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440 + https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440 - https://play-lh.googleusercontent.com/993tSmsQh1qgb1_XQUxD1wmS8Zuiydf0LsQK_nU2fAiJl62n0_Vl_5JMVK-J9lvVeQ=w2560-h1440 + https://play-lh.googleusercontent.com/993tSmsQh1qgb1_XQUxD1wmS8Zuiydf0LsQK_nU2fAiJl62n0_Vl_5JMVK-J9lvVeQ=w2560-h1440 - https://play-lh.googleusercontent.com/N3fn4cqX2iN0t9jzEo0vgEMBpUYWyRgOz8AtDsMLmF-BTyxO4l2uoAderEIhXPvPsg=w2560-h1440 + https://play-lh.googleusercontent.com/N3fn4cqX2iN0t9jzEo0vgEMBpUYWyRgOz8AtDsMLmF-BTyxO4l2uoAderEIhXPvPsg=w2560-h1440 - https://play-lh.googleusercontent.com/9wSQsPlMPSBAzekEcw1OVpzeOvYiHrYRYHgpa1aRk31pvR752gu_cUZws5x8JMCACw=w2560-h1440 + https://play-lh.googleusercontent.com/9wSQsPlMPSBAzekEcw1OVpzeOvYiHrYRYHgpa1aRk31pvR752gu_cUZws5x8JMCACw=w2560-h1440 - https://play-lh.googleusercontent.com/nU_MSwmEhiQ14Sx9xI3QFF1FhL7BYTDMbEYQg-XglSYwIMpYZDpP92_ybTtu4cVJSxo=w2560-h1440 + https://play-lh.googleusercontent.com/nU_MSwmEhiQ14Sx9xI3QFF1FhL7BYTDMbEYQg-XglSYwIMpYZDpP92_ybTtu4cVJSxo=w2560-h1440 - https://play-lh.googleusercontent.com/rhI5moPCKQpYdjnRaJJ_CdIxSAXUkrH7ddghuPWaW0vcyaltd-ZGZG4Kl6v3YKGVlzU=w2560-h1440 + https://play-lh.googleusercontent.com/rhI5moPCKQpYdjnRaJJ_CdIxSAXUkrH7ddghuPWaW0vcyaltd-ZGZG4Kl6v3YKGVlzU=w2560-h1440 - https://play-lh.googleusercontent.com/1zecL5SvayVkNoIHijeS_rm0Yr_XyHbp_dmSx70NXhjckvnezwvWkpINYBHAy4o8EaM=w2560-h1440 + https://play-lh.googleusercontent.com/1zecL5SvayVkNoIHijeS_rm0Yr_XyHbp_dmSx70NXhjckvnezwvWkpINYBHAy4o8EaM=w2560-h1440 - https://play-lh.googleusercontent.com/e5ibvuWm442dYN2z_pAwDC3Ktc7XOY6FEI4N7BkFB7DqGi3Q-Vl46KqoJ_EFSrXRNw=w2560-h1440 + https://play-lh.googleusercontent.com/e5ibvuWm442dYN2z_pAwDC3Ktc7XOY6FEI4N7BkFB7DqGi3Q-Vl46KqoJ_EFSrXRNw=w2560-h1440 - https://play-lh.googleusercontent.com/wD6xJK2nuzVyKUFODfQs2SEbUuSyEglojpp_FE6GeAuzdA_R44Dp6gTyN70KCwpRtD9M=w2560-h1440 + https://play-lh.googleusercontent.com/wD6xJK2nuzVyKUFODfQs2SEbUuSyEglojpp_FE6GeAuzdA_R44Dp6gTyN70KCwpRtD9M=w2560-h1440 + vcmilauncher.desktop + + + + + + + + + + + + + + + + + + https://vcmi.eu/ https://github.com/vcmi/vcmi/issues https://vcmi.eu/faq/ @@ -58,32 +103,15 @@ https://github.com/vcmi/vcmi/blob/master/docs/modders/Translations.md https://discord.gg/chBT42V https://github.com/vcmi/vcmi - - keyboard - pointing - touch - Game StrategyGame - - - - - - - - - - - - - - heroes3 - homm3 - - VCMI Team + + pointing + keyboard + touch + moderate moderate @@ -97,4 +125,8 @@ vcmilauncher vcmiserver + + heroes3 + homm3 +
diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index 7d09dd688..5a30b5c62 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/launcher/firstLaunch/firstlaunch_moc.cpp @@ -134,7 +134,16 @@ void FirstLaunchView::activateTabHeroesData() ui->pushButtonDataHelp->hide(); ui->labelDataHelp->hide(); } - heroesDataUpdate(); + if(heroesDataUpdate()) + return; + + QString installPath = getHeroesInstallDir(); + if(!installPath.isEmpty()) + { + auto reply = QMessageBox::question(this, tr("Heroes III installation found!"), tr("Copy data to VCMI folder?"), QMessageBox::Yes | QMessageBox::No); + if(reply == QMessageBox::Yes) + copyHeroesData(installPath); + } } void FirstLaunchView::activateTabModPreset() @@ -164,12 +173,14 @@ void FirstLaunchView::languageSelected(const QString & selectedLanguage) mainWindow->updateTranslation(); } -void FirstLaunchView::heroesDataUpdate() +bool FirstLaunchView::heroesDataUpdate() { - if(heroesDataDetect()) + bool detected = heroesDataDetect(); + if(detected) heroesDataDetected(); else heroesDataMissing(); + return detected; } void FirstLaunchView::heroesDataMissing() @@ -254,9 +265,26 @@ void FirstLaunchView::forceHeroesLanguage(const QString & language) node->String() = language.toStdString(); } -void FirstLaunchView::copyHeroesData() +QString FirstLaunchView::getHeroesInstallDir() { - QDir sourceRoot = QFileDialog::getExistingDirectory(this, "", "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); +#ifdef VCMI_WINDOWS + QString gogPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787", QSettings::NativeFormat).value("path").toString(); + if(!gogPath.isEmpty()) + return gogPath; + + QString cdPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0", QSettings::NativeFormat).value("AppPath").toString(); + if(!cdPath.isEmpty()) + return cdPath; +#endif + return QString{}; +} + +void FirstLaunchView::copyHeroesData(const QString & path) +{ + QDir sourceRoot = QDir(path); + + if(path.isEmpty()) + sourceRoot.setPath(QFileDialog::getExistingDirectory(this, {}, {}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks)); if(!sourceRoot.exists()) return; diff --git a/launcher/firstLaunch/firstlaunch_moc.h b/launcher/firstLaunch/firstlaunch_moc.h index dd773c365..d17438000 100644 --- a/launcher/firstLaunch/firstlaunch_moc.h +++ b/launcher/firstLaunch/firstlaunch_moc.h @@ -42,7 +42,7 @@ class FirstLaunchView : public QWidget void languageSelected(const QString & languageCode); // Tab Heroes III Data - void heroesDataUpdate(); + bool heroesDataUpdate(); bool heroesDataDetect(); void heroesDataMissing(); @@ -51,7 +51,8 @@ class FirstLaunchView : public QWidget void heroesLanguageUpdate(); void forceHeroesLanguage(const QString & language); - void copyHeroesData(); + QString getHeroesInstallDir(); + void copyHeroesData(const QString & path = {}); // Tab Mod Preset void modPresetUpdate(); diff --git a/launcher/icons/menu-lobby.png b/launcher/icons/menu-lobby.png deleted file mode 100644 index 6385fc3b5..000000000 Binary files a/launcher/icons/menu-lobby.png and /dev/null differ diff --git a/launcher/icons/room-private.png b/launcher/icons/room-private.png deleted file mode 100644 index 33896d9b9..000000000 Binary files a/launcher/icons/room-private.png and /dev/null differ diff --git a/launcher/jsonutils.cpp b/launcher/jsonutils.cpp index 895eee540..4635e9f50 100644 --- a/launcher/jsonutils.cpp +++ b/launcher/jsonutils.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "jsonutils.h" +#include "../lib/json/JsonNode.h" + static QVariantMap JsonToMap(const JsonMap & json) { QVariantMap map; @@ -87,7 +89,7 @@ QVariant JsonFromFile(QString filename) } const auto data = file.readAll(); - JsonNode node(data.data(), data.size()); + JsonNode node(reinterpret_cast(data.data()), data.size()); return toVariant(node); } @@ -114,7 +116,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); - file << toJson(object).toJson(); + file << toJson(object).toString(); } } diff --git a/launcher/jsonutils.h b/launcher/jsonutils.h index 6dd7e7bbf..791711eb0 100644 --- a/launcher/jsonutils.h +++ b/launcher/jsonutils.h @@ -10,10 +10,11 @@ #pragma once #include -#include "../lib/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; + namespace JsonUtils { QVariant toVariant(const JsonNode & node); diff --git a/launcher/languages.cpp b/launcher/languages.cpp index 670faa345..8dcd86d39 100644 --- a/launcher/languages.cpp +++ b/launcher/languages.cpp @@ -86,7 +86,7 @@ QString Languages::generateLanguageName(const Languages::Options & language) void Languages::fillLanguages(QComboBox * widget, bool includeAll) { - widget->blockSignals(true); // we do not want calls caused by initialization + QSignalBlocker guard(widget); // we do not want calls caused by initialization widget->clear(); std::string activeLanguage = includeAll ? @@ -115,13 +115,11 @@ void Languages::fillLanguages(QComboBox * widget, bool includeAll) if(activeLanguage == language.identifier) widget->setCurrentIndex(widget->count() - 1); } - - widget->blockSignals(false); } void Languages::fillLanguages(QListWidget * widget, bool includeAll) { - widget->blockSignals(true); // we do not want calls caused by initialization + QSignalBlocker guard(widget); // we do not want calls caused by initialization widget->clear(); std::string activeLanguage = includeAll ? @@ -154,5 +152,4 @@ void Languages::fillLanguages(QListWidget * widget, bool includeAll) if(activeLanguage == language.identifier) widget->setCurrentRow(widget->count() - 1); } - widget->blockSignals(false); } diff --git a/launcher/launcherdirs.cpp b/launcher/launcherdirs.cpp index 97d456bb5..81e474f94 100644 --- a/launcher/launcherdirs.cpp +++ b/launcher/launcherdirs.cpp @@ -34,3 +34,8 @@ QString CLauncherDirs::modsPath() { return pathToQString(VCMIDirs::get().userDataPath() / "Mods"); } + +QString CLauncherDirs::mapsPath() +{ + return pathToQString(VCMIDirs::get().userDataPath() / "Maps"); +} diff --git a/launcher/launcherdirs.h b/launcher/launcherdirs.h index 9117bd9fb..e549fb218 100644 --- a/launcher/launcherdirs.h +++ b/launcher/launcherdirs.h @@ -19,4 +19,5 @@ public: QString downloadsPath(); QString modsPath(); + QString mapsPath(); }; diff --git a/launcher/lobby/chat_moc.cpp b/launcher/lobby/chat_moc.cpp deleted file mode 100644 index a260ba312..000000000 --- a/launcher/lobby/chat_moc.cpp +++ /dev/null @@ -1,173 +0,0 @@ -/* - * chat_moc.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "chat_moc.h" -#include "ui_chat_moc.h" - -Chat::Chat(QWidget *parent) : - QWidget(parent), - ui(new Ui::Chat) -{ - ui->setupUi(this); - - namesCompleter.setModel(ui->listUsers->model()); - namesCompleter.setCompletionMode(QCompleter::InlineCompletion); - - ui->messageEdit->setCompleter(&namesCompleter); - - for([[maybe_unused]] auto i : {GLOBAL, ROOM}) - chatDocuments.push_back(new QTextDocument(this)); - - setChatId(GLOBAL); -} - -Chat::~Chat() -{ - delete ui; -} - -void Chat::setUsername(const QString & user) -{ - username = user; -} - -void Chat::setSession(const QString & s) -{ - session = s; - - on_chatSwitch_clicked(); -} - -void Chat::setChannel(const QString & channel) -{ - static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; - - setChatId(chatNames.value(channel)); -} - -void Chat::addUser(const QString & user) -{ - ui->listUsers->addItem(new QListWidgetItem("@" + user)); -} - -void Chat::clearUsers() -{ - ui->listUsers->clear(); -} - -void Chat::chatMessage(const QString & title, const QString & channel, QString body, bool isSystem) -{ - const QTextCharFormat regularFormat; - const QString boldHtml = "%1"; - const QString colorHtml = "%2"; - bool meMentioned = false; - bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24); - - static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; - QTextDocument * doc = ui->chat->document(); - if(chatNames.contains(channel)) - doc = chatDocuments[chatNames.value(channel)]; - - QTextCursor curs(doc); - curs.movePosition(QTextCursor::End); - - QString titleColor = "Olive"; - if(isSystem || title == "System") - titleColor = "ForestGreen"; - if(title == username) - titleColor = "Gold"; - - curs.insertHtml(boldHtml.arg(colorHtml.arg(titleColor, title + ": "))); - - QRegularExpression mentionRe("@[\\w\\d]+"); - auto subBody = body; - int mem = 0; - for(auto match = mentionRe.match(subBody); match.hasMatch(); match = mentionRe.match(subBody)) - { - body.insert(mem + match.capturedEnd(), QChar(-1)); - body.insert(mem + match.capturedStart(), QChar(-1)); - mem += match.capturedEnd() + 2; - subBody = body.right(body.size() - mem); - } - auto pieces = body.split(QChar(-1)); - for(auto & block : pieces) - { - if(block.startsWith("@")) - { - if(block == "@" + username) - { - meMentioned = true; - curs.insertHtml(boldHtml.arg(colorHtml.arg("IndianRed", block))); - } - else - curs.insertHtml(colorHtml.arg("DeepSkyBlue", block)); - } - else - { - if(isSystem) - curs.insertHtml(colorHtml.arg("ForestGreen", block)); - else - curs.insertText(block, regularFormat); - } - } - curs.insertText("\n", regularFormat); - - if(doc == ui->chat->document() && (meMentioned || isScrollBarBottom)) - { - ui->chat->ensureCursorVisible(); - ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); - } -} - -void Chat::chatMessage(const QString & title, QString body, bool isSystem) -{ - chatMessage(title, "", body, isSystem); -} - -void Chat::sysMessage(QString body) -{ - chatMessage("System", body, true); -} - -void Chat::sendMessage() -{ - QString msg(ui->messageEdit->text()); - ui->messageEdit->clear(); - emit messageSent(msg); -} - -void Chat::on_messageEdit_returnPressed() -{ - sendMessage(); -} - -void Chat::on_sendButton_clicked() -{ - sendMessage(); -} - -void Chat::on_chatSwitch_clicked() -{ - static const QMap chatNames{{GLOBAL, "global"}, {ROOM, "room"}}; - - if(chatId == GLOBAL && !session.isEmpty()) - emit channelSwitch(chatNames[ROOM]); - else - emit channelSwitch(chatNames[GLOBAL]); -} - -void Chat::setChatId(ChatId _chatId) -{ - static const QMap chatNames{{GLOBAL, "Global"}, {ROOM, "Room"}}; - - chatId = _chatId; - ui->chatSwitch->setText(chatNames[chatId] + " chat"); - ui->chat->setDocument(chatDocuments[chatId]); -} diff --git a/launcher/lobby/chat_moc.h b/launcher/lobby/chat_moc.h deleted file mode 100644 index d5a735f67..000000000 --- a/launcher/lobby/chat_moc.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * chat_moc.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include -#include - -namespace Ui { -class Chat; -} - -class Chat : public QWidget -{ - Q_OBJECT - - enum ChatId - { - GLOBAL = 0, - ROOM - }; - - QCompleter namesCompleter; - QString username, session; - ChatId chatId = GLOBAL; - - QVector chatDocuments; - -private: - void setChatId(ChatId); - void sendMessage(); - -public: - explicit Chat(QWidget *parent = nullptr); - ~Chat(); - - void setUsername(const QString &); - void setSession(const QString &); - void setChannel(const QString &); - - void clearUsers(); - void addUser(const QString & user); - - void chatMessage(const QString & title, const QString & channel, QString body, bool isSystem = false); - void chatMessage(const QString & title, QString body, bool isSystem = false); - -signals: - void messageSent(QString); - void channelSwitch(QString); - -public slots: - void sysMessage(QString body); - -private slots: - void on_messageEdit_returnPressed(); - - void on_sendButton_clicked(); - - void on_chatSwitch_clicked(); - -private: - Ui::Chat *ui; -}; diff --git a/launcher/lobby/chat_moc.ui b/launcher/lobby/chat_moc.ui deleted file mode 100644 index a3208d982..000000000 --- a/launcher/lobby/chat_moc.ui +++ /dev/null @@ -1,121 +0,0 @@ - - - Chat - - - - 0 - 0 - 465 - 413 - - - - Form - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - Users in lobby - - - -1 - - - - - - - Global chat - - - - - - - - - - 0 - 0 - - - - - 16777215 - 96 - - - - 0 - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - true - - - QListView::SinglePass - - - - - - - - - - -1 - - - 0 - - - - - - - - type you message - - - - - - - send - - - - - - - - - - diff --git a/launcher/lobby/lobby.cpp b/launcher/lobby/lobby.cpp deleted file mode 100644 index 3e8c9648e..000000000 --- a/launcher/lobby/lobby.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/* - * lobby.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "lobby.h" -#include "../lib/GameConstants.h" - -SocketLobby::SocketLobby(QObject *parent) : - QObject(parent) -{ - socket = new QTcpSocket(this); - connect(socket, SIGNAL(connected()), this, SLOT(connected())); - connect(socket, SIGNAL(disconnected()), this, SLOT(disconnected())); - connect(socket, SIGNAL(readyRead()), this, SLOT(readyRead())); - connect(socket, SIGNAL(bytesWritten(qint64)), this, SLOT(bytesWritten(qint64))); -} - -void SocketLobby::connectServer(const QString & host, int port, const QString & usr, int timeout) -{ - username = usr; - - socket->connectToHost(host, port); - - if(!socket->waitForDisconnected(timeout) && !isConnected) - { - emit text("Error: " + socket->errorString()); - emit disconnect(); - } -} - -void SocketLobby::disconnectServer() -{ - socket->disconnectFromHost(); -} - -void SocketLobby::requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap & mods) -{ - const QString sessionMessage = ProtocolStrings[CREATE].arg(session, pswd, QString::number(totalPlayers), prepareModsClientString(mods)); - send(sessionMessage); -} - -void SocketLobby::requestJoinSession(const QString & session, const QString & pswd, const QMap & mods) -{ - const QString sessionMessage = ProtocolStrings[JOIN].arg(session, pswd, prepareModsClientString(mods)); - send(sessionMessage); -} - -void SocketLobby::requestLeaveSession(const QString & session) -{ - const QString sessionMessage = ProtocolStrings[LEAVE].arg(session); - send(sessionMessage); -} - -void SocketLobby::requestReadySession(const QString & session) -{ - const QString sessionMessage = ProtocolStrings[READY].arg(session); - send(sessionMessage); -} - -void SocketLobby::send(const QString & msg) -{ - QByteArray str = msg.toUtf8(); - int sz = str.size(); - QByteArray pack((const char *)&sz, sizeof(sz)); - pack.append(str); - socket->write(pack); -} - -void SocketLobby::connected() -{ - isConnected = true; - emit text("Connected!"); - - QByteArray greetingBytes; - greetingBytes.append(ProtocolVersion); - greetingBytes.append(ProtocolEncoding.size()); - const QString greetingConst = QString(greetingBytes) - + ProtocolStrings[GREETING].arg(QString::fromStdString(ProtocolEncoding), - username, - QString::fromStdString(GameConstants::VCMI_VERSION)); - send(greetingConst); -} - -void SocketLobby::disconnected() -{ - isConnected = false; - emit disconnect(); - emit text("Disconnected!"); -} - -void SocketLobby::bytesWritten(qint64 bytes) -{ - qDebug() << "We wrote: " << bytes; -} - -void SocketLobby::readyRead() -{ - qDebug() << "Reading..."; - emit receive(socket->readAll()); -} - - -ServerCommand::ServerCommand(ProtocolConsts cmd, const QStringList & args): - command(cmd), - arguments(args) -{ -} - -QString prepareModsClientString(const QMap & mods) -{ - QStringList result; - for(auto & mod : mods.keys()) - { - result << mod + "&" + mods[mod]; - } - return result.join(";"); -} diff --git a/launcher/lobby/lobby.h b/launcher/lobby/lobby.h deleted file mode 100644 index 84fe67e22..000000000 --- a/launcher/lobby/lobby.h +++ /dev/null @@ -1,226 +0,0 @@ -/* - * lobby.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include -#include - -const unsigned int ProtocolVersion = 5; -const std::string ProtocolEncoding = "utf8"; - -class ProtocolError: public std::runtime_error -{ -public: - ProtocolError(const char * w): std::runtime_error(w) {} -}; - -enum ProtocolConsts -{ - //client consts - GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE, SETCHANNEL, - - //server consts - SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, CHATCHANNEL, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE, CHANNEL -}; - -const QMap ProtocolStrings -{ - //=== client commands === - - //handshaking with server - //%1: first byte is protocol_version, then size of encoding string in bytes, then encoding string - //%2: client name - //%3: VCMI version - {GREETING, "%1%2%3"}, - - //[unsupported] autorization with username - //%1: username - {USERNAME, "%1"}, - - //sending message to the chat - //%1: message text - {MESSAGE, "%1"}, - - //create new room - //%1: room name - //%2: password for the room - //%3: max number of players - //%4: mods used by host - // each mod has a format modname&modversion, mods should be separated by ; symbol - {CREATE, "%1%2%3%4"}, - - //join to the room - //%1: room name - //%2: password for the room - //%3: list of mods used by player - // each mod has a format modname&modversion, mods should be separated by ; symbol - {JOIN, "%1%2%3"}, - - //leave the room - //%1: room name - {LEAVE, "%1"}, - - //kick user from the current room - //%1: player username - {KICK, "%1"}, - - //signal that player is ready for game - //%1: room name - {READY, "%1"}, - - //[unsupported] start session immediately - //%1: room name - {FORCESTART, "%1"}, - - //request user list - {HERE, ""}, - - //used as reponse to healcheck - {ALIVE, ""}, - - //host sets game mode (new game or load game) - //%1: game mode - 0 for new game, 1 for load game - {HOSTMODE, "%1"}, - - //set new chat channel - //%1: channel name - {SETCHANNEL, "%1"}, - - //=== server commands === - //server commands are started from :>>, arguments are enumerated by : symbol - - //new session was created - //arg[0]: room name - {CREATED, "CREATED"}, - - //list of existing sessions - //arg[0]: amount of sessions, following arguments depending on it - //arg[x]: session name - //arg[x+1]: amount of players in the session - //arg[x+2]: total amount of players allowed - //arg[x+3]: True if session is protected by password - {SESSIONS, "SESSIONS"}, - - //user has joined to the session - //arg[0]: session name - //arg[1]: username (who was joined) - {JOINED, "JOIN"}, - - //user has left the session - //arg[0]: session name - //arg[1]: username (who has left) - {KICKED, "KICK"}, - - //session has been started - //arg[0]: session name - //arg[1]: uuid to be used for connection - {START, "START"}, - - //host ownership for the game session - //arg[0]: uuid to be used by vcmiserver - //arg[1]: amount of players (clients) to be connected - {HOST, "HOST"}, - - //room status - //arg[0]: amount of players, following arguments depending on it - //arg[x]: player username - //arg[x+1]: True if player is ready - {STATUS, "STATUS"}, //joined_players:player_name:is_ready - - //server error - //arg[0]: error message - {SRVERROR, "ERROR"}, - - //mods used in the session by host player - //arg[0]: amount of mods, following arguments depending on it - //arg[x]: mod name - //arg[x+1]: mod version - {MODS, "MODS"}, - - //mods used by user - //arg[0]: username - //arg[1]: amount of mods, following arguments depending on it - //arg[x]: mod name - //arg[x+1]: mod version - {CLIENTMODS, "MODSOTHER"}, - - //received chat message - //arg[0]: sender username - //arg[1]: channel - //arg[2]: message text - {CHAT, "MSG"}, - - //received chat message to specific channel - //arg[0]: sender username - //arg[1]: channel - //arg[2]: message text - {CHATCHANNEL, "MSGCH"}, - - //list of users currently in lobby - //arg[0]: amount of players, following arguments depend on it - //arg[x]: username - //arg[x+1]: room (empty if not in the room) - {USERS, "USERS"}, - - //healthcheck from server - {HEALTH, "HEALTH"}, - - //game mode (new game or load game) set by host - //arg[0]: game mode - {GAMEMODE, "GAMEMODE"}, - - //chat channel changed - //arg[0]: channel name - {CHANNEL, "CHANNEL"}, -}; - -class ServerCommand -{ -public: - ServerCommand(ProtocolConsts, const QStringList & arguments); - - const ProtocolConsts command; - const QStringList arguments; -}; - -class SocketLobby : public QObject -{ - Q_OBJECT -public: - explicit SocketLobby(QObject *parent = nullptr); - void connectServer(const QString & host, int port, const QString & username, int timeout); - void disconnectServer(); - void requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap & mods); - void requestJoinSession(const QString & session, const QString & pswd, const QMap & mods); - void requestLeaveSession(const QString & session); - void requestReadySession(const QString & session); - - void send(const QString &); - -signals: - - void text(QString); - void receive(QString); - void disconnect(); - -public slots: - - void connected(); - void disconnected(); - void bytesWritten(qint64 bytes); - void readyRead(); - -private: - QTcpSocket *socket; - bool isConnected = false; - QString username; -}; - -QString prepareModsClientString(const QMap & mods); diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp deleted file mode 100644 index 308148d3c..000000000 --- a/launcher/lobby/lobby_moc.cpp +++ /dev/null @@ -1,583 +0,0 @@ -/* - * lobby_moc.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "main.h" -#include "lobby_moc.h" -#include "ui_lobby_moc.h" -#include "lobbyroomrequest_moc.h" -#include "../mainwindow_moc.h" -#include "../modManager/cmodlist.h" -#include "../../lib/CConfigHandler.h" - -enum GameMode -{ - NEW_GAME = 0, LOAD_GAME = 1 -}; - -enum ModResolutionRoles -{ - ModNameRole = Qt::UserRole + 1, - ModEnableRole, - ModResolvableRole -}; - -Lobby::Lobby(QWidget *parent) : - QWidget(parent), - ui(new Ui::Lobby) -{ - ui->setupUi(this); - - connect(&socketLobby, SIGNAL(text(QString)), ui->chatWidget, SLOT(sysMessage(QString))); - connect(&socketLobby, SIGNAL(receive(QString)), this, SLOT(dispatchMessage(QString))); - connect(&socketLobby, SIGNAL(disconnect()), this, SLOT(onDisconnected())); - connect(ui->chatWidget, SIGNAL(messageSent(QString)), this, SLOT(onMessageSent(QString))); - connect(ui->chatWidget, SIGNAL(channelSwitch(QString)), this, SLOT(onChannelSwitch(QString))); - - QString hostString("%1:%2"); - hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String())); - hostString = hostString.arg(settings["launcher"]["lobbyPort"].Integer()); - - ui->serverEdit->setText(hostString); - ui->userEdit->setText(QString::fromStdString(settings["launcher"]["lobbyUsername"].String())); - ui->kickButton->setVisible(false); -} - -void Lobby::changeEvent(QEvent *event) -{ - if(event->type() == QEvent::LanguageChange) - { - ui->retranslateUi(this); - } - QWidget::changeEvent(event); -} - -Lobby::~Lobby() -{ - delete ui; -} - -QMap Lobby::buildModsMap() const -{ - QMap result; - QObject * mainWindow = qApp->activeWindow(); - if(!mainWindow) - mainWindow = parent(); - if(!mainWindow) - return result; //probably something is really wrong here - - while(mainWindow->parent()) - mainWindow = mainWindow->parent(); - const auto & modlist = qobject_cast(mainWindow)->getModList(); - - for(auto & modname : modlist.getModList()) - { - auto mod = modlist.getMod(modname); - if(mod.isEnabled()) - { - result[modname] = mod.getValue("version").toString(); - } - } - return result; -} - -bool Lobby::isModAvailable(const QString & modName, const QString & modVersion) const -{ - QObject * mainWindow = qApp->activeWindow(); - while(mainWindow->parent()) - mainWindow = mainWindow->parent(); - const auto & modlist = qobject_cast(mainWindow)->getModList(); - - if(!modlist.hasMod(modName)) - return false; - - auto mod = modlist.getMod(modName); - return (mod.isInstalled () || mod.isAvailable()) && (mod.getValue("version") == modVersion); -} - -void Lobby::serverCommand(const ServerCommand & command) try -{ - //initialize variables outside of switch block - const QString statusPlaceholder("%1 %2\n"); - const auto & args = command.arguments; - int amount, tagPoint; - QString joinStr; - switch(command.command) - { - case SRVERROR: - protocolAssert(args.size()); - ui->chatWidget->chatMessage("System error", args[0], true); - if(authentificationStatus == AuthStatus::AUTH_NONE) - authentificationStatus = AuthStatus::AUTH_ERROR; - break; - - case CREATED: - protocolAssert(args.size()); - hostSession = args[0]; - session = args[0]; - ui->chatWidget->setSession(session); - break; - - case SESSIONS: - protocolAssert(args.size()); - amount = args[0].toInt(); - protocolAssert(amount * 4 == (args.size() - 1)); - ui->sessionsTable->setRowCount(amount); - - tagPoint = 1; - for(int i = 0; i < amount; ++i) - { - QTableWidgetItem * sessionNameItem = new QTableWidgetItem(args[tagPoint++]); - ui->sessionsTable->setItem(i, 0, sessionNameItem); - - int playersJoined = args[tagPoint++].toInt(); - int playersTotal = args[tagPoint++].toInt(); - QTableWidgetItem * sessionPlayerItem = new QTableWidgetItem(QString("%1/%2").arg(playersJoined).arg(playersTotal)); - ui->sessionsTable->setItem(i, 1, sessionPlayerItem); - - QTableWidgetItem * sessionProtectedItem = new QTableWidgetItem(); - bool isPrivate = (args[tagPoint++] == "True"); - sessionProtectedItem->setData(Qt::UserRole, isPrivate); - if(isPrivate) - sessionProtectedItem->setIcon(QIcon("icons:room-private.png")); - ui->sessionsTable->setItem(i, 2, sessionProtectedItem); - } - break; - - case JOINED: - case KICKED: - protocolAssert(args.size() == 2); - if(args[1] == username) - { - hostModsMap.clear(); - session = ""; - ui->chatWidget->setSession(session); - ui->buttonReady->setText("Ready"); - ui->optNewGame->setChecked(true); - session = args[0]; - ui->chatWidget->setSession(session); - bool isHost = command.command == JOINED && hostSession == session; - ui->optNewGame->setEnabled(isHost); - ui->optLoadGame->setEnabled(isHost); - ui->stackedWidget->setCurrentWidget(command.command == JOINED ? ui->roomPage : ui->sessionsPage); - } - else - { - joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); - ui->chatWidget->sysMessage(joinStr.arg(args[1], args[0])); - } - break; - - case MODS: { - protocolAssert(args.size() > 0); - amount = args[0].toInt(); - protocolAssert(amount * 2 == (args.size() - 1)); - - tagPoint = 1; - for(int i = 0; i < amount; ++i, tagPoint += 2) - hostModsMap[args[tagPoint]] = args[tagPoint + 1]; - - updateMods(); - break; - } - - case CLIENTMODS: { - protocolAssert(args.size() >= 1); - auto & clientModsMap = clientsModsMap[args[0]]; - amount = args[1].toInt(); - protocolAssert(amount * 2 == (args.size() - 2)); - - tagPoint = 2; - for(int i = 0; i < amount; ++i, tagPoint += 2) - clientModsMap[args[tagPoint]] = args[tagPoint + 1]; - - break; - } - - - case STATUS: - protocolAssert(args.size() > 0); - amount = args[0].toInt(); - protocolAssert(amount * 2 == (args.size() - 1)); - - tagPoint = 1; - ui->playersList->clear(); - for(int i = 0; i < amount; ++i, tagPoint += 2) - { - if(args[tagPoint + 1] == "True") - ui->playersList->addItem(new QListWidgetItem(QIcon("icons:mod-enabled.png"), args[tagPoint])); - else - ui->playersList->addItem(new QListWidgetItem(QIcon("icons:mod-disabled.png"), args[tagPoint])); - - if(args[tagPoint] == username) - { - if(args[tagPoint + 1] == "True") - ui->buttonReady->setText("Not ready"); - else - ui->buttonReady->setText("Ready"); - } - } - break; - - case START: { - protocolAssert(args.size() == 1); - //actually start game - gameArgs << "--lobby"; - gameArgs << "--lobby-address" << serverUrl; - gameArgs << "--lobby-port" << QString::number(serverPort); - gameArgs << "--lobby-username" << username; - gameArgs << "--lobby-gamemode" << QString::number(isLoadGameMode); - gameArgs << "--uuid" << args[0]; - startGame(gameArgs); - break; - } - - case HOST: { - protocolAssert(args.size() == 2); - gameArgs << "--lobby-host"; - gameArgs << "--lobby-uuid" << args[0]; - gameArgs << "--lobby-connections" << args[1]; - break; - } - - case CHAT: { - protocolAssert(args.size() > 1); - QString msg; - for(int i = 1; i < args.size(); ++i) - msg += args[i]; - ui->chatWidget->chatMessage(args[0], msg); - break; - } - - case CHATCHANNEL: { - protocolAssert(args.size() > 2); - QString msg; - for(int i = 2; i < args.size(); ++i) - msg += args[i]; - ui->chatWidget->chatMessage(args[0], args[1], msg); - break; - } - - case CHANNEL: { - protocolAssert(args.size() == 1); - ui->chatWidget->setChannel(args[0]); - break; - } - - case HEALTH: { - socketLobby.send(ProtocolStrings[ALIVE]); - break; - } - - case USERS: { - protocolAssert(args.size() > 0); - amount = args[0].toInt(); - - protocolAssert(amount == (args.size() - 1)); - ui->chatWidget->clearUsers(); - for(int i = 0; i < amount; ++i) - { - ui->chatWidget->addUser(args[i + 1]); - } - break; - } - - case GAMEMODE: { - protocolAssert(args.size() == 1); - isLoadGameMode = args[0].toInt(); - if(isLoadGameMode) - ui->optLoadGame->setChecked(true); - else - ui->optNewGame->setChecked(true); - break; - } - - default: - ui->chatWidget->sysMessage("Unknown server command"); - } - - if(authentificationStatus == AuthStatus::AUTH_ERROR) - { - socketLobby.disconnectServer(); - } - else - { - authentificationStatus = AuthStatus::AUTH_OK; - ui->newButton->setEnabled(true); - } -} -catch(const ProtocolError & e) -{ - ui->chatWidget->chatMessage("System error", e.what(), true); -} - -void Lobby::dispatchMessage(QString txt) try -{ - if(txt.isEmpty()) - return; - - QStringList parseTags = txt.split(":>>"); - protocolAssert(parseTags.size() > 1 && parseTags[0].isEmpty() && !parseTags[1].isEmpty()); - - for(int c = 1; c < parseTags.size(); ++c) - { - QStringList parseArgs = parseTags[c].split(":"); - protocolAssert(parseArgs.size() > 1); - - auto ctype = ProtocolStrings.key(parseArgs[0]); - parseArgs.pop_front(); - ServerCommand cmd(ctype, parseArgs); - serverCommand(cmd); - } -} -catch(const ProtocolError & e) -{ - ui->chatWidget->chatMessage("System error", e.what(), true); -} - -void Lobby::onDisconnected() -{ - authentificationStatus = AuthStatus::AUTH_NONE; - session = ""; - ui->chatWidget->setSession(session); - ui->chatWidget->setChannel("global"); - ui->stackedWidget->setCurrentWidget(ui->sessionsPage); - ui->connectButton->setChecked(false); - ui->serverEdit->setEnabled(true); - ui->userEdit->setEnabled(true); - ui->newButton->setEnabled(false); - ui->joinButton->setEnabled(false); - ui->sessionsTable->setRowCount(0); -} - -void Lobby::protocolAssert(bool expr) -{ - if(!expr) - throw ProtocolError("Protocol error"); -} - -void Lobby::on_connectButton_toggled(bool checked) -{ - if(checked) - { - ui->connectButton->setText(tr("Disconnect")); - authentificationStatus = AuthStatus::AUTH_NONE; - username = ui->userEdit->text(); - ui->chatWidget->setUsername(username); - const int connectionTimeout = settings["launcher"]["connectionTimeout"].Integer(); - - auto serverStrings = ui->serverEdit->text().split(":"); - if(serverStrings.size() != 2) - { - QMessageBox::critical(this, "Connection error", "Server address must have the format URL:port"); - return; - } - - serverUrl = serverStrings[0]; - serverPort = serverStrings[1].toInt(); - - Settings node = settings.write["launcher"]; - node["lobbyUrl"].String() = serverUrl.toStdString(); - node["lobbyPort"].Integer() = serverPort; - node["lobbyUsername"].String() = username.toStdString(); - - ui->serverEdit->setEnabled(false); - ui->userEdit->setEnabled(false); - - ui->chatWidget->sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort)); - //show text immediately - ui->chatWidget->repaint(); - qApp->processEvents(); - - socketLobby.connectServer(serverUrl, serverPort, username, connectionTimeout); - } - else - { - ui->connectButton->setText(tr("Connect")); - ui->serverEdit->setEnabled(true); - ui->userEdit->setEnabled(true); - ui->chatWidget->clearUsers(); - hostModsMap.clear(); - updateMods(); - socketLobby.disconnectServer(); - } -} - -void Lobby::updateMods() -{ - ui->modsList->clear(); - if(hostModsMap.empty()) - return; - - auto createModListWidget = [](const QIcon & icon, const QString & label, const QString & name, bool enableFlag, bool resolveFlag) - { - auto * lw = new QListWidgetItem(icon, label); - lw->setData(ModResolutionRoles::ModNameRole, name); - lw->setData(ModResolutionRoles::ModEnableRole, enableFlag); - lw->setData(ModResolutionRoles::ModResolvableRole, resolveFlag); - return lw; - }; - - auto enabledMods = buildModsMap(); - for(const auto & mod : hostModsMap.keys()) - { - auto & modValue = hostModsMap[mod]; - auto modName = QString("%1 (v%2)").arg(mod, modValue); - if(enabledMods.contains(mod)) - { - if(enabledMods[mod] == modValue) - enabledMods.remove(mod); //mod fully matches, remove from list - else - { - //mod version mismatch - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-update.png"), modName, mod, true, false)); - } - } - else if(isModAvailable(mod, modValue)) - { - //mod is available and needs to be enabled - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-enabled.png"), modName, mod, true, true)); - } - else - { - //mod is not available and needs to be installed - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-delete.png"), modName, mod, true, false)); - } - } - for(const auto & remainMod : enabledMods.keys()) - { - auto modName = QString("%1 (v%2)").arg(remainMod, enabledMods[remainMod]); - //mod needs to be disabled - ui->modsList->addItem(createModListWidget(QIcon("icons:mod-disabled.png"), modName, remainMod, false, true)); - } - if(!ui->modsList->count()) - { - ui->buttonResolve->setEnabled(false); - ui->modsList->addItem(tr("No issues detected")); - } - else - { - ui->buttonResolve->setEnabled(true); - } -} - -void Lobby::on_newButton_clicked() -{ - new LobbyRoomRequest(socketLobby, "", buildModsMap(), this); -} - -void Lobby::on_joinButton_clicked() -{ - auto * item = ui->sessionsTable->item(ui->sessionsTable->currentRow(), 0); - if(item) - { - auto isPrivate = ui->sessionsTable->item(ui->sessionsTable->currentRow(), 2)->data(Qt::UserRole).toBool(); - if(isPrivate) - new LobbyRoomRequest(socketLobby, item->text(), buildModsMap(), this); - else - socketLobby.requestJoinSession(item->text(), "", buildModsMap()); - } -} - -void Lobby::on_buttonLeave_clicked() -{ - socketLobby.requestLeaveSession(session); -} - -void Lobby::on_buttonReady_clicked() -{ - if(ui->buttonReady->text() == "Ready") - ui->buttonReady->setText("Not ready"); - else - ui->buttonReady->setText("Ready"); - socketLobby.requestReadySession(session); -} - -void Lobby::on_sessionsTable_itemSelectionChanged() -{ - auto selection = ui->sessionsTable->selectedItems(); - ui->joinButton->setEnabled(!selection.empty()); -} - -void Lobby::on_playersList_currentRowChanged(int currentRow) -{ - ui->kickButton->setVisible(ui->playersList->currentItem() - && currentRow > 0 - && ui->playersList->currentItem()->text() != username); -} - -void Lobby::on_kickButton_clicked() -{ - if(ui->playersList->currentItem() && ui->playersList->currentItem()->text() != username) - socketLobby.send(ProtocolStrings[KICK].arg(ui->playersList->currentItem()->text())); -} - - -void Lobby::on_buttonResolve_clicked() -{ - QStringList toEnableList, toDisableList; - auto items = ui->modsList->selectedItems(); - if(items.empty()) - { - for(int i = 0; i < ui->modsList->count(); ++i) - items.push_back(ui->modsList->item(i)); - } - - for(auto * item : items) - { - auto modName = item->data(ModResolutionRoles::ModNameRole); - if(modName.isNull()) - continue; - - bool modToEnable = item->data(ModResolutionRoles::ModEnableRole).toBool(); - bool modToResolve = item->data(ModResolutionRoles::ModResolvableRole).toBool(); - - if(!modToResolve) - continue; - - if(modToEnable) - toEnableList << modName.toString(); - else - toDisableList << modName.toString(); - } - - //disabling first, then enabling - for(auto & mod : toDisableList) - emit disableMod(mod); - for(auto & mod : toEnableList) - emit enableMod(mod); -} - -void Lobby::on_optNewGame_toggled(bool checked) -{ - if(checked) - { - if(isLoadGameMode) - socketLobby.send(ProtocolStrings[HOSTMODE].arg(GameMode::NEW_GAME)); - } -} - -void Lobby::on_optLoadGame_toggled(bool checked) -{ - if(checked) - { - if(!isLoadGameMode) - socketLobby.send(ProtocolStrings[HOSTMODE].arg(GameMode::LOAD_GAME)); - } -} - -void Lobby::onMessageSent(QString message) -{ - socketLobby.send(ProtocolStrings[MESSAGE].arg(message)); -} - -void Lobby::onChannelSwitch(QString channel) -{ - socketLobby.send(ProtocolStrings[SETCHANNEL].arg(channel)); -} diff --git a/launcher/lobby/lobby_moc.h b/launcher/lobby/lobby_moc.h deleted file mode 100644 index 23a8ddac1..000000000 --- a/launcher/lobby/lobby_moc.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * lobby_moc.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once -#include -#include "lobby.h" - -namespace Ui { -class Lobby; -} - -class Lobby : public QWidget -{ - Q_OBJECT - - void changeEvent(QEvent *event) override; -public: - explicit Lobby(QWidget *parent = nullptr); - ~Lobby(); - -signals: - - void enableMod(QString mod); - void disableMod(QString mod); - -public slots: - void updateMods(); - -private slots: - void dispatchMessage(QString); - void serverCommand(const ServerCommand &); - void onMessageSent(QString message); - void onChannelSwitch(QString channel); - - void on_connectButton_toggled(bool checked); - - void on_newButton_clicked(); - - void on_joinButton_clicked(); - - void on_buttonLeave_clicked(); - - void on_buttonReady_clicked(); - - void onDisconnected(); - - void on_sessionsTable_itemSelectionChanged(); - - void on_playersList_currentRowChanged(int currentRow); - - void on_kickButton_clicked(); - - void on_buttonResolve_clicked(); - - void on_optNewGame_toggled(bool checked); - - void on_optLoadGame_toggled(bool checked); - -private: - QString serverUrl; - int serverPort; - bool isLoadGameMode = false; - - Ui::Lobby *ui; - SocketLobby socketLobby; - QString hostSession; - QString session; - QString username; - QStringList gameArgs; - QMap hostModsMap; - QMap> clientsModsMap; - - enum AuthStatus - { - AUTH_NONE, AUTH_OK, AUTH_ERROR - }; - - AuthStatus authentificationStatus = AUTH_NONE; - -private: - QMap buildModsMap() const; - bool isModAvailable(const QString & modName, const QString & modVersion) const; - - - void protocolAssert(bool); -}; diff --git a/launcher/lobby/lobby_moc.ui b/launcher/lobby/lobby_moc.ui deleted file mode 100644 index 07406b877..000000000 --- a/launcher/lobby/lobby_moc.ui +++ /dev/null @@ -1,311 +0,0 @@ - - - Lobby - - - - 0 - 0 - 652 - 383 - - - - - - - - 0 - - - - - - 0 - 0 - - - - Username - - - - - - - - 0 - 0 - - - - Connect - - - true - - - - - - - 127.0.0.1:5002 - - - - - - - Server - - - - - - - - - - -1 - - - 0 - - - - - 0 - - - 10 - - - 0 - - - - - - 0 - 0 - - - - - - - - - - - 0 - 0 - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - false - - - New room - - - - - - - false - - - Join room - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - false - - - 80 - - - false - - - true - - - 20 - - - 20 - - - - Session - - - - - Players - - - - - - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - -1 - - - - - Kick player - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - - - - Players in the room - - - - - - - Leave - - - - - - - Mods mismatch - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::MultiSelection - - - - - - - Ready - - - - - - - Resolve - - - - - - - 0 - - - - - New game - - - - - - - Load game - - - - - - - - - - - - - - - - Chat - QWidget -
lobby/chat_moc.h
- 1 -
-
- - -
diff --git a/launcher/lobby/lobbyroomrequest_moc.cpp b/launcher/lobby/lobbyroomrequest_moc.cpp deleted file mode 100644 index 62c2496db..000000000 --- a/launcher/lobby/lobbyroomrequest_moc.cpp +++ /dev/null @@ -1,59 +0,0 @@ -/* - * lobbyroomrequest_moc.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "lobbyroomrequest_moc.h" -#include "ui_lobbyroomrequest_moc.h" - -LobbyRoomRequest::LobbyRoomRequest(SocketLobby & socket, const QString & room, const QMap & mods, QWidget *parent) : - QDialog(parent), - ui(new Ui::LobbyRoomRequest), - socketLobby(socket), - mods(mods) -{ - ui->setupUi(this); - ui->nameEdit->setText(room); - if(!room.isEmpty()) - { - ui->nameEdit->setReadOnly(true); - ui->totalPlayers->setEnabled(false); - } - - show(); -} - -void LobbyRoomRequest::changeEvent(QEvent *event) -{ - if(event->type() == QEvent::LanguageChange) - { - ui->retranslateUi(this); - } - QDialog::changeEvent(event); -} - -LobbyRoomRequest::~LobbyRoomRequest() -{ - delete ui; -} - -void LobbyRoomRequest::on_buttonBox_accepted() -{ - if(ui->nameEdit->isReadOnly()) - { - socketLobby.requestJoinSession(ui->nameEdit->text(), ui->passwordEdit->text(), mods); - } - else - { - if(!ui->nameEdit->text().isEmpty()) - { - int totalPlayers = ui->totalPlayers->currentIndex() + 2; //where 2 is a minimum amount of players - socketLobby.requestNewSession(ui->nameEdit->text(), totalPlayers, ui->passwordEdit->text(), mods); - } - } -} - diff --git a/launcher/lobby/lobbyroomrequest_moc.h b/launcher/lobby/lobbyroomrequest_moc.h deleted file mode 100644 index eff7161e4..000000000 --- a/launcher/lobby/lobbyroomrequest_moc.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * lobbyroomrequest_moc.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#ifndef LOBBYROOMREQUEST_MOC_H -#define LOBBYROOMREQUEST_MOC_H - -#include -#include "lobby.h" - -namespace Ui { -class LobbyRoomRequest; -} - -class LobbyRoomRequest : public QDialog -{ - Q_OBJECT - - void changeEvent(QEvent *event) override; -public: - explicit LobbyRoomRequest(SocketLobby & socket, const QString & room, const QMap & mods, QWidget *parent = nullptr); - ~LobbyRoomRequest(); - -private slots: - void on_buttonBox_accepted(); - -private: - Ui::LobbyRoomRequest *ui; - SocketLobby & socketLobby; - QMap mods; -}; - -#endif // LOBBYROOMREQUEST_MOC_H diff --git a/launcher/lobby/lobbyroomrequest_moc.ui b/launcher/lobby/lobbyroomrequest_moc.ui deleted file mode 100644 index 5b8db9b7b..000000000 --- a/launcher/lobby/lobbyroomrequest_moc.ui +++ /dev/null @@ -1,151 +0,0 @@ - - - LobbyRoomRequest - - - Qt::WindowModal - - - - 0 - 0 - 227 - 188 - - - - Room settings - - - - - - false - - - true - - - - - - Room name - - - - - - - - - - Maximum players - - - - - - - - 0 - 0 - - - - 2 - - - - 2 - - - - - 3 - - - - - 4 - - - - - 5 - - - - - 6 - - - - - 7 - - - - - 8 - - - - - - - - Password (optional) - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - rejected() - LobbyRoomRequest - reject() - - - 316 - 260 - - - 286 - 274 - - - - - buttonBox - accepted() - LobbyRoomRequest - accept() - - - 248 - 254 - - - 157 - 274 - - - - - diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 55389163d..882925e34 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -53,7 +53,6 @@ void MainWindow::computeSidePanelSizes() QVector widgets = { ui->modslistButton, ui->settingsButton, - ui->lobbyButton, ui->aboutButton, ui->startEditorButton, ui->startGameButton @@ -86,10 +85,6 @@ MainWindow::MainWindow(QWidget * parent) ui->setupUi(this); - connect(ui->lobbyView, &Lobby::enableMod, ui->modlistView, &CModListView::enableModByName); - connect(ui->lobbyView, &Lobby::disableMod, ui->modlistView, &CModListView::disableModByName); - connect(ui->modlistView, &CModListView::modsChanged, ui->lobbyView, &Lobby::updateMods); - //load window settings QSettings s(Ui::teamName, Ui::appName); @@ -136,14 +131,14 @@ void MainWindow::detectPreferredLanguage() for (auto const & vcmiLang : Languages::getLanguageList()) if (vcmiLang.tagIETF == userLang.toStdString()) selectedLanguage = vcmiLang.identifier; - } - logGlobal->info("Selected language: %s", selectedLanguage); - - if (!selectedLanguage.empty()) - { - Settings node = settings.write["general"]["language"]; - node->String() = selectedLanguage; + if (!selectedLanguage.empty()) + { + logGlobal->info("Selected language: %s", selectedLanguage); + Settings node = settings.write["general"]["language"]; + node->String() = selectedLanguage; + return; + } } } @@ -151,7 +146,6 @@ void MainWindow::enterSetup() { ui->startGameButton->setEnabled(false); ui->startEditorButton->setEnabled(false); - ui->lobbyButton->setEnabled(false); ui->settingsButton->setEnabled(false); ui->aboutButton->setEnabled(false); ui->modslistButton->setEnabled(false); @@ -165,7 +159,6 @@ void MainWindow::exitSetup() ui->startGameButton->setEnabled(true); ui->startEditorButton->setEnabled(true); - ui->lobbyButton->setEnabled(true); ui->settingsButton->setEnabled(true); ui->aboutButton->setEnabled(true); ui->modslistButton->setEnabled(true); @@ -228,12 +221,6 @@ void MainWindow::on_settingsButton_clicked() ui->tabListWidget->setCurrentIndex(TabRows::SETTINGS); } -void MainWindow::on_lobbyButton_clicked() -{ - ui->startGameButton->setEnabled(false); - ui->tabListWidget->setCurrentIndex(TabRows::LOBBY); -} - void MainWindow::on_aboutButton_clicked() { ui->startGameButton->setEnabled(true); diff --git a/launcher/mainwindow_moc.h b/launcher/mainwindow_moc.h index e9ff8d075..14d26c712 100644 --- a/launcher/mainwindow_moc.h +++ b/launcher/mainwindow_moc.h @@ -38,9 +38,8 @@ private: { MODS = 0, SETTINGS = 1, - LOBBY = 2, - SETUP = 3, - ABOUT = 4, + SETUP = 2, + ABOUT = 3, }; void changeEvent(QEvent *event) override; @@ -65,7 +64,6 @@ public slots: private slots: void on_modslistButton_clicked(); void on_settingsButton_clicked(); - void on_lobbyButton_clicked(); void on_startEditorButton_clicked(); void on_aboutButton_clicked(); }; diff --git a/launcher/mainwindow_moc.ui b/launcher/mainwindow_moc.ui index 1208bbe04..2f7a56934 100644 --- a/launcher/mainwindow_moc.ui +++ b/launcher/mainwindow_moc.ui @@ -133,56 +133,6 @@ - - - - - 1 - 10 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Lobby - - - - icons:menu-lobby.pngicons:menu-lobby.png - - - - 64 - 64 - - - - true - - - false - - - true - - - Qt::ToolButtonTextUnderIcon - - - true - - - @@ -370,7 +320,6 @@ - @@ -392,12 +341,6 @@
settingsView/csettingsview_moc.h
1 - - Lobby - QWidget -
lobby/lobby_moc.h
- 1 -
FirstLaunchView QWidget diff --git a/launcher/modManager/cdownloadmanager_moc.cpp b/launcher/modManager/cdownloadmanager_moc.cpp index a0e3eddaa..563ce0b7e 100644 --- a/launcher/modManager/cdownloadmanager_moc.cpp +++ b/launcher/modManager/cdownloadmanager_moc.cpp @@ -54,9 +54,7 @@ CDownloadManager::FileEntry & CDownloadManager::getEntry(QNetworkReply * reply) if(entry.reply == reply) return entry; } - assert(0); - static FileEntry errorValue; - return errorValue; + throw std::runtime_error("Failed to find download entry"); } void CDownloadManager::downloadFinished(QNetworkReply * reply) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index da18609fb..e1a516e27 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -11,7 +11,6 @@ #include "cmodlist.h" #include "../lib/CConfigHandler.h" -#include "../../lib/JsonNode.h" #include "../../lib/filesystem/CFileInputStream.h" #include "../../lib/GameConstants.h" #include "../../lib/modding/CModVersion.h" @@ -38,6 +37,9 @@ bool CModEntry::isEnabled() const if(!isInstalled()) return false; + if (!isVisible()) + return false; + return modSettings["active"].toBool(); } @@ -105,7 +107,7 @@ bool CModEntry::isVisible() const return false; } - return !localData.isEmpty() || !repository.isEmpty(); + return !localData.isEmpty() || (!repository.isEmpty() && !repository.contains("mod")); } bool CModEntry::isTranslation() const @@ -141,6 +143,22 @@ QVariant CModEntry::getValue(QString value) const return getValueImpl(value, true); } +QStringList CModEntry::getDependencies() const +{ + QStringList result; + for (auto const & entry : getValue("depends").toStringList()) + result.push_back(entry.toLower()); + return result; +} + +QStringList CModEntry::getConflicts() const +{ + QStringList result; + for (auto const & entry : getValue("conflicts").toStringList()) + result.push_back(entry.toLower()); + return result; +} + QVariant CModEntry::getBaseValue(QString value) const { return getValueImpl(value, false); @@ -341,8 +359,8 @@ QStringList CModList::getRequirements(QString modname) { auto mod = getMod(modname); - for(auto entry : mod.getValue("depends").toStringList()) - ret += getRequirements(entry); + for(auto entry : mod.getDependencies()) + ret += getRequirements(entry.toLower()); } ret += modname; diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index 41b2f0fae..05aaa4f6b 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -76,6 +76,9 @@ public: QVariant getValue(QString value) const; QVariant getBaseValue(QString value) const; + QStringList getDependencies() const; + QStringList getConflicts() const; + static QString sizeToString(double size); }; diff --git a/launcher/modManager/cmodlistmodel_moc.cpp b/launcher/modManager/cmodlistmodel_moc.cpp index cece26b53..5ec35e400 100644 --- a/launcher/modManager/cmodlistmodel_moc.cpp +++ b/launcher/modManager/cmodlistmodel_moc.cpp @@ -38,7 +38,7 @@ QString CModListModel::modIndexToName(const QModelIndex & index) const QString CModListModel::modTypeName(QString modTypeID) const { - static QMap modTypes = { + static const QMap modTypes = { {"Translation", tr("Translation")}, {"Town", tr("Town") }, {"Test", tr("Test") }, diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 0e713bfda..528c448cc 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -48,6 +48,43 @@ void CModListView::changeEvent(QEvent *event) QWidget::changeEvent(event); } +void CModListView::dragEnterEvent(QDragEnterEvent* event) +{ + if(event->mimeData()->hasUrls()) + for(const auto & url : event->mimeData()->urls()) + for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp"})) + if(url.fileName().endsWith(ending, Qt::CaseInsensitive)) + { + event->acceptProposedAction(); + return; + } +} + +void CModListView::dropEvent(QDropEvent* event) +{ + const QMimeData* mimeData = event->mimeData(); + + if(mimeData->hasUrls()) + { + const QList urlList = mimeData->urls(); + + for (const auto & url : urlList) + { + QString urlStr = url.toString(); + QString fileName = url.fileName(); + if(urlStr.endsWith(".zip", Qt::CaseInsensitive)) + downloadFile(fileName.toLower() + // mod name currently comes from zip file -> remove suffixes from github zip download + .replace(QRegularExpression("-[0-9a-f]{40}"), "") + .replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip") + .replace("-main.zip", ".zip") + , urlStr, "mods", 0); + else + downloadFile(fileName, urlStr, "mods", 0); + } + } +} + void CModListView::setupFilterModel() { filterModel = new CModFilterModel(modModel, this); @@ -100,6 +137,8 @@ CModListView::CModListView(QWidget * parent) { ui->setupUi(this); + setAcceptDrops(true); + setupModModel(); setupFilterModel(); setupModsView(); @@ -218,12 +257,12 @@ QStringList CModListView::getModNames(QStringList input) for(const auto & modID : input) { - auto mod = modModel->getMod(modID); + auto mod = modModel->getMod(modID.toLower()); QString modName = mod.getValue("name").toString(); if (modName.isEmpty()) - result += modID; + result += modID.toLower(); else result += modName; } @@ -311,9 +350,9 @@ QString CModListView::genModInfoText(CModEntry & mod) if(needToShowSupportedLanguages) result += replaceIfNotEmpty(supportedLanguages, lineTemplate.arg(tr("Languages"))); - result += replaceIfNotEmpty(getModNames(mod.getValue("depends").toStringList()), lineTemplate.arg(tr("Required mods"))); - result += replaceIfNotEmpty(getModNames(mod.getValue("conflicts").toStringList()), lineTemplate.arg(tr("Conflicting mods"))); - result += replaceIfNotEmpty(getModNames(mod.getValue("description").toStringList()), textTemplate.arg(tr("Description"))); + result += replaceIfNotEmpty(getModNames(mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); + result += replaceIfNotEmpty(getModNames(mod.getConflicts()), lineTemplate.arg(tr("Conflicting mods"))); + result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg(tr("Description"))); result += "

"; // to get some empty space @@ -464,7 +503,7 @@ QStringList CModListView::findBlockingMods(QString modUnderTest) if(mod.isEnabled()) { // one of enabled mods have requirement (or this mod) marked as conflict - for(auto conflict : mod.getValue("conflicts").toStringList()) + for(auto conflict : mod.getConflicts()) { if(required.contains(conflict)) ret.push_back(name); @@ -482,10 +521,10 @@ QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled) { auto current = modModel->getMod(modName); - if(!current.isInstalled()) + if(!current.isInstalled() || !current.isVisible()) continue; - if(current.getValue("depends").toStringList().contains(mod)) + if(current.getDependencies().contains(mod, Qt::CaseInsensitive)) { if(!(current.isDisabled() && excludeDisabled)) ret += modName; @@ -591,7 +630,7 @@ void CModListView::downloadFile(QString file, QString url, QString description, this, SLOT(downloadFinished(QStringList,QStringList,QStringList))); connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)), - this, SLOT(downloadProgress(qint64,qint64))); + this, SLOT(extractionProgress(qint64,qint64))); connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); @@ -613,6 +652,14 @@ void CModListView::downloadProgress(qint64 current, qint64 max) ui->progressBar->setValue(current / (1024 * 1024)); } +void CModListView::extractionProgress(qint64 current, qint64 max) +{ + // display progress, in extracted files + ui->progressBar->setVisible(true); + ui->progressBar->setMaximum(max); + ui->progressBar->setValue(current); +} + void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors) { QString title = tr("Download failed"); @@ -669,15 +716,18 @@ void CModListView::hideProgressBar() void CModListView::installFiles(QStringList files) { QStringList mods; + QStringList maps; QStringList images; QVector repositories; // TODO: some better way to separate zip's with mods and downloaded repository files for(QString filename : files) { - if(filename.endsWith(".zip")) + if(filename.endsWith(".zip", Qt::CaseInsensitive)) mods.push_back(filename); - if(filename.endsWith(".json")) + else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive)) + maps.push_back(filename); + else if(filename.endsWith(".json", Qt::CaseInsensitive)) { //download and merge additional files auto repoData = JsonUtils::JsonFromFile(filename).toMap(); @@ -701,7 +751,7 @@ void CModListView::installFiles(QStringList files) } repositories.push_back(repoData); } - if(filename.endsWith(".png")) + else if(filename.endsWith(".png", Qt::CaseInsensitive)) images.push_back(filename); } @@ -710,6 +760,9 @@ void CModListView::installFiles(QStringList files) if(!mods.empty()) installMods(mods); + if(!maps.empty()) + installMaps(maps); + if(!images.empty()) loadScreenshots(); } @@ -786,6 +839,16 @@ void CModListView::installMods(QStringList archives) QFile::remove(archive); } +void CModListView::installMaps(QStringList maps) +{ + QString destDir = CLauncherDirs::get().mapsPath() + "/"; + + for(QString map : maps) + { + QFile(map).rename(destDir + map.section('/', -1, -1)); + } +} + void CModListView::on_refreshButton_clicked() { loadRepositories(); @@ -844,7 +907,7 @@ void CModListView::loadScreenshots() { // managed to load cached image QIcon icon(pixmap); - QListWidgetItem * item = new QListWidgetItem(icon, QString(tr("Screenshot %1")).arg(ui->screenshotsList->count() + 1)); + auto * item = new QListWidgetItem(icon, QString(tr("Screenshot %1")).arg(ui->screenshotsList->count() + 1)); ui->screenshotsList->addItem(item); } } @@ -955,4 +1018,3 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) return; } } - diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index cadfb0a51..2edbab3a8 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -54,12 +54,15 @@ class CModListView : public QWidget void downloadFile(QString file, QString url, QString description, qint64 size = 0); void installMods(QStringList archives); + void installMaps(QStringList maps); void installFiles(QStringList mods); QString genChangelogText(CModEntry & mod); QString genModInfoText(CModEntry & mod); void changeEvent(QEvent *event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent *event) override; signals: void modsChanged(); @@ -98,6 +101,7 @@ private slots: void dataChanged(const QModelIndex & topleft, const QModelIndex & bottomRight); void modSelected(const QModelIndex & current, const QModelIndex & previous); void downloadProgress(qint64 current, qint64 max); + void extractionProgress(qint64 current, qint64 max); void downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors); void modelReset(); void hideProgressBar(); diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 8e9ed4ee3..959a429c5 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -24,7 +24,9 @@ namespace { QString detectModArchive(QString path, QString modName, std::vector & filesToExtract) { - filesToExtract = ZipArchive::listFiles(qstringToPath(path)); + ZipArchive archive(qstringToPath(path)); + + filesToExtract = archive.listFiles(); QString modDirName; @@ -159,9 +161,6 @@ bool CModManager::canInstallMod(QString modname) if(mod.isInstalled()) return addError(modname, "Mod is already installed"); - - if(!mod.isAvailable()) - return addError(modname, "Mod is not available"); return true; } @@ -192,7 +191,7 @@ bool CModManager::canEnableMod(QString modname) if(!mod.isCompatible()) return addError(modname, "Mod is not compatible, please update VCMI and checkout latest mod revisions"); - for(auto modEntry : mod.getValue("depends").toStringList()) + for(auto modEntry : mod.getDependencies()) { if(!modList->hasMod(modEntry)) // required mod is not available return addError(modname, QString("Required mod %1 is missing").arg(modEntry)); @@ -205,11 +204,11 @@ bool CModManager::canEnableMod(QString modname) auto mod = modList->getMod(modEntry); // "reverse conflict" - enabled mod has this one as conflict - if(mod.isEnabled() && mod.getValue("conflicts").toStringList().contains(modname)) + if(mod.isEnabled() && mod.getConflicts().contains(modname)) return addError(modname, QString("This mod conflicts with %1").arg(modEntry)); } - for(auto modEntry : mod.getValue("conflicts").toStringList()) + for(auto modEntry : mod.getConflicts()) { // check if conflicting mod installed and enabled if(modList->hasMod(modEntry) && modList->getMod(modEntry).isEnabled()) @@ -232,7 +231,7 @@ bool CModManager::canDisableMod(QString modname) { auto current = modList->getMod(modEntry); - if(current.getValue("depends").toStringList().contains(modname) && current.isEnabled()) + if(current.getDependencies().contains(modname) && current.isEnabled()) return addError(modname, QString("This mod is needed to run %1").arg(modEntry)); } return true; @@ -285,14 +284,23 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) if(!modDirName.size()) return addError(modname, "Mod archive is invalid or corrupted"); - auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesToExtract]() + std::atomic filesCounter = 0; + + auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesCounter, &filesToExtract]() { - return ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir), filesToExtract); + ZipArchive archive(qstringToPath(archivePath)); + for (auto const & file : filesToExtract) + { + if (!archive.extract(qstringToPath(destDir), file)) + return false; + ++filesCounter; + } + return true; }); - while(futureExtract.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready) + while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) { - emit extractionProgress(0, 0); + emit extractionProgress(filesCounter, filesToExtract.size()); qApp->processEvents(); } diff --git a/launcher/modManager/imageviewer_moc.cpp b/launcher/modManager/imageviewer_moc.cpp index afd46b134..6d2ffbd7b 100644 --- a/launcher/modManager/imageviewer_moc.cpp +++ b/launcher/modManager/imageviewer_moc.cpp @@ -43,7 +43,7 @@ void ImageViewer::showPixmap(QPixmap & pixmap, QWidget * parent) { assert(!pixmap.isNull()); - ImageViewer * iw = new ImageViewer(parent); + auto * iw = new ImageViewer(parent); QSize size = pixmap.size(); size.scale(iw->calculateWindowSize(), Qt::KeepAspectRatio); diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index 5ac6648c2..fa5c97be6 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -90,7 +90,9 @@ void CSettingsView::loadSettings() ui->comboBoxFriendlyAI->setCurrentText(QString::fromStdString(settings["server"]["friendlyAI"].String())); ui->comboBoxNeutralAI->setCurrentText(QString::fromStdString(settings["server"]["neutralAI"].String())); ui->comboBoxEnemyAI->setCurrentText(QString::fromStdString(settings["server"]["enemyAI"].String())); + ui->comboBoxEnemyPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["playerAI"].String())); + ui->comboBoxAlliedPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["alliedAI"].String())); ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Integer()); @@ -115,6 +117,7 @@ void CSettingsView::loadSettings() ui->lineEditAutoSavePrefix->setEnabled(settings["general"]["useSavePrefix"].Bool()); Languages::fillLanguages(ui->comboBoxLanguage, false); + fillValidRenderers(); std::string cursorType = settings["video"]["cursor"].String(); size_t cursorTypeIndex = boost::range::find(cursorTypesList, cursorType) - cursorTypesList; @@ -161,6 +164,26 @@ void CSettingsView::fillValidScalingRange() #ifndef VCMI_MOBILE +static QStringList getAvailableRenderingDrivers() +{ + SDL_Init(SDL_INIT_VIDEO); + QStringList result; + + result += QString(); // empty value for autoselection + + int driversCount = SDL_GetNumRenderDrivers(); + + for(int it = 0; it < driversCount; it++) + { + SDL_RendererInfo info; + if (SDL_GetRenderDriverInfo(it, &info) == 0) + result += QString::fromLatin1(info.name); + } + + SDL_Quit(); + return result; +} + static QVector findAvailableResolutions(int displayIndex) { // Ugly workaround since we don't actually need SDL in Launcher @@ -195,13 +218,13 @@ static QVector findAvailableResolutions(int displayIndex) void CSettingsView::fillValidResolutionsForScreen(int screenIndex) { - ui->comboBoxResolution->blockSignals(true); // avoid saving wrong resolution after adding first item from the list + QSignalBlocker guard(ui->comboBoxResolution); // avoid saving wrong resolution after adding first item from the list + ui->comboBoxResolution->clear(); bool fullscreen = settings["video"]["fullscreen"].Bool(); bool realFullscreen = settings["video"]["realFullscreen"].Bool(); - if (!fullscreen || realFullscreen) { QVector resolutions = findAvailableResolutions(screenIndex); @@ -223,8 +246,21 @@ void CSettingsView::fillValidResolutionsForScreen(int screenIndex) // if selected resolution no longer exists, force update value to the largest (last) resolution if(resIndex == -1) ui->comboBoxResolution->setCurrentIndex(ui->comboBoxResolution->count() - 1); +} - ui->comboBoxResolution->blockSignals(false); +void CSettingsView::fillValidRenderers() +{ + QSignalBlocker guard(ui->comboBoxRendererType); // avoid saving wrong renderer after adding first item from the list + + ui->comboBoxRendererType->clear(); + + auto driversList = getAvailableRenderingDrivers(); + ui->comboBoxRendererType->addItems(driversList); + + std::string rendererName = settings["video"]["driver"].String(); + + int index = ui->comboBoxRendererType->findText(QString::fromStdString(rendererName)); + ui->comboBoxRendererType->setCurrentIndex(index); } #else void CSettingsView::fillValidResolutionsForScreen(int screenIndex) @@ -233,6 +269,13 @@ void CSettingsView::fillValidResolutionsForScreen(int screenIndex) ui->comboBoxResolution->hide(); ui->labelResolution->hide(); } + +void CSettingsView::fillValidRenderers() +{ + // untested on mobile platforms + ui->comboBoxRendererType->hide(); + ui->labelRendererType->hide(); +} #endif CSettingsView::CSettingsView(QWidget * parent) @@ -354,25 +397,6 @@ void CSettingsView::on_comboBoxCursorType_currentIndexChanged(int index) node->String() = cursorTypesList[index]; } -void CSettingsView::on_listWidgetSettings_currentRowChanged(int currentRow) -{ - QVector targetWidgets = { - ui->labelGeneral, - ui->labelVideo, - ui->labelArtificialIntelligence, - ui->labelRepositories - }; - - QWidget * currentTarget = targetWidgets[currentRow]; - - // We want to scroll in a way that will put target widget in topmost visible position - // To show not just header, but all settings in this group as well - // In order to do that, let's scroll to the very bottom and the scroll back up until target widget is visible - int maxPosition = ui->settingsScrollArea->verticalScrollBar()->maximum(); - ui->settingsScrollArea->verticalScrollBar()->setValue(maxPosition); - ui->settingsScrollArea->ensureWidgetVisible(currentTarget, 5, 5); -} - void CSettingsView::loadTranslation() { Languages::fillLanguages(ui->comboBoxLanguageBase, true); @@ -540,3 +564,10 @@ void CSettingsView::on_spinBoxReservedArea_valueChanged(int arg1) node->Float() = float(arg1) / 100; // percentage -> ratio } + +void CSettingsView::on_comboBoxRendererType_currentTextChanged(const QString &arg1) +{ + Settings node = settings.write["video"]["driver"]; + node->String() = arg1.toStdString(); +} + diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 0a144f7f5..eb557bd7e 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -45,7 +45,6 @@ private slots: void on_comboBoxAutoSave_currentIndexChanged(int index); void on_comboBoxLanguage_currentIndexChanged(int index); void on_comboBoxCursorType_currentIndexChanged(int index); - void on_listWidgetSettings_currentRowChanged(int currentRow); void on_pushButtonTranslation_clicked(); void on_comboBoxLanguageBase_currentIndexChanged(int index); @@ -76,9 +75,12 @@ private slots: void on_spinBoxReservedArea_valueChanged(int arg1); + void on_comboBoxRendererType_currentTextChanged(const QString &arg1); + private: Ui::CSettingsView * ui; + void fillValidRenderers(); void fillValidResolutionsForScreen(int screenIndex); void fillValidScalingRange(); QSize getPreferredRenderingResolution(); diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index deae0833c..240216df1 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -6,8 +6,8 @@ 0 0 - 832 - 350 + 985 + 683 @@ -26,65 +26,6 @@ 0 - - - - - 1 - 0 - - - - - 200 - 0 - - - - - true - - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - false - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SelectRows - - - 4 - - - - General - - - - - Video - - - - - Artificial Intelligence - - - - - Mod Repositories - - - - @@ -106,28 +47,105 @@ 0 - -356 - 610 - 873 + 0 + 969 + 818 - - + + + + + - Heroes III Translation + - - + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + 75 true - General + Video + + + + + + + + 75 + true + + + + Artificial Intelligence + + + + + + + Cursor + + + + + + + + + + Heroes III Data Language + + + + + + + + + + Display index + + + + + + + + + + + + + true @@ -138,72 +156,62 @@ - - + + - Interface Scaling + Heroes III Translation - - + + - Autosave prefix + Enemy AI in battles - + + + + Additional repository + + + + + + + + + + + + + + Show intro + + + + + + + + 75 + true + + + + Mod Repositories + + + + Adventure Map Allies - - - - - true - - - - Video - - - - - - - 20 - - - 1000 - - - 10 - - - - - - - - true - - - - Artificial Intelligence - - - - - - - empty = map name prefix - - - - + true @@ -216,27 +224,40 @@ - - + + - - - - - - - - Autosave limit (0 = off) + Interface Scaling - + + + 1 + + + + Off + + + + + On + + + - - - - Additional repository + + + + 50 + + + 400 + + + 10 @@ -259,56 +280,21 @@ - - - - Show intro + + + + 1024 + + + 65535 + + + 3030 - - - - 1 - - - - Off - - - - - On - - - - - - - - Enemy AI in battles - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - + + false @@ -327,10 +313,99 @@ - - + + - + + + + + + + + 20 + + + 1000 + + + 10 + + + + + + + + + + true + + + + + + + Reserved screen area + + + + + + + Resolution + + + + + + + VCMI Language + + + + + + + Framerate Limit + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + Neutral AI in battles + + + + + + + Friendly AI in battles + + + + + + + @@ -365,123 +440,10 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - + + - Display index - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - - - - - - - - Default repository - - - - - - - Heroes III Data Language - - - - - - - - - - true - - - - - - - Framerate Limit - - - - - - - Friendly AI in battles - - - - - - - VCMI Language - - - - - - - - - - true - - - - - - - - - - 50 - - - 400 - - - 10 - - - - - - - - - - - true - - - - Mod Repositories + Autosave @@ -492,84 +454,13 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
- - + + - Resolution + Autosave prefix - - - - 1024 - - - 65535 - - - 3030 - - - - - - - Autosave - - - - - - - Cursor - - - - - - - - Hardware - - - - - Software - - - - - - - - - - - Network port - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - @@ -587,59 +478,58 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + + + empty = map name prefix + + + + + + + + 75 + true + + - Refresh now + General - + - BattleAI + VCAI - BattleAI + VCAI - StupidAI + Nullkiller - - - - Check on startup - + + + + + Hardware + + + + + Software + + - - - - Neutral AI in battles - - - - - - - Reserved screen area - - - - - - - Adventure Map Enemies - - - - + 1 @@ -656,6 +546,16 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use + + + + Adventure Map Enemies + + + + + + @@ -663,10 +563,65 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + - + Check on startup + + + + + + + Autosave limit (0 = off) + + + + + + + Network port + + + + + + + Refresh now + + + + + + + Default repository + + + + + + + + + + Renderer diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 518ce01ed..0582a05c1 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -1,89 +1,89 @@ - + AboutProjectView VCMI on Discord - 访问VCMI的Discord + 访问VCMI的Discord Have a question? Found a bug? Want to help? Join us! - 有疑问?找到BUG?需要帮助?加入我们! + 有疑问?找到BUG?需要帮助?加入我们! VCMI on Github - 访问VCMI的GUTHUB + 访问VCMI的Github Our Community - + 联系社区 VCMI on Slack - 访问VCMI的Slack + 访问VCMI的Slack Build Information - + 编译信息 User data directory - 用户数据目录 + 用户数据目录 Open - 打开 + 打开 Check for updates - + 检查更新 Game version - + 游戏版本 Log files directory - 日志文件目录 + 日志文件目录 Data Directories - 数据目录 + 数据目录 Game data directory - + 游戏数据目录 Operating System - + 操作系统 Project homepage - + 项目主页 Report a bug - + 反馈bug @@ -121,7 +121,7 @@ Maps - + 地图 @@ -181,7 +181,7 @@ Compatibility - 兼容性 + 兼容性 @@ -219,6 +219,7 @@ All mods + Mod统一翻译为模组 所有模组 @@ -253,7 +254,7 @@ - + Description 详细介绍 @@ -303,284 +304,297 @@ 终止 - + Mod name - MOD名称 + 模组名称 - + Installed version 已安装的版本 - + Latest version 最新版本 - + Size - + 大小 - + Download size 下载大小 - + Authors 作者 - + License 授权许可 - + Contact 联系方式 - + Compatibility 兼容性 - - + + Required VCMI version 需要VCMI版本 - + Supported VCMI version 支持的VCMI版本 - + Supported VCMI versions 支持的VCMI版本 - + Languages 语言 - + Required mods - 前置MODs + Mod统一翻译为模组 + 前置模组 - + Conflicting mods - 冲突的MODs + Mod统一翻译为模组 + 冲突的模组 - + This mod can not be installed or enabled because the following dependencies are not present 这个模组无法被安装或者激活,因为下列依赖项未满足 - + This mod can not be enabled because the following mods are incompatible with it 这个模组无法被激活,因为下列模组与其不兼容 - + This mod cannot be disabled because it is required by the following mods 这个模组无法被禁用,因为它被下列模组所依赖 - + This mod cannot be uninstalled or updated because it is required by the following mods 这个模组无法被卸载或者更新,因为它被下列模组所依赖 - + This is a submod and it cannot be installed or uninstalled separately from its parent mod 这是一个附属模组它无法在所属模组外被直接被安装或者卸载 - + Notes 笔记注释 - + Downloading %s%. %p% (%v MB out of %m MB) finished - + 下载进度 %s%. %p% (%v MB 共 %m MB) 已完成 - + Download failed - + 下载失败 - + Unable to download all files. Encountered errors: - + 无法下载全部文件。 + +遇到问题: + + - + Install successfully downloaded? - - - - - Installing mod %1 - - - - - Operation failed - + + +安装下载成功的部分? - Encountered errors: - - + Installing mod %1 + 正在安装模组 %1 - + + Operation failed + 操作失败 + + + + Encountered errors: + + 遇到问题: + + + + Screenshot %1 截图 %1 - + Mod is incompatible - MOD不兼容 + Mod统一翻译为模组 + 模组不兼容 CSettingsView - - - + + + Off 关闭 - - + Artificial Intelligence 人工智能 - - + Mod Repositories 模组仓库 - + Interface Scaling - + 界面缩放 - + Neutral AI in battles - + 战场中立生物AI - + Enemy AI in battles - + 战场敌方玩家AI - + Additional repository - + 额外仓库 - + Adventure Map Allies - + 冒险地图友方玩家 - + Adventure Map Enemies - + 冒险地图敌方玩家 - + Windowed - + 窗口化 - + Borderless fullscreen - + 无边框全屏 - + Exclusive fullscreen - - - - - Autosave limit (0 = off) - - - - - Friendly AI in battles - - - - - Framerate Limit - - - - - Autosave prefix - - - - - empty = map name prefix - + 独占全屏 + Autosave limit (0 = off) + 自动保存限制 (0 = 不限制) + + + + Friendly AI in battles + 战场友方单位AI + + + + Framerate Limit + 帧率限制 + + + + Autosave prefix + 自动保存文件名前缀 + + + + empty = map name prefix + 空 = 地图名称前缀 + + + Refresh now - + 立即刷新 - + Default repository - + 默认仓库 - - - + + Renderer + 渲染器 + + + + + On 开启 - + Cursor 鼠标指针 - + Heroes III Data Language 英雄无敌3数据语言 - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -588,140 +602,116 @@ Windowed - game will run inside a window that covers part of your screen Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. - + 选择游戏的显示方式 + +窗口化 -游戏会运行在一个窗口内,该窗口只占据部分屏幕。 + +无边框全屏模式 - 游戏会运行在一个覆盖全部屏幕的窗口,使用你当前屏幕的分辨率。 + +独占全屏模式 - 游戏会运行在一个覆盖全部屏幕的窗口,使用和你选择的分辨率。 - + Reserved screen area - + 保留屏幕区域 - + Hardware 硬件 - + Software 软件 - + Heroes III Translation 发布版本里找不到这个项,不太清楚意义 英雄无敌3翻译 - + Check on startup 启动时检查更新 - + Fullscreen 全屏 - - + General 通用设置 - + VCMI Language VCMI语言 - + Resolution 分辨率 - + Autosave 自动存档 - + VSync - + 垂直同步 - + Display index 显示器序号 - + Network port 网络端口 - - + Video 视频设置 - + Show intro 显示开场动画 - + Active 激活 - + Disabled 禁用 - + Enable 启用 - + Not Installed 未安装 - + Install 安装 - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -799,7 +789,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Interface Improvements - + 界面改进 @@ -814,7 +804,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - + 安装提供各种各样界面改进的模组,例如美化随机地图界面或添加战场行动选项 @@ -834,7 +824,7 @@ Heroes® of Might and Magic® III HD is currently not supported! VCMI on Github - 访问VCMI的GUTHUB + 访问VCMI的Github @@ -891,7 +881,8 @@ Heroes® of Might and Magic® III HD is currently not supported! Install VCMI Mod Preset - 安装VCMI预设MOD + Mod统一翻译为模组 + 安装VCMI预设模组 @@ -908,6 +899,16 @@ Heroes® of Might and Magic® III HD is currently not supported! In The Wake of Gods 追随神迹 + + + Heroes III installation found! + 英雄无敌3安装目录已找到! + + + + Copy data to VCMI folder? + 复制数据到VCMI文件夹吗? + ImageViewer @@ -922,7 +923,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Czech - + @@ -937,7 +938,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Finnish - + @@ -952,12 +953,12 @@ Heroes® of Might and Magic® III HD is currently not supported! Hungarian - + Italian - + @@ -972,7 +973,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Portuguese - + @@ -987,12 +988,12 @@ Heroes® of Might and Magic® III HD is currently not supported! Swedish - + Turkish - + @@ -1002,7 +1003,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Vietnamese - + @@ -1025,118 +1026,6 @@ Heroes® of Might and Magic® III HD is currently not supported! 自动 (%1) - - Lobby - - - - Connect - 连接 - - - - Username - 用户名 - - - - Server - 服务器 - - - - Session - 会话 - - - - Players - 玩家 - - - - Resolve - 解决 - - - - New game - 新游戏 - - - - Load game - 加载游戏 - - - - New room - 新房间 - - - - Join room - 加入房间 - - - - Ready - 准备 - - - - Mods mismatch - MODs不匹配 - - - - Leave - 离开 - - - - Kick player - 踢出玩家 - - - - Players in the room - 大厅中的玩家 - - - - Disconnect - 断开 - - - - No issues detected - 没有发现问题 - - - - LobbyRoomRequest - - - Room settings - 房间设置 - - - - Room name - 房间名称 - - - - Maximum players - 最大玩家数 - - - - Password (optional) - 密码(可选) - - MainWindow @@ -1150,25 +1039,20 @@ Heroes® of Might and Magic® III HD is currently not supported! 设置 - + Help - + 帮助 - + Map Editor 地图编辑器 - + Start game 开始游戏 - - - Lobby - 大厅 - Mods diff --git a/launcher/translation/czech.ts b/launcher/translation/czech.ts new file mode 100644 index 000000000..38b38a4e9 --- /dev/null +++ b/launcher/translation/czech.ts @@ -0,0 +1,1073 @@ + + + + + AboutProjectView + + + VCMI on Discord + VCMI na Discordu + + + + Have a question? Found a bug? Want to help? Join us! + Máte otázku? Našli jste chybu? Chcete pomoct? Připojte se k nám! + + + + VCMI on Github + VCMI na GitHubu + + + + Our Community + Naše komunita + + + + VCMI on Slack + VCMI na Slacku + + + + Build Information + Informace o sestavení + + + + User data directory + Složka uživatelských dat + + + + + + Open + Otevřít + + + + Check for updates + Zkontrolovat aktualizace + + + + Game version + Verze hry + + + + Log files directory + Složka záznamů hry + + + + Data Directories + Složky dat + + + + Game data directory + Složka herních dat + + + + Operating System + Operační systém + + + + Project homepage + Domovská stránka projektu + + + + Report a bug + Nahlásit chybu + + + + CModListModel + + + Translation + Překlad + + + + Town + Město + + + + Test + Zkouška + + + + Templates + Šablony + + + + Spells + Kouzla + + + + Music + Hudba + + + + Maps + Mapy + + + + Sounds + Zvuky + + + + Skills + Schopnosti + + + + + Other + Ostatní + + + + Objects + Objekty + + + + + Mechanics + Mechaniky + + + + + Interface + Rozhraní + + + + Heroes + Hrdinové + + + + + Graphical + Grafika + + + + Expansion + Rozšíření + + + + Creatures + Bojovníci + + + + Compatibility + Kompabilita + + + + Artifacts + Artefakty + + + + AI + AI + + + + Name + Název + + + + Type + Druh + + + + Version + Verze + + + + CModListView + + + Filter + Filtrovat + + + + All mods + Všechny modifikace + + + + Downloadable + Stahovatelné + + + + Installed + Nainstalované + + + + Updatable + Aktualizovatelné + + + + Active + Aktivní + + + + Inactive + Neaktivní + + + + Download && refresh repositories + Stáhnout a aktualizovat repozitáře + + + + + Description + Popis + + + + Changelog + Seznam změn + + + + Screenshots + Snímky obrazovky + + + + Uninstall + Odinstalovat + + + + Enable + Povolit + + + + Disable + Zakázat + + + + Update + Aktualizovat + + + + Install + Instalovat + + + + %p% (%v KB out of %m KB) + %p% (%v KB z %m KB) + + + + Abort + Zrušit + + + + Mod name + Název modifikace + + + + Installed version + Nainstalovaná verze + + + + Latest version + Nejnovější verze + + + + Size + Velikost + + + + Download size + Velikost ke stažení + + + + Authors + Autoři + + + + License + Licence + + + + Contact + Kontakt + + + + Compatibility + Kompabilita + + + + + Required VCMI version + Vyžadovaná verze VCMI + + + + Supported VCMI version + Podporovaná verze VCMI + + + + Supported VCMI versions + Podporované verze VCMI + + + + Languages + Jazyky + + + + Required mods + Vyžadované modifikace VCMI + + + + Conflicting mods + Modifikace v kolizi + + + + This mod can not be installed or enabled because the following dependencies are not present + Tato modifikace nemůže být nainstalována nebo povolena, protože následující závislosti nejsou přítomny + + + + This mod can not be enabled because the following mods are incompatible with it + Tato modifikace nemůže být povolena, protože následující modifikace s ní nejsou kompatibilní + + + + This mod cannot be disabled because it is required by the following mods + Tato modifikace nemůže být zakázána, protože je vyžadována následujícími modifikacemi + + + + This mod cannot be uninstalled or updated because it is required by the following mods + Tato modifikace nemůže být odinstalována nebo aktualizována, protože je vyžadována následujícími modifikacemi + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod + Toto je podmodifikace, která nemůže být nainstalována nebo odinstalována bez její rodičovské modifikace + + + + Notes + Poznámky + + + + Downloading %s%. %p% (%v MB out of %m MB) finished + Stahování %s%. %p% (%v MB z %m MB) dokončeno + + + + Download failed + Stahování selhalo + + + + Unable to download all files. + +Encountered errors: + + + Nelze stáhnout všechny soubory. + +Vyskytly se chyby: + + + + + + + +Install successfully downloaded? + + +Nainstalovat úspěšně stažené? + + + + Installing mod %1 + Instalování modifikace %1 + + + + Operation failed + Operace selhala + + + + Encountered errors: + + Vyskytly se chyby: + + + + + Screenshot %1 + Snímek obrazovky %1 + + + + Mod is incompatible + Modifikace není kompatibilní + + + + CSettingsView + + + + + Off + Vypnuto + + + + Artificial Intelligence + Umělá inteligence + + + + Mod Repositories + Repozitáře modifikací + + + + Interface Scaling + Škálování rozhraní + + + + Neutral AI in battles + Neutrální AI v bitvách + + + + Enemy AI in battles + Nepřátelská AI v bitvách + + + + Additional repository + Další repozitáře + + + + Adventure Map Allies + Spojenci na mapě světa + + + + Adventure Map Enemies + Nepřátelé na mapě světa + + + + Windowed + V okně + + + + Borderless fullscreen + Celá obrazovka bez okrajů + + + + Exclusive fullscreen + Exkluzivní celá obrazovka + + + + Autosave limit (0 = off) + Limit aut. uložení (0=vypnuto) + + + + Friendly AI in battles + Přátelské AI v bitvách + + + + Framerate Limit + Omezení snímků za sekundu + + + + Autosave prefix + Předpona aut. uložení + + + + empty = map name prefix + prázná = předpona - název mapy + + + + Refresh now + Obnovit nyní + + + + Default repository + Výchozí repozitář + + + + Renderer + + + + + + + On + Zapnuto + + + + Cursor + Kurzor + + + + Heroes III Data Language + Jazyk dat Heroes III + + + + Select display mode for game + +Windowed - game will run inside a window that covers part of your screen + +Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. + +Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. + Vyberte režim zobrazení pro hru + +V okně - hra bude běžet v okně zakrývajícím část vaší obrazovky + +Celá obrazovka bez okrajů- hra poběží v okně, které zakryje vaši celou obrazovku se stejným rozlišením. + +Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybrané rozlišení. + + + + Reserved screen area + Vyhrazená část obrazovky + + + + Hardware + Hardware + + + + Software + Software + + + + Heroes III Translation + Překlad Heroes III + + + + Check on startup + Zkontrolovat při zapnutí + + + + Fullscreen + Celá obrazovka + + + + General + Všeobecné + + + + VCMI Language + Jazyk VCMI + + + + Resolution + Rozlišení + + + + Autosave + Automatické uložení + + + + VSync + VSync + + + + Display index + + + + + Network port + Síťový port + + + + Video + Zobrazení + + + + Show intro + Zobrazit intro + + + + Active + Aktivní + + + + Disabled + + + + + Enable + Povolit + + + + Not Installed + + + + + Install + Instalovat + + + + FirstLaunchView + + + Language + Jazyk + + + + Heroes III Data + Data Heroes III + + + + Mods Preset + Předvybrané modifikace + + + + Select your language + Vyberte váš jazyk + + + + Have a question? Found a bug? Want to help? Join us! + Máte otázku? Našli jste chybu? Chcete pomoct? Připojte se k nám! + + + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps that need to be completed. + +Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death. + +Heroes® of Might and Magic® III HD is currently not supported! + Děkujeme za instalaci VCMI! + +Před začátkem hraní musíte ještě dokončit pár kroků. + +Prosíme, mějte na paměti, že abyste mohli hrát VCMI, musíte vlastnit originální datové soubory Heroes® of Might and Magic® III: Complete nebo The Shadow of Death. + +Heroes® of Might and Magic® III HD není v současnosti podporovaný! + + + + Locate Heroes III data files + Najít soubory dat Heroes III + + + + If you don't have a copy of Heroes III installed, you can use our automatic installation tool 'vcmibuilder', which only requires the GoG.com Heroes III installer. Please visit our wiki for detailed instructions. + Pokud nemáte Heroes III nainstalované, můžete použít náš automatický instalační nástroj 'vcmibuilder', který vyžaduje pouze GOG instalátor Heroes III. Prosíme, navštivte naši wiki pro podrobné instrukce. + + + + To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. + Pro běh VCMI, datové soubory Heroes III musí být přítomny v jednom z určených umístění. Prosíme, zkopírujte data Heroes do jedné z těchto složek. + + + + Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. + Nebo můžete poskytnout složku s instalací Heroes III a VCMI zkopíruje existující data automaticky. + + + + Your Heroes III data files have been successfully found. + Vaše soubory dat Heroes III byly úspěšně nalezeny. + + + + The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually + Automatické rozpoznání jazyka Heroes III selhalo. Prosíme, vyberte jazyk vašich Heroes III ručně + + + + Interface Improvements + Vylepšení rozhraní + + + + Install a translation of Heroes III in your preferred language + Instalovat překlad Heroes III vašeho upřednostněného jazyka + + + + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher + Nyní můžete volitelně nainstalovat další modifikace, nebo též kdykoliv potom pomocí spouštěče VCMI + + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + Instalovat modifikaci, která poskytuje různá vylepšení rozhraní, například lepší rozhraní pro náhodné mapy a volitelné akce v bitvách + + + + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team + + + + + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion + + + + + Finish + Dokončit + + + + VCMI on Github + VCMI na GitHubu + + + + VCMI on Slack + VCMI na Slacku + + + + VCMI on Discord + VCMI na Discordu + + + + + Next + Další + + + + Open help in browser + Otevřít nápovědu v prohlížeči + + + + Search again + Hledat znovu + + + + Heroes III data files + Soubory dat Heroes III + + + + Copy existing data + Kopírovat existující data + + + + Your Heroes III language has been successfully detected. + Váš jazyk Heroes III byl úspěšně zjištěn. + + + + Heroes III language + Jazyk Heroes III + + + + + Back + Zpět + + + + Install VCMI Mod Preset + Instalovat předvybrané modifiakce VCMI + + + + Horn of the Abyss + + + + + Heroes III Translation + Překlady Heroes III + + + + In The Wake of Gods + + + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + + + + ImageViewer + + + Image Viewer + Prohlížeč obrázků + + + + Language + + + Czech + Čeština + + + + Chinese + Čínština + + + + English + Angličtina + + + + Finnish + Finština + + + + French + Francouzština + + + + German + Němčina + + + + Hungarian + Maďarština + + + + Italian + Italština + + + + Korean + Korejština + + + + Polish + Polština + + + + Portuguese + Portugalština + + + + Russian + Ruština + + + + Spanish + Španělština + + + + Swedish + Švédština + + + + Turkish + Turečtina + + + + Ukrainian + Ukrajinština + + + + Vietnamese + Vietnamština + + + + Other (East European) + Ostatní (východní Evropa) + + + + Other (Cyrillic Script) + Ostatní (azbuka) + + + + Other (West European) + Ostatní (západní Evropa) + + + + Auto (%1) + Automaticky (%1) + + + + MainWindow + + + VCMI Launcher + Spouštěč VCMI + + + + Settings + Nastavení + + + + Help + Nápověda + + + + Map Editor + Editor map + + + + Start game + Spustit hru + + + + Mods + Modifikace + + + + UpdateDialog + + + You have the latest version + Máte nejnovější verzi + + + + Close + Zavřít + + + + Check for updates on startup + Zkontrolovat aktualizace při startu + + + diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 94a2f2716..593169a2a 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -252,7 +252,7 @@ - + Description @@ -302,123 +302,123 @@ - + Mod name - + Installed version - + Latest version - + Size - + Download size - + Authors - + License - + Contact - + Compatibility - - + + Required VCMI version - + Supported VCMI version - + Supported VCMI versions - + Languages - + Required mods - + Conflicting mods - + This mod can not be installed or enabled because the following dependencies are not present - + This mod can not be enabled because the following mods are incompatible with it - + This mod cannot be disabled because it is required by the following mods - + This mod cannot be uninstalled or updated because it is required by the following mods - + This is a submod and it cannot be installed or uninstalled separately from its parent mod - + Notes - + Downloading %s%. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -427,35 +427,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 - + Mod is incompatible @@ -463,123 +463,126 @@ Install successfully downloaded? CSettingsView - - - + + + Off - - + Artificial Intelligence - - + Mod Repositories - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + Renderer + + + + + + On - + Cursor - + Heroes III Data Language - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -590,136 +593,106 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Reserved screen area - + Hardware - + Software - + Heroes III Translation - + Check on startup - + Fullscreen - - + General - + VCMI Language - + Resolution - + Autosave - + VSync - + Display index - + Network port - - + Video - + Show intro - + Active - + Disabled - + Enable - + Not Installed - + Install - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -900,6 +873,16 @@ Heroes® of Might and Magic® III HD is currently not supported! In The Wake of Gods + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + ImageViewer @@ -1017,118 +1000,6 @@ Heroes® of Might and Magic® III HD is currently not supported! - - Lobby - - - - Connect - - - - - Username - - - - - Server - - - - - Session - - - - - Players - - - - - Resolve - - - - - New game - - - - - Load game - - - - - New room - - - - - Join room - - - - - Ready - - - - - Mods mismatch - - - - - Leave - - - - - Kick player - - - - - Players in the room - - - - - Disconnect - - - - - No issues detected - - - - - LobbyRoomRequest - - - Room settings - - - - - Room name - - - - - Maximum players - - - - - Password (optional) - - - MainWindow @@ -1142,25 +1013,20 @@ Heroes® of Might and Magic® III HD is currently not supported! - + Help - + Map Editor - + Start game - - - Lobby - - Mods diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index 3d02b580c..d73d50dd2 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -252,7 +252,7 @@ - + Description Description @@ -302,128 +302,128 @@ Abandonner - + Mod name Nom du mod - + Installed version Version installée - + Latest version Dernière version - + Size - + Download size Taille de téléchargement - + Authors Auteur(s) - + License Licence - + Contact Contact - + Compatibility Compatibilité - - + + Required VCMI version Version requise de VCMI - + Supported VCMI version Version supportée de VCMI - + Supported VCMI versions Versions supportées de VCMI - + Languages Langues - + Required mods Mods requis - + Conflicting mods Mods en conflit - + This mod can not be installed or enabled because the following dependencies are not present Ce mod ne peut pas être installé ou activé car les dépendances suivantes ne sont pas présents - + This mod can not be enabled because the following mods are incompatible with it Ce mod ne peut pas être installé ou activé, car les dépendances suivantes sont incompatibles avec lui - + This mod cannot be disabled because it is required by the following mods Ce mod ne peut pas être désactivé car il est requis pour les dépendances suivantes - + This mod cannot be uninstalled or updated because it is required by the following mods Ce mod ne peut pas être désinstallé ou mis à jour car il est requis pour les dépendances suivantes - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Ce sous-mod ne peut pas être installé ou mis à jour séparément du mod parent - + Notes Notes - + Downloading %s%. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -432,35 +432,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 Impression écran %1 - + Mod is incompatible Ce mod est incompatible @@ -468,48 +468,46 @@ Install successfully downloaded? CSettingsView - - - + + + Off Désactivé - - + Artificial Intelligence Intelligence Artificielle - - + Mod Repositories Dépôts de Mod - - - + + + On Activé - + Enemy AI in battles IA ennemie dans les batailles - + Default repository Dépôt par défaut - + VSync - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -526,211 +524,186 @@ Mode fenêtré sans bord - le jeu s"exécutera dans une fenêtre qui couvre Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écran et utilisera la résolution sélectionnée. - + Windowed Fenêtré - + Borderless fullscreen Fenêtré sans bord - + Exclusive fullscreen Plein écran exclusif - + Reserved screen area - + Neutral AI in battles IA neutre dans les batailles - + Autosave limit (0 = off) - + Adventure Map Enemies Ennemis de la carte d"aventure - + Autosave prefix - + empty = map name prefix - + Interface Scaling Mise à l"échelle de l"interface - + Cursor Curseur - + Heroes III Data Language Langue des Données de Heroes III - + Framerate Limit Limite de fréquence d"images - + Hardware Matériel - + Software Logiciel - + + Renderer + + + + Heroes III Translation Traduction de Heroes III - + Adventure Map Allies Alliés de la carte d"aventure - + Additional repository Dépôt supplémentaire - + Check on startup Vérifier au démarrage - + Refresh now Actualiser maintenant - + Friendly AI in battles IA amicale dans les batailles - + Fullscreen Plein écran - - + General Général - + VCMI Language Langue de VCMI - + Resolution Résolution - + Autosave Sauvegarde automatique - + Display index Index d'affichage - + Network port Port de réseau - - + Video Vidéo - + Show intro Montrer l'intro - + Active Actif - + Disabled Désactivé - + Enable Activé - + Not Installed Pas Installé - + Install Installer - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -917,6 +890,16 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion Installer une version compatible de "In The Wake of Gods", une extension Heroes III créée par des fans + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + ImageViewer @@ -1034,118 +1017,6 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Auto (%1) - - Lobby - - - Username - Nom d'utilisateur - - - - - Connect - Connecter - - - - Server - Serveur - - - - New room - Nouveau salon - - - - Join room - Rejoindre le salon - - - - Session - Session - - - - Players - Joueurs - - - - Kick player - Jeter le joueur - - - - Players in the room - Joueurs dans le salon - - - - Leave - Quitter - - - - Mods mismatch - Incohérence de mods - - - - Ready - Prêt - - - - Resolve - Résoudre - - - - New game - Nouvelle partie - - - - Load game - Charger une partie - - - - Disconnect - Déconnecter - - - - No issues detected - Pas de problème détecté - - - - LobbyRoomRequest - - - Room settings - Paramètres de salon - - - - Room name - Nom de salon - - - - Maximum players - Maximum de joueurs - - - - Password (optional) - Mot de passe (optionnel) - - MainWindow @@ -1165,21 +1036,16 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge - Lobby - Salle d'attente - - - Help Aide - + Map Editor Éditeur de carte - + Start game Démarrer une partie diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index e08a8a7ad..36eab6711 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -121,7 +121,7 @@ Maps - + Karten @@ -180,7 +180,7 @@ Compatibility - Kompatibilität + Kompatibilität @@ -252,7 +252,7 @@ - + Description Beschreibung @@ -302,160 +302,167 @@ Abbrechen - + Mod name Mod-Name - + Installed version Installierte Version - + Latest version Letzte Version - + Size - + Größe - + Download size Downloadgröße - + Authors Autoren - + License Lizenz - + Contact Kontakt - + Compatibility Kompatibilität - - + + Required VCMI version Benötigte VCMI Version - + Supported VCMI version Unterstützte VCMI Version - + Supported VCMI versions Unterstützte VCMI Versionen - + Languages Sprachen - + Required mods Benötigte Mods - + Conflicting mods Mods mit Konflikt - + This mod can not be installed or enabled because the following dependencies are not present Diese Mod kann nicht installiert oder aktiviert werden, da die folgenden Abhängigkeiten nicht vorhanden sind - + This mod can not be enabled because the following mods are incompatible with it Diese Mod kann nicht aktiviert werden, da folgende Mods nicht mit dieser Mod kompatibel sind - + This mod cannot be disabled because it is required by the following mods Diese Mod kann nicht deaktiviert werden, da sie zum Ausführen der folgenden Mods erforderlich ist - + This mod cannot be uninstalled or updated because it is required by the following mods Diese Mod kann nicht deinstalliert oder aktualisiert werden, da sie für die folgenden Mods erforderlich ist - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Dies ist eine Submod und kann nicht separat von der Hauptmod installiert oder deinstalliert werden - + Notes Anmerkungen - + Downloading %s%. %p% (%v MB out of %m MB) finished - + Herunterladen von %s%. %p% (%v MB von %m MB) beendet - + Download failed - + Download fehlgeschlagen - + Unable to download all files. Encountered errors: - + Es konnten nicht alle Dateien heruntergeladen werden. + +Es sind Fehler aufgetreten: + + - + Install successfully downloaded? - - - - - Installing mod %1 - - - - - Operation failed - + + +Installation erfolgreich heruntergeladen? - Encountered errors: - - + Installing mod %1 + Installation von Mod %1 - + + Operation failed + Operation fehlgeschlagen + + + + Encountered errors: + + Aufgetretene Fehler: + + + + Screenshot %1 Screenshot %1 - + Mod is incompatible Mod ist inkompatibel @@ -463,123 +470,126 @@ Install successfully downloaded? CSettingsView - - - + + + Off Aus - - + Artificial Intelligence Künstliche Intelligenz - - + Mod Repositories Mod-Repositorien - + Interface Scaling Skalierung der Benutzeroberfläche - + Neutral AI in battles Neutrale KI in Kämpfen - + Enemy AI in battles Gegnerische KI in Kämpfen - + Additional repository Zusätzliches Repository - + Adventure Map Allies Abenteuerkarte Verbündete - + Adventure Map Enemies Abenteuerkarte Feinde - + Windowed Fenstermodus - + Borderless fullscreen Randloser Vollbildmodus - + Exclusive fullscreen Exklusiver Vollbildmodus - + Autosave limit (0 = off) Limit für Autospeicherung (0 = aus) - + Friendly AI in battles Freundliche KI in Kämpfen - + Framerate Limit Limit der Bildrate - + Autosave prefix Präfix für Autospeicherung - + empty = map name prefix leer = Kartenname als Präfix - + Refresh now Jetzt aktualisieren - + Default repository Standard Repository - - - + + Renderer + + + + + + On An - + Cursor Zeiger - + Heroes III Data Language Sprache der Heroes III Daten - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -596,136 +606,106 @@ Randloser Fenstermodus - das Spiel läuft in einem Fenster, das den gesamten Bil Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwendet die gewählte Auflösung. - + Reserved screen area - + Reservierter Bildschirmbereich - + Hardware Hardware - + Software Software - + Heroes III Translation Heroes III Übersetzung - + Check on startup Beim Start prüfen - + Fullscreen Vollbild - - + General Allgemein - + VCMI Language VCMI-Sprache - + Resolution Auflösung - + Autosave Autospeichern - + VSync - + VSync - + Display index Anzeige-Index - + Network port Netzwerk-Port - - + Video Video - + Show intro Intro anzeigen - + Active Aktiv - + Disabled Deaktiviert - + Enable Aktivieren - + Not Installed Nicht installiert - + Install Installieren - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -912,6 +892,16 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! In The Wake of Gods In The Wake of Gods + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + ImageViewer @@ -1029,118 +1019,6 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Auto (%1) - - Lobby - - - - Connect - Verbinden - - - - Username - Benutzername - - - - Server - Server - - - - Session - Sitzung - - - - Players - Spieler - - - - Resolve - Auflösen - - - - New game - Neues Spiel - - - - Load game - Spiel laden - - - - New room - Neuer Raum - - - - Join room - Raum beitreten - - - - Ready - Bereit - - - - Mods mismatch - Mods stimmen nicht überein - - - - Leave - Verlassen - - - - Kick player - Spieler kicken - - - - Players in the room - Spieler im Raum - - - - Disconnect - Verbindung trennen - - - - No issues detected - Keine Probleme festgestellt - - - - LobbyRoomRequest - - - Room settings - Raumeinstellungen - - - - Room name - Raumname - - - - Maximum players - Maximale Spieler - - - - Password (optional) - Passwort (optional) - - MainWindow @@ -1160,21 +1038,16 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! - Lobby - Lobby - - - Help Hilfe - + Map Editor Karteneditor - + Start game Spiel starten diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 4116e62f7..d4748758f 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -121,7 +121,7 @@ Maps - + Mapy @@ -180,7 +180,7 @@ Compatibility - Kompatybilność + Kompatybilność @@ -252,7 +252,7 @@ - + Description Opis @@ -302,160 +302,167 @@ Przerwij - + Mod name Nazwa moda - + Installed version Zainstalowana wersja - + Latest version Najnowsza wersja - + Size - + Rozmiar - + Download size Rozmiar pobierania - + Authors Autorzy - + License Licencja - + Contact Kontakt - + Compatibility Kompatybilność - - + + Required VCMI version Wymagana wersja VCMI - + Supported VCMI version Wspierana wersja VCMI - + Supported VCMI versions Wspierane wersje VCMI - + Languages Języki - + Required mods Wymagane mody - + Conflicting mods Konfliktujące mody - + This mod can not be installed or enabled because the following dependencies are not present Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione - + This mod can not be enabled because the following mods are incompatible with it Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne - + This mod cannot be disabled because it is required by the following mods Ten mod nie może zostać wyłączony ponieważ jest wymagany do uruchomienia następujących modów - + This mod cannot be uninstalled or updated because it is required by the following mods Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów - + This is a submod and it cannot be installed or uninstalled separately from its parent mod To jest moduł składowy innego moda i nie może być zainstalowany lub odinstalowany oddzielnie od moda nadrzędnego - + Notes Uwagi - + Downloading %s%. %p% (%v MB out of %m MB) finished - + Pobieranie %s%. %p% (%v MB z %m MB) ukończono - + Download failed - + Pobieranie nieudane - + Unable to download all files. Encountered errors: - + Nie udało się pobrać wszystkich plików. + +Napotkane błędy: + + - + Install successfully downloaded? - - - - - Installing mod %1 - - - - - Operation failed - + + +Zainstalować pomyślnie pobrane? - Encountered errors: - - + Installing mod %1 + Instalowanie modyfikacji %1 - + + Operation failed + Operacja nieudana + + + + Encountered errors: + + Napotkane błędy: + + + + Screenshot %1 Zrzut ekranu %1 - + Mod is incompatible Mod jest niekompatybilny @@ -463,123 +470,126 @@ Install successfully downloaded? CSettingsView - - - + + + Off Wyłączony - - + Artificial Intelligence Sztuczna Inteligencja - - + Mod Repositories Repozytoria modów - + Interface Scaling Skala interfejsu - + Neutral AI in battles AI bitewne jednostek neutralnych - + Enemy AI in battles AI bitewne wrogów - + Additional repository Dodatkowe repozytorium - + Adventure Map Allies AI sojuszników mapy przygody - + Adventure Map Enemies AI wrogów mapy przygody - + Windowed Okno - + Borderless fullscreen Pełny ekran (tryb okna) - + Exclusive fullscreen Pełny ekran klasyczny - + Autosave limit (0 = off) Limit autozapisów (0 = brak) - + Friendly AI in battles AI bitewne sojuszników - + Framerate Limit Limit FPS - + Autosave prefix Przedrostek autozapisu - + empty = map name prefix puste = przedrostek z nazwy mapy - + Refresh now Odśwież - + Default repository Domyślne repozytorium - - - + + Renderer + + + + + + On Włączony - + Cursor Kursor - + Heroes III Data Language Język plików Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -596,136 +606,106 @@ Pełny ekran w trybie okna - gra uruchomi się w oknie przysłaniającym cały e Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybranej przez ciebie rozdzielczości ekranu. - + Reserved screen area - + Zarezerwowany obszar ekranu - + Hardware Sprzętowy - + Software Programowy - + Heroes III Translation Tłumaczenie Heroes III - + Check on startup Sprawdzaj przy uruchomieniu - + Fullscreen Pełny ekran - - + General Ogólne - + VCMI Language Język VCMI - + Resolution Rozdzielczość - + Autosave Autozapis - + VSync - + Synchronizacja pionowa (VSync) - + Display index Numer wyświetlacza - + Network port Port sieciowy - - + Video Obraz - + Show intro Pokaż intro - + Active Aktywny - + Disabled Wyłączone - + Enable Włącz - + Not Installed Nie zainstalowano - + Install Zainstaluj - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -912,6 +892,16 @@ Heroes III: HD Edition nie jest obecnie wspierane! In The Wake of Gods In The Wake of Gods + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + ImageViewer @@ -926,87 +916,87 @@ Heroes III: HD Edition nie jest obecnie wspierane! Czech - + Czeski Chinese - + Chiński English - + Angielski Finnish - + Fiński French - + Francuski German - + Niemiecki Hungarian - + Węgierski Italian - + Włoski Korean - + Koreański Polish - + Polski Portuguese - + Portugalski Russian - + Rosyjski Spanish - + Hiszpański Swedish - + Szwedzki Turkish - + Turecki Ukrainian - + Ukraiński Vietnamese - + Wietnamski @@ -1026,119 +1016,7 @@ Heroes III: HD Edition nie jest obecnie wspierane! Auto (%1) - - - - - Lobby - - - - Connect - Połącz - - - - Username - Nazwa użytkownika - - - - Server - Serwer - - - - Session - Sesja - - - - Players - Gracze - - - - Resolve - Rozwiąż - - - - New game - Nowa gra - - - - Load game - Wczytaj grę - - - - New room - Nowy pokój - - - - Join room - Dołącz - - - - Ready - Zgłoś gotowość - - - - Mods mismatch - Niezgodność modów - - - - Leave - Wyjdź - - - - Kick player - Wyrzuć gracza - - - - Players in the room - Gracze w pokoju - - - - Disconnect - Rozłącz - - - - No issues detected - Nie znaleziono problemów - - - - LobbyRoomRequest - - - Room settings - Ustawienia pokoju - - - - Room name - Nazwa pokoju - - - - Maximum players - Maks. ilość graczy - - - - Password (optional) - Hasło (opcjonalnie) + @@ -1154,25 +1032,20 @@ Heroes III: HD Edition nie jest obecnie wspierane! Ustawienia - + Help Pomoc - + Map Editor Edytor map - + Start game Uruchom grę - - - Lobby - Lobby - Mods diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index 718618d7d..2ee2ae217 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -252,7 +252,7 @@ - + Description Описание @@ -302,123 +302,123 @@ Отмена - + Mod name Название мода - + Installed version Установленная версия - + Latest version Последняя версия - + Size - + Download size Размер загрузки - + Authors Авторы - + License Лицензия - + Contact Контакты - + Compatibility Совместимость - - + + Required VCMI version Требуемая версия VCMI - + Supported VCMI version Поддерживаемая версия VCMI - + Supported VCMI versions Поддерживаемые версии VCMI - + Languages Языки - + Required mods Зависимости - + Conflicting mods Конфликтующие моды - + This mod can not be installed or enabled because the following dependencies are not present Этот мод не может быть установлен или активирован, так как отсутствуют следующие зависимости - + This mod can not be enabled because the following mods are incompatible with it Этот мод не может быть установлен или активирован, так как следующие моды несовместимы с этим - + This mod cannot be disabled because it is required by the following mods Этот мод не может быть выключен, так как он является зависимостью для следующих - + This mod cannot be uninstalled or updated because it is required by the following mods Этот мод не может быть удален или обновлен, так как является зависимостью для следующих модов - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Это вложенный мод, он не может быть установлен или удален отдельно от родительского - + Notes Замечания - + Downloading %s%. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -427,35 +427,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 Скриншот %1 - + Mod is incompatible Мод несовместим @@ -463,154 +463,156 @@ Install successfully downloaded? CSettingsView - + Interface Scaling - - - + + + Off Отключено - - - + + + On Включено - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Check on startup Проверять при запуске - + Fullscreen Полноэкранный режим - - + General Общее - + VCMI Language Язык VCMI - + Cursor Курсор - - + Artificial Intelligence Искусственный интеллект - - + Mod Repositories Репозитории модов - + Adventure Map Allies - + Refresh now - + Adventure Map Enemies - + VSync - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Reserved screen area - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Default repository - + + Renderer + + + + Heroes III Data Language Язык данных Героев III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -621,105 +623,76 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Аппаратный - + Software Программный - + Heroes III Translation Перевод Героев III - + Resolution Разрешение экрана - + Autosave Автосохранение - + Display index Дисплей - + Network port Сетевой порт - - + Video Графика - + Show intro Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -906,6 +879,16 @@ Heroes® of Might and Magic® III HD is currently not supported! In The Wake of Gods + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + ImageViewer @@ -1023,118 +1006,6 @@ Heroes® of Might and Magic® III HD is currently not supported! Авто (%1) - - Lobby - - - - Connect - Подключиться - - - - Username - Имя пользователя - - - - Server - Сервер - - - - Session - Сессия - - - - Players - Игроки - - - - Resolve - Скорректировать - - - - New game - Новая игра - - - - Load game - Загрузить игру - - - - New room - Создать комнату - - - - Join room - Присоединиться к комнате - - - - Ready - Готово - - - - Mods mismatch - Моды не совпадают - - - - Leave - Выйти - - - - Kick player - Выгнать игрока - - - - Players in the room - Игроки в комнате - - - - Disconnect - Отключиться - - - - No issues detected - Проблем не обнаружено - - - - LobbyRoomRequest - - - Room settings - Настройки комнаты - - - - Room name - Название - - - - Maximum players - Максимум игроков - - - - Password (optional) - Пароль (не обязательно) - - MainWindow @@ -1148,25 +1019,20 @@ Heroes® of Might and Magic® III HD is currently not supported! Параметры - + Help - + Map Editor Редактор карт - + Start game Играть - - - Lobby - Лобби - Mods diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index b0a953ab7..fab6c2e93 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -6,84 +6,84 @@ VCMI on Discord - VCMI en Discord + VCMI en Discord Have a question? Found a bug? Want to help? Join us! - ¿Tienes alguna pregunta? ¿Encontraste algún error? ¿Quieres ayudar? ¡Únete a nosotros! + ¿Tienes alguna pregunta? ¿Encontraste algún error? ¿Quieres ayudar? ¡Únete a nosotros! VCMI on Github - VCMI en Github + VCMI en Github Our Community - + Nuestra comunidad VCMI on Slack - VCMI en Slack + VCMI en Slack Build Information - + Información de la versión User data directory - Directorio de datos del usuario + Directorio de datos del usuario Open - Abrir + Abrir Check for updates - + Comprobar actualizaciones Game version - + Versión del juego Log files directory - Directorio de archivos de registro + Directorio de archivos de registro Data Directories - Directorios de datos + Directorios de datos Game data directory - + Directorio de los datos del juego Operating System - + Sistema operativo Project homepage - + Página web del proyecto Report a bug - + Informar de un error @@ -121,7 +121,7 @@ Maps - + Mapas @@ -180,7 +180,7 @@ Compatibility - Compatibilidad + Compatibilidad @@ -252,7 +252,7 @@ - + Description Descripción @@ -302,160 +302,167 @@ Cancelar - + Mod name Nombre del mod - + Installed version Versión instalada - + Latest version Última versión - + Size - + Tamaño - + Download size Tamaño de descarga - + Authors Autores - + License Licencia - + Contact Contacto - + Compatibility Compatibilidad - - + + Required VCMI version Versión de VCMI requerida - + Supported VCMI version Versión de VCMI compatible - + Supported VCMI versions Versiones de VCMI compatibles - + Languages Idiomas - + Required mods Mods requeridos - + Conflicting mods Mods conflictivos - + This mod can not be installed or enabled because the following dependencies are not present Este mod no se puede instalar o habilitar porque no están presentes las siguientes dependencias - + This mod can not be enabled because the following mods are incompatible with it Este mod no se puede habilitar porque los siguientes mods son incompatibles con él - + This mod cannot be disabled because it is required by the following mods No se puede desactivar este mod porque es necesario para ejecutar los siguientes mods - + This mod cannot be uninstalled or updated because it is required by the following mods No se puede desinstalar o actualizar este mod porque es necesario para ejecutar los siguientes mods - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Este es un submod y no se puede instalar o desinstalar por separado del mod principal - + Notes Notas - + Downloading %s%. %p% (%v MB out of %m MB) finished - + Descargando %s%. %p% (%v MB de %m MB) completado - + Download failed - + Descarga fallida - + Unable to download all files. Encountered errors: - + No se han podido descargar todos los ficheros. + +Errores encontrados: + + - + Install successfully downloaded? - - - - - Installing mod %1 - - - - - Operation failed - + + +Instalar lo correctamente descargado? - Encountered errors: - - + Installing mod %1 + Instalando mod %1 - + + Operation failed + Operación fallida + + + + Encountered errors: + + Errores encontrados: + + + + Screenshot %1 Captura de pantalla %1 - + Mod is incompatible El mod es incompatible @@ -463,175 +470,176 @@ Install successfully downloaded? CSettingsView - - - + + + Off Desactivado - - + Artificial Intelligence Inteligencia Artificial - - + Mod Repositories Repositorios de Mods - + Interface Scaling - + Escala de la interfaz - + Neutral AI in battles - + IA neutral en batallas - + Enemy AI in battles - + IA enemiga en batallas - + Additional repository - + Repositorio adicional - + Adventure Map Allies - + Aliados en el Mapa de aventuras - + Adventure Map Enemies - + Enemigos en el Mapa de aventuras - + Windowed - + Ventana - + Borderless fullscreen - + Ventana completa sin bordes - + Exclusive fullscreen - - - - - Autosave limit (0 = off) - - - - - Friendly AI in battles - - - - - Framerate Limit - - - - - Autosave prefix - - - - - empty = map name prefix - + Pantalla completa + Autosave limit (0 = off) + Límite de autosaves (0 = sin límite) + + + + Friendly AI in battles + IA amistosa en batallas + + + + Framerate Limit + Límite de fotogramas + + + + Autosave prefix + Prefijo autoguardado + + + + empty = map name prefix + Vacio = prefijo del mapa + + + Refresh now - + Actualizar - + Default repository - + Repositorio por defecto - - - + + Renderer + Render + + + + + On - Encendido + Activado - + Cursor Cursor - + Heroes III Translation Traducción de Heroes III - + Reserved screen area - + Área de pantalla reservada - + Fullscreen Pantalla completa - - + General General - + VCMI Language Idioma de VCMI - + Resolution Resolución - + Autosave Autoguardado - + VSync - + Sincronización vertical - + Display index Mostrar índice - + Network port Puerto de red - - + Video Vídeo - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -639,87 +647,65 @@ Windowed - game will run inside a window that covers part of your screen Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. - + Selecciona el modo de visualización del juego + +En ventana - el juego se ejecutará dentro de una ventana que forma parte de tu pantalla. + +Ventana sin bordes - el juego se ejecutará en una ventana que cubre completamente tu pantalla, usando la misma resolución. + +Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará la resolución seleccionada. - + Hardware Hardware - + Software Software - + Show intro Mostrar introducción - + Check on startup Comprovar al inicio - + Heroes III Data Language Idioma de los datos de Heroes III. - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -791,12 +777,12 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Interface Improvements - + Mejora de la interfaz Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles - + Instalar mod que proporciona varias mejoras en la interfaz, como mejor interacción en los mapas aleatorios y más opciones en las batallas @@ -906,6 +892,16 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Finish Finalizar + + + Heroes III installation found! + Instalación de Heroes III encontrada! + + + + Copy data to VCMI folder? + Copiar datos a la carpeta VCMI? + ImageViewer @@ -920,7 +916,7 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Czech - + Czech (Checo) @@ -935,7 +931,7 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Finnish - + Finnish (Finlandés) @@ -950,12 +946,12 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Hungarian - + Hungarian (Húngaro) Italian - + Italian (Italiano) @@ -970,7 +966,7 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Portuguese - + Portuguese (Portugués) @@ -985,12 +981,12 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Swedish - + Swedish (Sueco) Turkish - + Turkish (Turco) @@ -1000,7 +996,7 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Vietnamese - + Vietnamese (Vietnamita) @@ -1010,12 +1006,12 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Other (Cyrillic Script) - + Otro (Escritura cirílica) Other (West European) - + Otro (Europa del Este) @@ -1023,118 +1019,6 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Automático (%1) - - Lobby - - - - Connect - Conectar - - - - Username - Nombre de usuario - - - - Server - Servidor - - - - Session - Sesión - - - - Players - Jugadores - - - - Resolve - Resolver - - - - New game - Nueva partida - - - - Load game - Cargar partida - - - - New room - Nueva sala - - - - Join room - Unirse a la sala - - - - Ready - Listo - - - - Mods mismatch - No coinciden los mods - - - - Leave - Salir - - - - Kick player - Expulsar jugador - - - - Players in the room - Jugadores en la sala - - - - Disconnect - Desconectar - - - - No issues detected - No se han detectado problemas - - - - LobbyRoomRequest - - - Room settings - Configuración de la sala - - - - Room name - Nombre de la sala - - - - Maximum players - Jugadores máximos - - - - Password (optional) - Contraseña (opcional) - - MainWindow @@ -1148,25 +1032,20 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Configuración - + Help - + Ayuda - + Map Editor Editor de Mapas - + Start game Iniciar juego - - - Lobby - Sala de Espera - Mods diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 37c76d07c..5a8f80cd5 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -252,7 +252,7 @@ - + Description Опис @@ -302,123 +302,123 @@ Відмінити - + Mod name Назва модифікації - + Installed version Встановлена версія - + Latest version Найновіша версія - + Size Розмір - + Download size Розмір для завантаження - + Authors Автори - + License Ліцензія - + Contact Контакти - + Compatibility Сумісність - - + + Required VCMI version Необхідна версія VCMI - + Supported VCMI version Підтримувана версія VCMI - + Supported VCMI versions Підтримувані версії VCMI - + Languages Мови - + Required mods Необхідні модифікації - + Conflicting mods Конфліктуючі модифікації - + This mod can not be installed or enabled because the following dependencies are not present Цю модифікацію не можна встановити чи активувати, оскільки відсутні наступні залежності - + This mod can not be enabled because the following mods are incompatible with it Цю модифікацію не можна ввімкнути, оскільки наступні модифікації несумісні з цією модифікацією - + This mod cannot be disabled because it is required by the following mods Цю модифікацію не можна відключити, оскільки вона необхідна для запуску наступних модифікацій - + This mod cannot be uninstalled or updated because it is required by the following mods Цю модифікацію не можна видалити або оновити, оскільки вона необхідна для запуску наступних модифікацій - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Це вкладена модифікація, і її не можна встановити або видалити окремо від батьківської модифікації - + Notes Примітки - + Downloading %s%. %p% (%v MB out of %m MB) finished Завантажуємо %s%. %p% (%v МБ з %m Мб) виконано - + Download failed Помилка завантаження - + Unable to download all files. Encountered errors: @@ -431,7 +431,7 @@ Encountered errors: - + Install successfully downloaded? @@ -440,29 +440,29 @@ Install successfully downloaded? Встановити успішно завантажені? - + Installing mod %1 Встановлення модифікації %1 - + Operation failed Операція завершилася невдало - + Encountered errors: Виникли помилки: - + Screenshot %1 Знімок екрану %1 - + Mod is incompatible Модифікація несумісна @@ -470,123 +470,126 @@ Install successfully downloaded? CSettingsView - - - + + + Off Вимкнено - - + Artificial Intelligence Штучний інтелект - - + Mod Repositories Репозиторії модифікацій - + Interface Scaling Масштабування інтерфейсу - + Neutral AI in battles Нейтральний ШІ в боях - + Enemy AI in battles Ворожий ШІ в боях - + Additional repository Додатковий репозиторій - + Adventure Map Allies Союзники на мапі пригод - + Adventure Map Enemies Вороги на мапі пригод - + Windowed У вікні - + Borderless fullscreen Повноекранне вікно - + Exclusive fullscreen Повноекранний (ексклюзивно) - + Autosave limit (0 = off) Кількість автозбережень - + Friendly AI in battles Дружній ШІ в боях - + Framerate Limit Обмеження частоти кадрів - + Autosave prefix Префікс назв автозбережень - + empty = map name prefix (використовувати назву карти) - + Refresh now Оновити зараз - + Default repository Стандартний репозиторій - - - + + Renderer + Рендерер + + + + + On Увімкнено - + Cursor Курсор - + Heroes III Data Language Мова Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -603,136 +606,106 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. - + Reserved screen area Зарезервована зона екрану - + Hardware Апаратний - + Software Програмний - + Heroes III Translation Переклад Heroes III - + Check on startup Перевіряти на старті - + Fullscreen Повноекранний режим - - + General Загальні налаштування - + VCMI Language Мова VCMI - + Resolution Роздільна здатність - + Autosave Автозбереження - + VSync Вертикальна синхронізація - + Display index Дісплей - + Network port Мережевий порт - - + Video Графіка - + Show intro Вступні відео - + Active Активні - + Disabled Деактивований - + Enable Активувати - + Not Installed Не встановлено - + Install Встановити - - Chat - - - Form - - - - - Users in lobby - Гравців у лобі - - - - Global chat - Загальний чат - - - - type you message - введіть повідомлення - - - - send - Відправити - - FirstLaunchView @@ -919,6 +892,16 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс In The Wake of Gods In The Wake of Gods + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + ImageViewer @@ -1036,118 +1019,6 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Авто (%1) - - Lobby - - - - Connect - Підключитися - - - - Username - Ім'я користувача - - - - Server - Сервер - - - - Session - Сесія - - - - Players - Гравці - - - - Resolve - Розв'язати - - - - New game - Нова гра - - - - Load game - Завантажити гру - - - - New room - Створити кімнату - - - - Join room - Приєднатися до кімнати - - - - Ready - Готовність - - - - Mods mismatch - Модифікації, що не збігаються - - - - Leave - Вийти з кімнати - - - - Kick player - Виключити гравця - - - - Players in the room - Гравці у кімнаті - - - - Disconnect - Від'єднатися - - - - No issues detected - Проблем не виявлено - - - - LobbyRoomRequest - - - Room settings - Налаштування кімнати - - - - Room name - Назва кімнати - - - - Maximum players - Максимум гравців - - - - Password (optional) - Пароль (за бажанням) - - MainWindow @@ -1167,21 +1038,16 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс - Lobby - Лобі - - - Help Допомога - + Map Editor Редактор мап - + Start game Грати diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index f92e9e7fe..58affaf3b 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -252,7 +252,7 @@ - + Description Mô tả @@ -302,123 +302,123 @@ Hủy - + Mod name Tên bản sửa đổi - + Installed version Phiên bản cài đặt - + Latest version Phiên bản mới nhất - + Size - + Download size Kích thước tải về - + Authors Tác giả - + License Giấy phép - + Contact Liên hệ - + Compatibility Tương thích - - + + Required VCMI version Cần phiên bản VCMI - + Supported VCMI version Hỗ trợ phiên bản VCMI - + Supported VCMI versions Phiên bản VCMI hỗ trợ - + Languages Ngôn ngữ - + Required mods Cần các bản sửa đổi - + Conflicting mods Bản sửa đổi không tương thích - + This mod can not be installed or enabled because the following dependencies are not present Bản sửa đổi này không thể cài đặt hoặc kích hoạt do thiếu các bản sửa đổi sau - + This mod can not be enabled because the following mods are incompatible with it Bản sửa đổi này không thể kích hoạt do không tương thích các bản sửa đổi sau - + This mod cannot be disabled because it is required by the following mods Bản sửa đổi này không thể tắt do cần thiết cho các bản sửa đổi sau - + This mod cannot be uninstalled or updated because it is required by the following mods Bản sửa đổi này không thể gỡ bỏ hoặc nâng cấp do cần thiết cho các bản sửa đổi sau - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Đây là bản con, không thể cài đặt hoặc gỡ bỏ tách biệt với bản cha - + Notes Ghi chú - + Downloading %s%. %p% (%v MB out of %m MB) finished - + Download failed - + Unable to download all files. Encountered errors: @@ -427,35 +427,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 Hình ảnh %1 - + Mod is incompatible Bản sửa đổi này không tương thích @@ -463,123 +463,126 @@ Install successfully downloaded? CSettingsView - - - + + + Off Tắt - - + Artificial Intelligence Trí tuệ nhân tạo - - + Mod Repositories Nguồn bản sửa đổi - + Interface Scaling Phóng đại giao diện - + Neutral AI in battles Máy hoang dã trong trận đánh - + Enemy AI in battles Máy đối thủ trong trận đánh - + Additional repository Nguồn bổ sung - + Adventure Map Allies Máy liên minh ở bản đồ phiêu lưu - + Adventure Map Enemies Máy đối thủ ở bản đồ phiêu lưu - + Windowed Cửa sổ - + Borderless fullscreen Toàn màn hình không viền - + Exclusive fullscreen Toàn màn hình riêng biệt - + Autosave limit (0 = off) Giới hạn lưu tự động (0 = không giới hạn) - + Friendly AI in battles Máy liên minh trong trận đánh - + Framerate Limit Giới hạn khung hình - + Autosave prefix Thêm tiền tố vào lưu tự động - + empty = map name prefix Rỗng = tên bản đồ - + Refresh now Làm mới - + Default repository Nguồn mặc định - - - + + Renderer + + + + + + On Bật - + Cursor Con trỏ - + Heroes III Data Language Ngôn ngữ dữ liệu Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -596,136 +599,106 @@ Toàn màn hình không viền - Trò chơi chạy toàn màn hình, dùng chung Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng độ phân giải được chọn. - + Reserved screen area Diện tích màn hình dành riêng - + Hardware Phần cứng - + Software Phần mềm - + Heroes III Translation Bản dịch Heroes III - + Check on startup Kiểm tra khi khởi động - + Fullscreen Toàn màn hình - - + General Chung - + VCMI Language Ngôn ngữ VCMI - + Resolution Độ phân giải - + Autosave Tự động lưu - + VSync - + Display index Mục hiện thị - + Network port Cổng mạng - - + Video Phim ảnh - + Show intro Hiện thị giới thiệu - + Active Bật - + Disabled Tắt - + Enable Bật - + Not Installed Chưa cài đặt - + Install Cài đặt - - Chat - - - Form - - - - - Users in lobby - - - - - Global chat - - - - - type you message - - - - - send - - - FirstLaunchView @@ -912,6 +885,16 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD!In The Wake of Gods In The Wake of Gods + + + Heroes III installation found! + + + + + Copy data to VCMI folder? + + ImageViewer @@ -1029,118 +1012,6 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD!Tự động (%1) - - Lobby - - - - Connect - Kết nối - - - - Username - Tên đăng nhập - - - - Server - Máy chủ - - - - Session - Phiên - - - - Players - Người chơi - - - - Resolve - Phân tích - - - - New game - Tạo mới - - - - Load game - Tải lại - - - - New room - Tạo phòng - - - - Join room - Vào phòng - - - - Ready - Sẵn sàng - - - - Mods mismatch - Bản sửa đổi chưa giống - - - - Leave - Rời khỏi - - - - Kick player - Mời ra - - - - Players in the room - Người chơi trong phòng - - - - Disconnect - Thoát - - - - No issues detected - Không có vấn đề - - - - LobbyRoomRequest - - - Room settings - Cài đặt phòng - - - - Room name - Tên phòng - - - - Maximum players - Số người chơi tối đa - - - - Password (optional) - Mật khẩu (tùy chọn) - - MainWindow @@ -1154,25 +1025,20 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD!Cài đặt - + Help - + Map Editor Tạo bản đồ - + Start game Chơi ngay - - - Lobby - Sảnh - Mods diff --git a/launcher/updatedialog_moc.cpp b/launcher/updatedialog_moc.cpp index d5cd01ed1..55f4eb94f 100644 --- a/launcher/updatedialog_moc.cpp +++ b/launcher/updatedialog_moc.cpp @@ -67,7 +67,7 @@ UpdateDialog::UpdateDialog(bool calledManually, QWidget *parent): } auto byteArray = response->readAll(); - JsonNode node(byteArray.constData(), byteArray.size()); + JsonNode node(reinterpret_cast(byteArray.constData()), byteArray.size()); loadFromJson(node); }); } @@ -79,7 +79,7 @@ UpdateDialog::~UpdateDialog() void UpdateDialog::showUpdateDialog(bool isManually) { - UpdateDialog * dialog = new UpdateDialog(isManually); + auto * dialog = new UpdateDialog(isManually); dialog->setAttribute(Qt::WA_DeleteOnClose); } @@ -96,7 +96,7 @@ void UpdateDialog::loadFromJson(const JsonNode & node) node["updateType"].getType() != JsonNode::JsonType::DATA_STRING || node["version"].getType() != JsonNode::JsonType::DATA_STRING || node["changeLog"].getType() != JsonNode::JsonType::DATA_STRING || - node.getType() != JsonNode::JsonType::DATA_STRUCT) //we need at least one link - other are optional + node["downloadLinks"].getType() != JsonNode::JsonType::DATA_STRUCT) //we need at least one link - other are optional { ui->plainTextEdit->setPlainText("Cannot read JSON from url or incorrect JSON data"); return; diff --git a/launcher/vcmilauncher.desktop b/launcher/vcmilauncher.desktop index b916a002d..ad792e8bb 100644 --- a/launcher/vcmilauncher.desktop +++ b/launcher/vcmilauncher.desktop @@ -1,10 +1,15 @@ [Desktop Entry] Type=Application Name=VCMI -GenericName=Strategy Game Engine -Comment=Launcher for open engine of Heroes of Might and Magic 3 +GenericName=Strategy Game +GenericName[cs]=Strategická hra +GenericName[de]=Strategiespiel +Comment=Open-source recreation of Heroes of Might & Magic III +Comment[cs]=Spouštěč enginu s otevřeným kódem pro Heroes of Might and Magic III +Comment[de]=Open-Source-Nachbau von Heroes of Might and Magic III Icon=vcmiclient Exec=vcmilauncher Categories=Game;StrategyGame; -Version=1.0 -Keywords=heroes;homm3; +Version=1.5 +Keywords=heroes of might and magic;heroes;homm;homm3;strategy; +SingleMainWindow=true diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index 92da87763..66aafc53e 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -33,10 +33,11 @@ DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtAnyPosition(const CArtifactSet DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtBackpackPosition(const CArtifactSet * target, const ArtifactID & aid) { const auto * art = aid.toArtifact(); - if(art->canBePutAt(target, ArtifactPosition::BACKPACK_START)) - { - return ArtifactPosition::BACKPACK_START; - } + if(target->bearerType() == ArtBearer::HERO) + if(art->canBePutAt(target, ArtifactPosition::BACKPACK_START)) + { + return ArtifactPosition::BACKPACK_START; + } return ArtifactPosition::PRE_FIRST; } @@ -74,6 +75,49 @@ DLL_LINKAGE const std::vector & ArtifactUtils::constituentWorn return positions; } +DLL_LINKAGE const std::vector & ArtifactUtils::allWornSlots() +{ + static const std::vector positions = + { + ArtifactPosition::HEAD, + ArtifactPosition::SHOULDERS, + ArtifactPosition::NECK, + ArtifactPosition::RIGHT_HAND, + ArtifactPosition::LEFT_HAND, + ArtifactPosition::TORSO, + ArtifactPosition::RIGHT_RING, + ArtifactPosition::LEFT_RING, + ArtifactPosition::FEET, + ArtifactPosition::MISC1, + ArtifactPosition::MISC2, + ArtifactPosition::MISC3, + ArtifactPosition::MISC4, + ArtifactPosition::MISC5, + ArtifactPosition::MACH1, + ArtifactPosition::MACH2, + ArtifactPosition::MACH3, + ArtifactPosition::MACH4, + ArtifactPosition::SPELLBOOK + }; + + return positions; +} + +DLL_LINKAGE const std::vector & ArtifactUtils::commanderSlots() +{ + static const std::vector positions = + { + ArtifactPosition::COMMANDER1, + ArtifactPosition::COMMANDER2, + ArtifactPosition::COMMANDER3, + ArtifactPosition::COMMANDER4, + ArtifactPosition::COMMANDER5, + ArtifactPosition::COMMANDER6 + }; + + return positions; +} + DLL_LINKAGE bool ArtifactUtils::isArtRemovable(const std::pair & slot) { return slot.second.artifact @@ -107,11 +151,16 @@ DLL_LINKAGE bool ArtifactUtils::isSlotEquipment(const ArtifactPosition & slot) DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, const size_t reqSlots) { - const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP); - if(backpackCap < 0) - return true; + if(target->bearerType() == ArtBearer::HERO) + { + const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP); + if(backpackCap < 0) + return true; + else + return target->artifactsInBackpack.size() + reqSlots <= backpackCap; + } else - return target->artifactsInBackpack.size() + reqSlots <= backpackCap; + return false; } DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( @@ -143,14 +192,14 @@ DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & sid) { - auto ret = new CArtifactInstance(VLC->arth->objects[ArtifactID::SPELL_SCROLL]); + auto ret = new CArtifactInstance(ArtifactID(ArtifactID::SPELL_SCROLL).toArtifact()); auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::SPELL, BonusSource::ARTIFACT_INSTANCE, -1, BonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), BonusSubtypeID(sid)); ret->addNewBonus(bonus); return ret; } -DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(CArtifact * art) +DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const CArtifact * art) { assert(art); @@ -172,7 +221,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(CArtifa DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const ArtifactID & aid) { - return ArtifactUtils::createNewArtifactInstance(VLC->arth->objects[aid]); + return ArtifactUtils::createNewArtifactInstance(aid.toArtifact()); } DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID) @@ -211,10 +260,13 @@ DLL_LINKAGE void ArtifactUtils::insertScrrollSpellName(std::string & description // However other language versions don't have name placeholder at all, so we have to be careful auto nameStart = description.find_first_of('['); auto nameEnd = description.find_first_of(']', nameStart); - if(sid.getNum() >= 0) + + if(nameStart != std::string::npos && nameEnd != std::string::npos) { - if(nameStart != std::string::npos && nameEnd != std::string::npos) - description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toSpell(VLC->spells())->getNameTranslated()); + if(sid.getNum() >= 0) + description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toEntity(VLC->spells())->getNameTranslated()); + else + description = description.erase(nameStart, nameEnd - nameStart + 2); // erase "[spell name] " - including space } } diff --git a/lib/ArtifactUtils.h b/lib/ArtifactUtils.h index 4b30f946d..74fe67db1 100644 --- a/lib/ArtifactUtils.h +++ b/lib/ArtifactUtils.h @@ -31,6 +31,8 @@ namespace ArtifactUtils // TODO: Make this constexpr when the toolset is upgraded DLL_LINKAGE const std::vector & unmovableSlots(); DLL_LINKAGE const std::vector & constituentWornSlots(); + DLL_LINKAGE const std::vector & allWornSlots(); + DLL_LINKAGE const std::vector & commanderSlots(); DLL_LINKAGE bool isArtRemovable(const std::pair & slot); DLL_LINKAGE bool checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, const ArtifactID & artID, const ArtifactPosition & slot); DLL_LINKAGE bool isSlotBackpack(const ArtifactPosition & slot); @@ -38,7 +40,7 @@ namespace ArtifactUtils DLL_LINKAGE bool isBackpackFreeSlots(const CArtifactSet * target, const size_t reqSlots = 1); DLL_LINKAGE std::vector assemblyPossibilities(const CArtifactSet * artSet, const ArtifactID & aid); DLL_LINKAGE CArtifactInstance * createScroll(const SpellID & sid); - DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(CArtifact * art); + DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const CArtifact * art); DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const ArtifactID & aid); DLL_LINKAGE CArtifactInstance * createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID = SpellID::NONE); DLL_LINKAGE void insertScrrollSpellName(std::string & description, const SpellID & sid); diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index a6e8d9466..b366bda55 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -13,7 +13,6 @@ #include "VCMI_Lib.h" #include "GameConstants.h" #include "GameSettings.h" -#include "JsonNode.h" #include "bonuses/BonusList.h" #include "bonuses/Bonus.h" #include "bonuses/IBonusBearer.h" @@ -93,6 +92,16 @@ int AFactionMember::getPrimSkillLevel(PrimarySkill id) const int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const { + int32_t maxGoodMorale = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size(); + int32_t maxBadMorale = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); + + if(getBonusBearer()->hasBonusOfType(BonusType::MAX_MORALE)) + { + if(bonusList && !bonusList->empty()) + bonusList = std::make_shared(); + return maxGoodMorale; + } + static const auto unaffectedByMoraleSelector = Selector::type()(BonusType::NON_LIVING).Or(Selector::type()(BonusType::UNDEAD)) .Or(Selector::type()(BonusType::SIEGE_WEAPON)).Or(Selector::type()(BonusType::NO_MORALE)); @@ -109,14 +118,21 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const static const std::string cachingStrMor = "type_MORALE"; bonusList = getBonusBearer()->getBonuses(moraleSelector, cachingStrMor); - int32_t maxGoodMorale = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size(); - int32_t maxBadMorale = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); - return std::clamp(bonusList->totalValue(), maxBadMorale, maxGoodMorale); } int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const { + int32_t maxGoodLuck = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size(); + int32_t maxBadLuck = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); + + if(getBonusBearer()->hasBonusOfType(BonusType::MAX_LUCK)) + { + if(bonusList && !bonusList->empty()) + bonusList = std::make_shared(); + return maxGoodLuck; + } + if(getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK)) { if(bonusList && !bonusList->empty()) @@ -128,9 +144,6 @@ int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const static const std::string cachingStrLuck = "type_LUCK"; bonusList = getBonusBearer()->getBonuses(luckSelector, cachingStrLuck); - int32_t maxGoodLuck = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size(); - int32_t maxBadLuck = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); - return std::clamp(bonusList->totalValue(), maxBadLuck, maxGoodLuck); } @@ -154,15 +167,14 @@ ui32 ACreature::getMaxHealth() const return std::max(1, value); //never 0 } -ui32 ACreature::speed(int turn, bool useBind) const +ui32 ACreature::getMovementRange(int turn) const { //war machines cannot move if(getBonusBearer()->hasBonus(Selector::type()(BonusType::SIEGE_WEAPON).And(Selector::turns(turn)))) { return 0; } - //bind effect check - doesn't influence stack initiative - if(useBind && getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn)))) + if(getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn)))) { return 0; } diff --git a/lib/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index b5bcedb90..4d1eb5591 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -11,7 +11,7 @@ #include #include "BattleFieldHandler.h" -#include "JsonNode.h" +#include "json/JsonBonus.h" VCMI_LIB_NAMESPACE_BEGIN @@ -21,6 +21,7 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co auto * info = new BattleFieldInfo(BattleField(index), identifier); + info->modScope = scope; info->graphics = ImagePath::fromJson(json["graphics"]); info->icon = json["icon"].String(); info->name = json["name"].String(); @@ -49,16 +50,11 @@ std::vector BattleFieldHandler::loadLegacyData() const std::vector & BattleFieldHandler::getTypeNames() const { - static const std::vector types = std::vector { "battlefield" }; + static const auto types = std::vector { "battlefield" }; return types; } -std::vector BattleFieldHandler::getDefaultAllowed() const -{ - return std::vector(); -} - int32_t BattleFieldInfo::getIndex() const { return battlefield.getNum(); @@ -71,7 +67,7 @@ int32_t BattleFieldInfo::getIconIndex() const std::string BattleFieldInfo::getJsonKey() const { - return identifier; + return modScope + ':' + identifier; } std::string BattleFieldInfo::getNameTextID() const diff --git a/lib/BattleFieldHandler.h b/lib/BattleFieldHandler.h index 9bacf79a2..b338cd772 100644 --- a/lib/BattleFieldHandler.h +++ b/lib/BattleFieldHandler.h @@ -27,6 +27,7 @@ public: bool isSpecial; ImagePath graphics; std::string name; + std::string modScope; std::string identifier; std::string icon; si32 iconIndex; @@ -53,19 +54,6 @@ public: std::string getNameTranslated() const override; void registerIcons(const IconRegistar & cb) const override; BattleField getId() const override; - - template void serialize(Handler & h, const int version) - { - h & name; - h & identifier; - h & isSpecial; - h & graphics; - h & icon; - h & iconIndex; - h & battlefield; - h & impassableHexes; - - } }; class DLL_LINKAGE BattleFieldService : public EntityServiceT @@ -82,14 +70,8 @@ public: const std::string & identifier, size_t index) override; - virtual const std::vector & getTypeNames() const override; - virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } + const std::vector & getTypeNames() const override; + std::vector loadLegacyData() override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 413a51bb2..d18bfe425 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -15,7 +15,7 @@ #include "GameSettings.h" #include "mapObjects/MapObjects.h" #include "constants/StringConstants.h" - +#include "json/JsonBonus.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "serializer/JsonSerializeFormat.h" @@ -49,12 +49,12 @@ bool CCombinedArtifact::isCombined() const return !(constituents.empty()); } -const std::vector & CCombinedArtifact::getConstituents() const +const std::vector & CCombinedArtifact::getConstituents() const { return constituents; } -const std::vector & CCombinedArtifact::getPartOf() const +const std::vector & CCombinedArtifact::getPartOf() const { return partOf; } @@ -167,7 +167,7 @@ bool CArtifact::isBig() const bool CArtifact::isTradable() const { - switch(id) + switch(id.toEnum()) { case ArtifactID::SPELLBOOK: case ArtifactID::GRAIL: @@ -181,9 +181,9 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b { auto simpleArtCanBePutAt = [this](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool { - if(ArtifactUtils::isSlotBackpack(slot)) + if(artSet->bearerType() == ArtBearer::HERO && ArtifactUtils::isSlotBackpack(slot)) { - if(isBig() || !ArtifactUtils::isBackpackFreeSlots(artSet)) + if(isBig() || (!assumeDestRemoved && !ArtifactUtils::isBackpackFreeSlots(artSet))) return false; return true; } @@ -258,6 +258,7 @@ CArtifact::CArtifact() possibleSlots[ArtBearer::HERO]; //we want to generate map entry even if it will be empty possibleSlots[ArtBearer::CREATURE]; //we want to generate map entry even if it will be empty possibleSlots[ArtBearer::COMMANDER]; + possibleSlots[ArtBearer::ALTAR]; } //This destructor should be placed here to avoid side effects @@ -328,7 +329,7 @@ std::vector CArtHandler::loadLegacyData() const std::vector artSlots = { ART_POS_LIST }; #undef ART_POS - static std::map classes = + static const std::map classes = {{'S',"SPECIAL"}, {'T',"TREASURE"},{'N',"MINOR"},{'J',"MAJOR"},{'R',"RELIC"},}; CLegacyConfigParser parser(TextPath::builtin("DATA/ARTRAITS.TXT")); @@ -349,11 +350,10 @@ std::vector CArtHandler::loadLegacyData() { if(parser.readString() == "x") { - artData["slot"].Vector().push_back(JsonNode()); - artData["slot"].Vector().back().String() = artSlot; + artData["slot"].Vector().emplace_back(artSlot); } } - artData["class"].String() = classes[parser.readString()[0]]; + artData["class"].String() = classes.at(parser.readString()[0]); artData["text"]["description"].String() = parser.readString(); parser.endLine(); @@ -371,7 +371,7 @@ void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode objects.emplace_back(object); - registerObject(scope, "artifact", name, object->id); + registerObject(scope, "artifact", name, object->id.getNum()); } void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -383,7 +383,7 @@ void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode assert(objects[index] == nullptr); // ensure that this id was not loaded before objects[index] = object; - registerObject(scope, "artifact", name, object->id); + registerObject(scope, "artifact", name, object->id.getNum()); } const std::vector & CArtHandler::getTypeNames() const @@ -460,7 +460,7 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode VLC->identifiers()->requestIdentifier(scope, "object", "artifact", [=](si32 index) { JsonNode conf; - conf.setMeta(scope); + conf.setModScope(scope); VLC->objtypeh->loadSubObject(art->identifier, conf, Obj::ARTIFACT, art->getIndex()); @@ -468,17 +468,17 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode { JsonNode templ; templ["animation"].String() = art->advMapDef; - templ.setMeta(scope); + templ.setModScope(scope); // add new template. // Necessary for objects added via mods that don't have any templates in H3 VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->addTemplate(templ); } - // object does not have any templates - this is not usable object (e.g. pseudo-art like lock) - if(VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->getTemplates().empty()) - VLC->objtypeh->removeSubObject(Obj::ARTIFACT, art->getIndex()); }); + if(art->isTradable()) + art->possibleSlots.at(ArtBearer::ALTAR).push_back(ArtifactPosition::ALTAR); + return art; } @@ -600,82 +600,13 @@ void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) { // when this code is called both combinational art as well as component are loaded // so it is safe to access any of them - art->constituents.push_back(objects[id]); + art->constituents.push_back(ArtifactID(id).toArtifact()); objects[id]->partOf.push_back(art); }); } } } -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) -{ - std::set potentialPicks; - - // Select artifacts that satisfy provided criterias - for (auto const * artifact : allowedArtifacts) - { - assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized - - if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE) - continue; - - if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR) - continue; - - if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR) - continue; - - if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC) - continue; - - if (!accepts(artifact->id)) - continue; - - potentialPicks.insert(artifact->id); - } - - return pickRandomArtifact(rand, potentialPicks); -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) -{ - // No allowed artifacts at all - give Grail - this can't be banned (hopefully) - // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior - if (potentialPicks.empty()) - { - logGlobal->warn("Failed to find artifact that matches requested parameters!"); - return ArtifactID::GRAIL; - } - - // Find how many times least used artifacts were picked by randomizer - int leastUsedTimes = std::numeric_limits::max(); - for (auto const & artifact : potentialPicks) - if (allocatedArtifacts[artifact] < leastUsedTimes) - leastUsedTimes = allocatedArtifacts[artifact]; - - // Pick all artifacts that were used least number of times - std::set preferredPicks; - for (auto const & artifact : potentialPicks) - if (allocatedArtifacts[artifact] == leastUsedTimes) - preferredPicks.insert(artifact); - - assert(!preferredPicks.empty()); - - ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand); - allocatedArtifacts[artID] += 1; // record +1 more usage - return artID; -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) -{ - return pickRandomArtifact(rand, 0xff, std::move(accepts)); -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags) -{ - return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); -} - void CArtHandler::makeItCreatureArt(CArtifact * a, bool onlyCreature) { if (onlyCreature) @@ -693,13 +624,13 @@ void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) a->possibleSlots[ArtBearer::HERO].clear(); a->possibleSlots[ArtBearer::CREATURE].clear(); } - for (int i = ArtifactPosition::COMMANDER1; i <= ArtifactPosition::COMMANDER6; ++i) - a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(i)); + for(const auto & slot : ArtifactUtils::commanderSlots()) + a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(slot)); } -bool CArtHandler::legalArtifact(const ArtifactID & id) +bool CArtHandler::legalArtifact(const ArtifactID & id) const { - auto art = objects[id]; + auto art = id.toArtifact(); //assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components if(art->isCombined()) @@ -708,37 +639,27 @@ bool CArtHandler::legalArtifact(const ArtifactID & id) if(art->aClass < CArtifact::ART_TREASURE || art->aClass > CArtifact::ART_RELIC) return false; // invalid class - if(!art->possibleSlots[ArtBearer::HERO].empty()) + if(art->possibleSlots.count(ArtBearer::HERO) && !art->possibleSlots.at(ArtBearer::HERO).empty()) return true; - if(!art->possibleSlots[ArtBearer::CREATURE].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) + if(art->possibleSlots.count(ArtBearer::CREATURE) && !art->possibleSlots.at(ArtBearer::CREATURE).empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) return true; - if(!art->possibleSlots[ArtBearer::COMMANDER].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) + if(art->possibleSlots.count(ArtBearer::COMMANDER) && !art->possibleSlots.at(ArtBearer::COMMANDER).empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) return true; return false; } -void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) +std::set CArtHandler::getDefaultAllowed() const { - allowedArtifacts.clear(); - allocatedArtifacts.clear(); + std::set allowedArtifacts; - for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) + for (auto artifact : objects) { - if (allowed[i] && legalArtifact(ArtifactID(i))) - allowedArtifacts.push_back(objects[i]); - //keep im mind that artifact can be worn by more than one type of bearer + if (!artifact->isCombined()) + allowedArtifacts.insert(artifact->getId()); } -} - -std::vector CArtHandler::getDefaultAllowed() const -{ - std::vector allowedArtifacts; - allowedArtifacts.resize(127, true); - allowedArtifacts.resize(141, false); - allowedArtifacts.resize(size(), true); return allowedArtifacts; } @@ -749,7 +670,6 @@ void CArtHandler::afterLoadFinalization() { for(auto &bonus : art->getExportedBonusList()) { - assert(art == objects[art->id]); assert(bonus->source == BonusSource::ARTIFACT); bonus->sid = BonusSourceID(art->id); } @@ -975,9 +895,9 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const } if(vstd::contains(artifactsWorn, pos)) return &artifactsWorn.at(pos); - if(pos >= ArtifactPosition::AFTER_LAST ) + if(ArtifactUtils::isSlotBackpack(pos)) { - int backpackPos = (int)pos - ArtifactPosition::BACKPACK_START; + auto backpackPos = pos - ArtifactPosition::BACKPACK_START; if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size()) return nullptr; else @@ -989,6 +909,9 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const { + if(bearerType() == ArtBearer::ALTAR) + return artifactsInBackpack.size() < GameConstants::ALTAR_ARTIFACTS_SLOTS; + if(const ArtSlotInfo *s = getSlot(pos)) return (onlyLockCheck || !s->artifact) && !s->locked; @@ -1080,9 +1003,9 @@ void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const s void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) { - for(ArtifactPosition ap = ArtifactPosition::HEAD; ap < ArtifactPosition::AFTER_LAST; ap.advance(1)) + for(const auto & slot : ArtifactUtils::allWornSlots()) { - serializeJsonSlot(handler, ap, map); + serializeJsonSlot(handler, slot, map); } std::vector backpackTemp; diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 898c5ec23..a4c416028 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -30,7 +30,8 @@ class JsonSerializeFormat; #define ART_BEARER_LIST \ ART_BEARER(HERO)\ ART_BEARER(CREATURE)\ - ART_BEARER(COMMANDER) + ART_BEARER(COMMANDER)\ + ART_BEARER(ALTAR) namespace ArtBearer { @@ -47,18 +48,12 @@ class DLL_LINKAGE CCombinedArtifact protected: CCombinedArtifact() = default; - std::vector constituents; // Artifacts IDs a combined artifact consists of, or nullptr. - std::vector partOf; // Reverse map of constituents - combined arts that include this art + std::vector constituents; // Artifacts IDs a combined artifact consists of, or nullptr. + std::vector partOf; // Reverse map of constituents - combined arts that include this art public: bool isCombined() const; - const std::vector & getConstituents() const; - const std::vector & getPartOf() const; - - template void serialize(Handler & h, const int version) - { - h & constituents; - h & partOf; - } + const std::vector & getConstituents() const; + const std::vector & getPartOf() const; }; class DLL_LINKAGE CScrollArtifact @@ -83,12 +78,6 @@ public: const std::vector > & getBonusesPerLevel() const; std::vector > & getThresholdBonuses(); const std::vector > & getThresholdBonuses() const; - - template void serialize(Handler & h, const int version) - { - h & bonusesPerLevel; - h & thresholdBonuses; - } }; // Container for artifacts. Not for instances. @@ -118,7 +107,7 @@ public: std::string getJsonKey() const override; void registerIcons(const IconRegistar & cb) const override; ArtifactID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; + const IBonusBearer * getBonusBearer() const override; std::string getDescriptionTranslated() const override; std::string getEventTranslated() const override; @@ -144,25 +133,6 @@ public: // Is used for testing purposes only void setImage(int32_t iconIndex, std::string image, std::string large); - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & static_cast(*this); - h & static_cast(*this); - h & image; - h & large; - h & advMapDef; - h & iconIndex; - h & price; - h & possibleSlots; - h & aClass; - h & id; - h & modScope; - h & identifier; - h & warMachine; - h & onlyOnWaterMap; - } - CArtifact(); ~CArtifact(); @@ -172,24 +142,11 @@ public: class DLL_LINKAGE CArtHandler : public CHandlerBase { public: - /// Stores number of times each artifact was placed on map via randomization - std::map allocatedArtifacts; - - /// List of artifacts allowed on the map - std::vector allowedArtifacts; - void addBonuses(CArtifact *art, const JsonNode &bonusList); static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor - /// Gets a artifact ID randomly and removes the selected artifact from this handler. - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set filtered); - - bool legalArtifact(const ArtifactID & id); - void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed + bool legalArtifact(const ArtifactID & id) const; static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true); static void makeItCommanderArt(CArtifact * a, bool onlyCommander = true); @@ -201,14 +158,7 @@ public: void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - h & objects; - h & allowedArtifacts; - h & allocatedArtifacts; - } + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; @@ -230,7 +180,7 @@ struct DLL_LINKAGE ArtSlotInfo ArtSlotInfo() : locked(false) {} const CArtifactInstance * getArt() const; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & artifact; h & locked; @@ -274,7 +224,7 @@ public: virtual void removeArtifact(ArtifactPosition slot); virtual ~CArtifactSet(); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & artifactsInBackpack; h & artifactsWorn; diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index d80373251..0451e2d2f 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -62,12 +62,9 @@ void CCombinedArtifactInstance::addPlacementMap(CArtifactSet::ArtPlacementMap & SpellID CScrollArtifactInstance::getScrollSpellID() const { auto artInst = static_cast(this); - const auto bonus = artInst->getBonusLocalFirst(Selector::type()(BonusType::SPELL)); + const auto bonus = artInst->getFirstBonus(Selector::type()(BonusType::SPELL)); if(!bonus) - { - logMod->warn("Warning: %s doesn't bear any spell!", artInst->nodeName()); return SpellID::NONE; - } return bonus->subtype.as(); } @@ -110,7 +107,7 @@ void CArtifactInstance::init() setNodeType(ARTIFACT_INSTANCE); } -CArtifactInstance::CArtifactInstance(CArtifact * art) +CArtifactInstance::CArtifactInstance(const CArtifact * art) { init(); setType(art); @@ -121,10 +118,10 @@ CArtifactInstance::CArtifactInstance() init(); } -void CArtifactInstance::setType(CArtifact * art) +void CArtifactInstance::setType(const CArtifact * art) { artType = art; - attachTo(*art); + attachToSource(*art); } std::string CArtifactInstance::nodeName() const @@ -155,9 +152,9 @@ void CArtifactInstance::setId(ArtifactInstanceID id) this->id = id; } -bool CArtifactInstance::canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved) const +bool CArtifactInstance::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const { - return artType->canBePutAt(al.getHolderArtSet(), al.slot, assumeDestRemoved); + return artType->canBePutAt(artSet, slot, assumeDestRemoved); } bool CArtifactInstance::isCombined() const @@ -165,15 +162,20 @@ bool CArtifactInstance::isCombined() const return artType->isCombined(); } -void CArtifactInstance::putAt(const ArtifactLocation & al) +bool CArtifactInstance::isScroll() const { - auto placementMap = al.getHolderArtSet()->putArtifact(al.slot, this); + return artType->isScroll(); +} + +void CArtifactInstance::putAt(CArtifactSet & set, const ArtifactPosition slot) +{ + auto placementMap = set.putArtifact(slot, this); addPlacementMap(placementMap); } -void CArtifactInstance::removeFrom(const ArtifactLocation & al) +void CArtifactInstance::removeFrom(CArtifactSet & set, const ArtifactPosition slot) { - al.getHolderArtSet()->removeArtifact(al.slot); + set.removeArtifact(slot); for(auto & part : partsInfo) { if(part.slot != ArtifactPosition::PRE_FIRST) @@ -181,10 +183,10 @@ void CArtifactInstance::removeFrom(const ArtifactLocation & al) } } -void CArtifactInstance::move(const ArtifactLocation & src, const ArtifactLocation & dst) +void CArtifactInstance::move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot) { - removeFrom(src); - putAt(dst); + removeFrom(srcSet, srcSlot); + putAt(dstSet, dstSlot); } void CArtifactInstance::deserializationFix() diff --git a/lib/CArtifactInstance.h b/lib/CArtifactInstance.h index 2c87adf08..7c100a91f 100644 --- a/lib/CArtifactInstance.h +++ b/lib/CArtifactInstance.h @@ -27,7 +27,7 @@ public: { ConstTransitivePtr art; ArtifactPosition slot; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & art; h & slot; @@ -41,7 +41,7 @@ public: const std::vector & getPartsInfo() const; void addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & partsInfo; } @@ -73,25 +73,27 @@ protected: ArtifactInstanceID id; public: - ConstTransitivePtr artType; + const CArtifact * artType = nullptr; - CArtifactInstance(CArtifact * art); + CArtifactInstance(const CArtifact * art); CArtifactInstance(); - void setType(CArtifact * art); + void setType(const CArtifact * art); std::string nodeName() const override; std::string getDescription() const; ArtifactID getTypeId() const; ArtifactInstanceID getId() const; void setId(ArtifactInstanceID id); - bool canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved = false) const; + bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE, + bool assumeDestRemoved = false) const; bool isCombined() const; - void putAt(const ArtifactLocation & al); - void removeFrom(const ArtifactLocation & al); - void move(const ArtifactLocation & src, const ArtifactLocation & dst); + bool isScroll() const; + void putAt(CArtifactSet & set, const ArtifactPosition slot); + void removeFrom(CArtifactSet & set, const ArtifactPosition slot); + void move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot); void deserializationFix(); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & static_cast(*this); diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index b5cce121f..c77a66c3b 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -13,12 +13,12 @@ #include "CBonusTypeHandler.h" -#include "JsonNode.h" #include "filesystem/Filesystem.h" #include "GameConstants.h" #include "CCreatureHandler.h" #include "CGeneralTextHandler.h" +#include "json/JsonUtils.h" #include "spells/CSpellHandler.h" template class std::vector; @@ -76,10 +76,10 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonu if (text.find("${val}") != std::string::npos) boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype)))); - if (text.find("${subtype.creature}") != std::string::npos) + if (text.find("${subtype.creature}") != std::string::npos && bonus->subtype.as().hasValue()) boost::algorithm::replace_all(text, "${subtype.creature}", bonus->subtype.as().toCreature()->getNamePluralTranslated()); - if (text.find("${subtype.spell}") != std::string::npos) + if (text.find("${subtype.spell}") != std::string::npos && bonus->subtype.as().hasValue()) boost::algorithm::replace_all(text, "${subtype.spell}", bonus->subtype.as().toSpell()->getNameTranslated()); return text; @@ -95,8 +95,11 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu case BonusType::SPELL_IMMUNITY: { fullPath = true; - const CSpell * sp = bonus->subtype.as().toSpell(); - fileName = sp->getIconImmune(); + if (bonus->subtype.as().hasValue()) + { + const CSpell * sp = bonus->subtype.as().toSpell(); + fileName = sp->getIconImmune(); + } break; } case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools @@ -174,6 +177,9 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu if (bonus->subtype == BonusCustomSubtype::damageTypeRanged) fileName = "DamageReductionRanged.bmp"; + if (bonus->subtype == BonusCustomSubtype::damageTypeAll) + fileName = "DamageReductionAll.bmp"; + break; } diff --git a/lib/CBonusTypeHandler.h b/lib/CBonusTypeHandler.h index 2ddecfb30..9969f28c6 100644 --- a/lib/CBonusTypeHandler.h +++ b/lib/CBonusTypeHandler.h @@ -27,7 +27,7 @@ public: std::string getNameTextID() const; std::string getDescriptionTextID() const; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & icon; h & identifier; @@ -53,7 +53,7 @@ public: std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const override; ImagePath bonusToGraphics(const std::shared_ptr & bonus) const override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { //for now always use up to date configuration //once modded bonus type will be implemented, serialize only them diff --git a/lib/CBuildingHandler.cpp b/lib/CBuildingHandler.cpp index 6db07ac15..be986ee8b 100644 --- a/lib/CBuildingHandler.cpp +++ b/lib/CBuildingHandler.cpp @@ -12,7 +12,7 @@ VCMI_LIB_NAMESPACE_BEGIN -BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set & builtBuildings) +BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std::set & builtBuildings) { static const std::vector campToERMU = { @@ -47,13 +47,13 @@ BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set 1) + if(hordeLvlsPerTType[townType.getNum()].size() > 1) { - BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType][1]); + BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType.getNum()][1]); if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built return BuildingID::HORDE_2_UPGR; diff --git a/lib/CBuildingHandler.h b/lib/CBuildingHandler.h index a0aab5615..059b9f087 100644 --- a/lib/CBuildingHandler.h +++ b/lib/CBuildingHandler.h @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CBuildingHandler { public: - static BuildingID campToERMU(int camp, int townType, const std::set & builtBuildings); + static BuildingID campToERMU(int camp, FactionID townType, const std::set & builtBuildings); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 5df984740..c89c125ee 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -10,9 +10,10 @@ #include "StdInc.h" #include "CConfigHandler.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/GameConstants.h" -#include "../lib/VCMIDirs.h" +#include "filesystem/Filesystem.h" +#include "GameConstants.h" +#include "VCMIDirs.h" +#include "json/JsonUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -89,7 +90,7 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath JsonUtils::minimize(savedConf, schema); std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(dataFilename))->c_str(), std::ofstream::out | std::ofstream::trunc); - file << savedConf.toJson(); + file << savedConf.toString(); } JsonNode & SettingsStorage::getNode(const std::vector & path) diff --git a/lib/CConfigHandler.h b/lib/CConfigHandler.h index a446d8e5c..915863a52 100644 --- a/lib/CConfigHandler.h +++ b/lib/CConfigHandler.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/JsonNode.h" +#include "json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index 72de08baa..a0eca4814 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -143,13 +143,6 @@ LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) HANDLE dfile = CreateFileA(mname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); logGlobal->error("Crash info will be put in %s", mname); - // flush loggers - std::string padding(1000, '@'); - - logGlobal->error(padding); - logAi->error(padding); - logNetwork->error(padding); - auto dumpType = MiniDumpWithDataSegs; if(settings["general"]["extraDump"].Bool()) diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index eb03049ce..21fda1133 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -14,11 +14,13 @@ #include "ResourceSet.h" #include "filesystem/Filesystem.h" #include "VCMI_Lib.h" +#include "CRandomGenerator.h" #include "CTownHandler.h" #include "GameSettings.h" #include "constants/StringConstants.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" +#include "json/JsonBonus.h" #include "serializer/JsonDeserializer.h" #include "serializer/JsonUpdater.h" #include "mapObjectConstructors/AObjectTypeHandler.h" @@ -263,7 +265,7 @@ bool CCreature::isDoubleWide() const */ bool CCreature::isGood () const { - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::GOOD; + return VLC->factions()->getById(faction)->getAlignment() == EAlignment::GOOD; } /** @@ -272,7 +274,7 @@ bool CCreature::isGood () const */ bool CCreature::isEvil () const { - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::EVIL; + return VLC->factions()->getById(faction)->getAlignment() == EAlignment::EVIL; } si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought @@ -324,7 +326,7 @@ bool CCreature::isMyUpgrade(const CCreature *anotherCre) const bool CCreature::valid() const { - return this == VLC->creh->objects[idNumber]; + return this == (*VLC->creh)[idNumber]; } std::string CCreature::nodeName() const @@ -388,7 +390,7 @@ void CCreature::serializeJson(JsonSerializeFormat & handler) if(handler.updating) { cost.serializeJson(handler, "cost"); - handler.serializeInt("faction", faction);//TODO: unify with deferred resolve + handler.serializeId("faction", faction); } handler.serializeInt("level", level); @@ -397,34 +399,26 @@ void CCreature::serializeJson(JsonSerializeFormat & handler) if(!handler.saving) { if(ammMin > ammMax) + { logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier); + std::swap(ammMin, ammMax); + } } } CCreatureHandler::CCreatureHandler() : expAfterUpgrade(0) { - VLC->creh = this; loadCommanders(); } -const CCreature * CCreatureHandler::getCreature(const std::string & scope, const std::string & identifier) const -{ - std::optional index = VLC->identifiers()->getIdentifier(scope, "creature", identifier); - - if(!index) - throw std::runtime_error("Creature not found "+identifier); - - return objects[*index]; -} - void CCreatureHandler::loadCommanders() { auto configResource = JsonPath::builtin("config/commanders.json"); std::string modSource = VLC->modh->findResourceOrigin(configResource); JsonNode data(configResource); - data.setMeta(modSource); + data.setModScope(modSource); const JsonNode & config = data; // switch to const data accessors @@ -612,10 +606,20 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); - cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); - cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); + int minDamage = node["damage"]["min"].Integer(); + int maxDamage = node["damage"]["max"].Integer(); - assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); + if (minDamage <= maxDamage) + { + cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); + cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); + } + else + { + logMod->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage); + cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); + cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); + } if(!node["shots"].isNull()) cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); @@ -636,7 +640,7 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json VLC->identifiers()->requestIdentifier(scope, "object", "monster", [=](si32 index) { JsonNode conf; - conf.setMeta(scope); + conf.setModScope(scope); VLC->objtypeh->loadSubObject(cre->identifier, conf, Obj::MONSTER, cre->getId().num); if (!advMapFile.isNull()) @@ -645,7 +649,7 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json templ["animation"] = advMapFile; if (!advMapMask.isNull()) templ["mask"] = advMapMask; - templ.setMeta(scope); + templ.setModScope(scope); // if creature has custom advMapFile, reset any potentially imported H3M templates and use provided file instead VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->clearTemplates(); @@ -666,18 +670,6 @@ const std::vector & CCreatureHandler::getTypeNames() const return typeNames; } -std::vector CCreatureHandler::getDefaultAllowed() const -{ - std::vector ret; - - ret.reserve(objects.size()); - for(const CCreature * crea : objects) - { - ret.push_back(crea ? !crea->special : false); - } - return ret; -} - void CCreatureHandler::loadCrExpMod() { if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values @@ -790,15 +782,13 @@ void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) } do //parse everything that's left { - CreatureID sid = static_cast(parser.readNumber()); //id = this particular creature ID + CreatureID sid = parser.readNumber(); //id = this particular creature ID b.sid = BonusSourceID(sid); bl.clear(); loadStackExp(b, bl, parser); for(const auto & b : bl) - { - objects[sid]->addNewBonus(b); //add directly to CCreature Node - } + objects[sid.getNum()]->addNewBonus(b); //add directly to CCreature Node } while (parser.endLine()); @@ -855,8 +845,8 @@ void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser missile["attackClimaxFrame"].Float() = parser.readNumber(); // assume that creature is not a shooter and should not have whole missile field - if (missile["frameAngles"].Vector()[0].Float() == 0 && - missile["attackClimaxFrame"].Float() == 0) + if (missile["frameAngles"].Vector()[0].Integer() == 0 && + missile["attackClimaxFrame"].Integer() == 0) graphics.Struct().erase("missile"); } @@ -998,10 +988,10 @@ void CCreatureHandler::loadStackExperience(CCreature * creature, const JsonNode int lastVal = 0; for (const JsonNode &val : values) { - if (val.Float() != lastVal) + if (val.Integer() != lastVal) { JsonNode bonusInput = exp["bonus"]; - bonusInput["val"].Float() = static_cast(val.Float()) - lastVal; + bonusInput["val"].Float() = val.Integer() - lastVal; auto bonus = JsonUtils::parseBonus (bonusInput); bonus->source = BonusSource::STACK_EXPERIENCE; @@ -1350,34 +1340,23 @@ CCreatureHandler::~CCreatureHandler() CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const { - int r = 0; - if(tier == -1) //pick any allowed creature + std::vector allowed; + for(const auto & creature : objects) { - do - { - r = (*RandomGeneratorUtil::nextItem(objects, rand))->getId(); - } while (objects[r] && objects[r]->special); // find first "not special" creature + if(creature->special) + continue; + + if (creature->level == tier || tier == -1) + allowed.push_back(creature->getId()); } - else + + if(allowed.empty()) { - assert(vstd::iswithin(tier, 1, 7)); - std::vector allowed; - for(const auto & creature : objects) - { - if(!creature->special && creature->level == tier) - allowed.push_back(creature->getId()); - } - - if(allowed.empty()) - { - logGlobal->warn("Cannot pick a random creature of tier %d!", tier); - return CreatureID::NONE; - } - - return *RandomGeneratorUtil::nextItem(allowed, rand); + logGlobal->warn("Cannot pick a random creature of tier %d!", tier); + return CreatureID::NONE; } - assert (r >= 0); //should always be, but it crashed - return CreatureID(r); + + return *RandomGeneratorUtil::nextItem(allowed, rand); } diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index ab8d57f2c..b38652ba4 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -14,9 +14,7 @@ #include "ConstTransitivePtr.h" #include "ResourceSet.h" #include "GameConstants.h" -#include "JsonNode.h" #include "IHandlerBase.h" -#include "CRandomGenerator.h" #include "Color.h" #include "filesystem/ResourcePath.h" @@ -29,6 +27,7 @@ class CLegacyConfigParser; class CCreatureHandler; class CCreature; class JsonSerializeFormat; +class CRandomGenerator; class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode { @@ -52,7 +51,8 @@ class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode TResources cost; //cost[res_id] - amount of that resource required to buy creature from dwelling public: - ui32 ammMin, ammMax; // initial size of stack of these creatures on adventure map (if not set in editor) + ui32 ammMin; // initial size of stack of these creatures on adventure map (if not set in editor) + ui32 ammMax; bool special = true; // Creature is not available normally (war machines, commanders, several unused creatures, etc @@ -83,11 +83,6 @@ public: struct RayColor { ColorRGBA start; ColorRGBA end; - - template void serialize(Handler &h, const int version) - { - h & start & end; - } }; double timeBetweenFidgets, idleAnimationTime, @@ -100,25 +95,7 @@ public: AnimationPath projectileImageName; std::vector projectileRay; - //bool projectileSpin; //if true, appropriate projectile is spinning during flight - template void serialize(Handler &h, const int version) - { - h & timeBetweenFidgets; - h & idleAnimationTime; - h & walkAnimationTime; - h & attackAnimationTime; - h & upperRightMissleOffsetX; - h & rightMissleOffsetX; - h & lowerRightMissleOffsetX; - h & upperRightMissleOffsetY; - h & rightMissleOffsetY; - h & lowerRightMissleOffsetY; - h & missleFrameAngles; - h & attackClimaxFrame; - h & projectileImageName; - h & projectileRay; - } } animation; //sound info @@ -132,18 +109,6 @@ public: AudioPath wince; // attacked but did not die AudioPath startMoving; AudioPath endMoving; - - template void serialize(Handler &h, const int version) - { - h & attack; - h & defend; - h & killed; - h & move; - h & shoot; - h & wince; - h & startMoving; - h & endMoving; - } } sounds; ArtifactID warMachine; @@ -160,7 +125,7 @@ public: std::string getJsonKey() const override; void registerIcons(const IconRegistar & cb) const override; CreatureID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; + const IBonusBearer * getBonusBearer() const override; int32_t getAdvMapAmountMin() const override; int32_t getAdvMapAmountMax() const override; @@ -210,35 +175,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & cost; - h & upgrades; - h & fightValue; - h & AIValue; - h & growth; - h & hordeGrowth; - h & ammMin; - h & ammMax; - h & level; - h & animDefName; - h & iconIndex; - h & smallIconName; - h & largeIconName; - - h & idNumber; - h & faction; - h & sounds; - h & animation; - - h & doubleWide; - h & special; - h & identifier; - h & modScope; - h & warMachine; - } - CCreature(); private: @@ -285,8 +221,6 @@ public: std::vector< std::vector > skillLevels; //how much of a bonus will be given to commander with every level. SPELL_POWER also gives CASTS and RESISTANCE std::vector , std::pair > > skillRequirements; // first - Bonus, second - which two skills are needed to use it - const CCreature * getCreature(const std::string & scope, const std::string & identifier) const; - CreatureID pickRandomMonster(CRandomGenerator & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any CCreatureHandler(); @@ -302,20 +236,6 @@ public: std::vector loadLegacyData() override; - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - //TODO: should be optimized, not all these informations needs to be serialized (same for ccreature) - h & doubledCreatures; - h & objects; - h & expRanks; - h & maxExpPerBattle; - h & expAfterUpgrade; - h & skillLevels; - h & skillRequirements; - h & commanderLevelPremy; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 34849d337..71c3fb0d5 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -79,7 +79,7 @@ bool CCreatureSet::setCreature(SlotID slot, CreatureID type, TQuantity quantity) SlotID CCreatureSet::getSlotFor(const CreatureID & creature, ui32 slotsAmount) const /*returns -1 if no slot available */ { - return getSlotFor(VLC->creh->objects[creature], slotsAmount); + return getSlotFor(creature.toCreature(), slotsAmount); } SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const @@ -141,7 +141,7 @@ bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmoun { assert(c && c->valid()); TQuantity max = 0; - TQuantity min = std::numeric_limits::max(); + auto min = std::numeric_limits::max(); for(const auto & elem : stacks) { @@ -304,7 +304,7 @@ void CCreatureSet::sweep() void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging) { - const CCreature *c = VLC->creh->objects[cre]; + const CCreature *c = cre.toCreature(); if(!hasStackAtSlot(slot)) { @@ -415,12 +415,9 @@ int CCreatureSet::stacksCount() const return static_cast(stacks.size()); } -void CCreatureSet::setFormation(bool tight) +void CCreatureSet::setFormation(EArmyFormation mode) { - if (tight) - formation = EArmyFormation::TIGHT; - else - formation = EArmyFormation::LOOSE; + formation = mode; } void CCreatureSet::setStackCount(const SlotID & slot, TQuantity count) @@ -458,7 +455,7 @@ const CStackInstance & CCreatureSet::getStack(const SlotID & slot) const return *getStackPtr(slot); } -const CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const +CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const { if(hasStackAtSlot(slot)) return stacks.find(slot)->second; @@ -742,27 +739,26 @@ void CStackInstance::giveStackExp(TExpType exp) if (!vstd::iswithin(level, 1, 7)) level = 0; - CCreatureHandler * creh = VLC->creh; - ui32 maxExp = creh->expRanks[level].back(); + ui32 maxExp = VLC->creh->expRanks[level].back(); vstd::amin(exp, static_cast(maxExp)); //prevent exp overflow due to different types - vstd::amin(exp, (maxExp * creh->maxExpPerBattle[level])/100); + vstd::amin(exp, (maxExp * VLC->creh->maxExpPerBattle[level])/100); vstd::amin(experience += exp, maxExp); //can't get more exp than this limit } void CStackInstance::setType(const CreatureID & creID) { - if(creID.getNum() >= 0 && creID.getNum() < VLC->creh->objects.size()) - setType(VLC->creh->objects[creID]); + if (creID == CreatureID::NONE) + setType(nullptr);//FIXME: unused branch? else - setType((const CCreature*)nullptr); + setType(creID.toCreature()); } void CStackInstance::setType(const CCreature *c) { if(type) { - detachFrom(const_cast(*type)); + detachFromSource(*type); if (type->isMyUpgrade(c) && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) experience = static_cast(experience * VLC->creh->expAfterUpgrade / 100.0); } @@ -770,7 +766,7 @@ void CStackInstance::setType(const CCreature *c) CStackBasicDescriptor::setType(c); if(type) - attachTo(const_cast(*type)); + attachToSource(*type); } std::string CStackInstance::bonusToString(const std::shared_ptr& bonus, bool description) const { @@ -812,7 +808,7 @@ bool CStackInstance::valid(bool allowUnrandomized) const { if(!randomStack) { - return (type && type == VLC->creh->objects[type->getId()]); + return (type && type == type->getId().toEntity(VLC)); } else return allowUnrandomized; @@ -870,7 +866,7 @@ ArtBearer::ArtBearer CStackInstance::bearerType() const CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) { assert(!getArt(pos)); - assert(art->artType->canBePutAt(this, pos)); + assert(art->canBePutAt(this, pos)); attachTo(*art); return CArtifactSet::putArtifact(pos, art); @@ -995,14 +991,14 @@ ArtBearer::ArtBearer CCommanderInstance::bearerType() const bool CCommanderInstance::gainsLevel() const { - return experience >= static_cast(VLC->heroh->reqExp(level + 1)); + return experience >= VLC->heroh->reqExp(level + 1); } //This constructor should be placed here to avoid side effects CStackBasicDescriptor::CStackBasicDescriptor() = default; CStackBasicDescriptor::CStackBasicDescriptor(const CreatureID & id, TQuantity Count): - type(VLC->creh->objects[id]), + type(id.toCreature()), count(Count) { } @@ -1017,6 +1013,11 @@ const Creature * CStackBasicDescriptor::getType() const return type; } +CreatureID CStackBasicDescriptor::getId() const +{ + return type->getId(); +} + TQuantity CStackBasicDescriptor::getCount() const { return count; @@ -1053,7 +1054,7 @@ void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) std::string typeName; handler.serializeString("type", typeName); if(!typeName.empty()) - setType(VLC->creh->getCreature(ModScope::scopeMap(), typeName)); + setType(CreatureID(CreatureID::decode(typeName)).toCreature()); } } diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 634e62ba4..10654b46a 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -39,13 +39,14 @@ public: virtual ~CStackBasicDescriptor() = default; const Creature * getType() const; + CreatureID getId() const; TQuantity getCount() const; virtual void setType(const CCreature * c); friend bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { if(h.saving) { @@ -84,7 +85,7 @@ public: const CArmedInstance * const & armyObj; //stack must be part of some army, army must be part of some object TExpType experience;//commander needs same amount of exp as hero - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); @@ -126,7 +127,7 @@ public: ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet void removeArtifact(ArtifactPosition pos) override; ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet - virtual std::string nodeName() const override; //from CBonusSystemnode + std::string nodeName() const override; //from CBonusSystemnode void deserializationFix(); PlayerColor getOwner() const override; }; @@ -156,7 +157,7 @@ public: int getLevel() const override; ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & alive; @@ -196,18 +197,12 @@ public: bool setCreature(SlotID slot, CreatureID cre, TQuantity count) override; operator bool() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & army; } }; -enum class EArmyFormation : uint8_t -{ - LOOSE, - TIGHT -}; - namespace NArmyFormation { static const std::vector names{ "wide", "tight" }; @@ -234,7 +229,7 @@ public: void addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature void addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature void clearSlots() override; - void setFormation(bool tight); + void setFormation(EArmyFormation tight); CArmedInstance *castToArmyObj(); //basic operations @@ -253,7 +248,7 @@ public: void setToArmy(CSimpleArmy &src); //erases all our army and moves stacks from src to us; src MUST NOT be an armed object! WARNING: use it wisely. Or better do not use at all. const CStackInstance & getStack(const SlotID & slot) const; //stack must exist - const CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr + CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr const CCreature * getCreature(const SlotID & slot) const; //workaround of map issue; int getStackCount(const SlotID & slot) const; TExpType getStackExperience(const SlotID & slot) const; @@ -285,7 +280,7 @@ public: bool contains(const CStackInstance *stack) const; bool canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks = true) const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & stacks; h & formation; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 5ab149426..8f9a7b4f9 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -16,6 +16,9 @@ #include "gameState/TavernHeroesPool.h" #include "gameState/QuestInfo.h" #include "mapObjects/CGHeroInstance.h" +#include "mapObjects/CGTownInstance.h" +#include "mapObjects/MiscObjects.h" +#include "networkPacks/ArtifactLocation.h" #include "CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo @@ -52,19 +55,19 @@ const PlayerSettings * CGameInfoCallback::getPlayerSettings(PlayerColor color) c return &gs->scenarioOps->getIthPlayersSettings(color); } -bool CGameInfoCallback::isAllowed(int32_t type, int32_t id) const +bool CGameInfoCallback::isAllowed(SpellID id) const { - switch(type) - { - case 0: - return gs->map->allowedSpells[id]; - case 1: - return gs->map->allowedArtifact[id]; - case 2: - return gs->map->allowedAbilities[id]; - default: - ERROR_RET_VAL_IF(1, "Wrong type!", false); - } + return gs->map->allowedSpells.count(id) != 0; +} + +bool CGameInfoCallback::isAllowed(ArtifactID id) const +{ + return gs->map->allowedArtifact.count(id) != 0; +} + +bool CGameInfoCallback::isAllowed(SecondarySkill id) const +{ + return gs->map->allowedAbilities.count(id) != 0; } std::optional CGameInfoCallback::getPlayerID() const @@ -121,20 +124,6 @@ TurnTimerInfo CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const return TurnTimerInfo{}; } -const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const -{ - if(gs->map->questIdentifierToId.empty()) - { - //assume that it is VCMI map and quest identifier equals instance identifier - return getObj(ObjectInstanceID(identifier), true); - } - else - { - ERROR_RET_VAL_IF(!vstd::contains(gs->map->questIdentifierToId, identifier), "There is no object with such quest identifier!", nullptr); - return getObj(gs->map->questIdentifierToId[identifier]); - } -} - /************************************************************************/ /* */ /************************************************************************/ @@ -381,7 +370,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero if(creature->getFaction() == factionIndex && static_cast(creature->getAIValue()) > maxAIValue) { maxAIValue = creature->getAIValue(); - mostStrong = creature; + mostStrong = creature.get(); } } @@ -720,15 +709,14 @@ bool CGameInfoCallback::isPlayerMakingTurn(PlayerColor player) const return gs->actingPlayers.count(player); } -CGameInfoCallback::CGameInfoCallback(CGameState * GS): - gs(GS) +CGameInfoCallback::CGameInfoCallback(): + gs(nullptr) { } -std::shared_ptr> CPlayerSpecificInfoCallback::getVisibilityMap() const +CGameInfoCallback::CGameInfoCallback(CGameState * GS): + gs(GS) { - //boost::shared_lock lock(*gs->mx); - return gs->getPlayerTeam(*getPlayerID())->fogOfWarMap; } int CPlayerSpecificInfoCallback::howManyTowns() const @@ -741,7 +729,7 @@ int CPlayerSpecificInfoCallback::howManyTowns() const std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo(bool onlyOur) const { //boost::shared_lock lock(*gs->mx); - std::vector < const CGTownInstance *> ret = std::vector < const CGTownInstance *>(); + auto ret = std::vector < const CGTownInstance *>(); for(const auto & i : gs->players) { for(const auto & town : i.second.towns) @@ -791,7 +779,7 @@ int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool int3 CPlayerSpecificInfoCallback::getGrailPos( double *outKnownRatio ) { - if (!getPlayerID() || CGObelisk::obeliskCount == 0) + if (!getPlayerID() || gs->map->obeliskCount == 0) { *outKnownRatio = 0.0; } @@ -799,10 +787,10 @@ int3 CPlayerSpecificInfoCallback::getGrailPos( double *outKnownRatio ) { TeamID t = gs->getPlayerTeam(*getPlayerID())->id; double visited = 0.0; - if(CGObelisk::visited.count(t)) - visited = static_cast(CGObelisk::visited[t]); + if(gs->map->obelisksVisited.count(t)) + visited = static_cast(gs->map->obelisksVisited[t]); - *outKnownRatio = visited / CGObelisk::obeliskCount; + *outKnownRatio = visited / gs->map->obeliskCount; } return gs->map->grailPos; } @@ -966,6 +954,11 @@ const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid return gs->map->objects[oid.num]; } +const CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const +{ + return gs->getArtSet(loc); +} + std::vector CGameInfoCallback::getVisibleTeleportObjects(std::vector ids, PlayerColor player) const { vstd::erase_if(ids, [&](const ObjectInstanceID & id) -> bool diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index e1a19d5c3..31aafdde5 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -40,6 +40,8 @@ class CGameState; class PathfinderConfig; struct TurnTimerInfo; +struct ArtifactLocation; +class CArtifactSet; class CArmedInstance; class CGObjectInstance; class CGHeroInstance; @@ -55,7 +57,9 @@ public: // //various virtual int getDate(Date mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month // const StartInfo * getStartInfo(bool beforeRandomization = false)const; - virtual bool isAllowed(int32_t type, int32_t id) const = 0; //type: 0 - spell; 1- artifact; 2 - secondary skill + virtual bool isAllowed(SpellID id) const = 0; + virtual bool isAllowed(ArtifactID id) const = 0; + virtual bool isAllowed(SecondarySkill id) const = 0; //player virtual std::optional getPlayerID() const = 0; @@ -89,7 +93,6 @@ public: // std::vector getFlaggableObjects(int3 pos) const; // const CGObjectInstance * getTopObj (int3 pos) const; // PlayerColor getOwner(ObjectInstanceID heroID) const; -// const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) //map // int3 guardingCreaturePosition (int3 pos) const; @@ -130,7 +133,7 @@ class DLL_LINKAGE CGameInfoCallback : public IGameInfoCallback protected: CGameState * gs;//todo: replace with protected const getter, only actual Server and Client objects should hold game state - CGameInfoCallback() = default; + CGameInfoCallback(); CGameInfoCallback(CGameState * GS); bool hasAccess(std::optional playerId) const; @@ -141,7 +144,9 @@ public: //various int getDate(Date mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month virtual const StartInfo * getStartInfo(bool beforeRandomization = false)const; - bool isAllowed(int32_t type, int32_t id) const override; //type: 0 - spell; 1- artifact; 2 - secondary skill + bool isAllowed(SpellID id) const override; + bool isAllowed(ArtifactID id) const override; + bool isAllowed(SecondarySkill id) const override; //player std::optional getPlayerID() const override; @@ -166,7 +171,7 @@ public: virtual void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out)const; //hero - virtual const CGHeroInstance * getHero(ObjectInstanceID objid) const override; + const CGHeroInstance * getHero(ObjectInstanceID objid) const override; const CGHeroInstance * getHeroWithSubid(int subid) const override; virtual int getHeroCount(PlayerColor player, bool includeGarrisoned) const; virtual bool getHeroInfo(const CGObjectInstance * hero, InfoAboutHero & dest, const CGObjectInstance * selectedObject = nullptr) const; @@ -174,16 +179,16 @@ public: virtual int64_t estimateSpellDamage(const CSpell * sp, const CGHeroInstance * hero) const; //estimates damage of given spell; returns 0 if spell causes no dmg virtual const CArtifactInstance * getArtInstance(ArtifactInstanceID aid) const; virtual const CGObjectInstance * getObjInstance(ObjectInstanceID oid) const; + virtual const CArtifactSet * getArtSet(const ArtifactLocation & loc) const; //virtual const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const; //objects - virtual const CGObjectInstance * getObj(ObjectInstanceID objid, bool verbose = true) const override; + const CGObjectInstance * getObj(ObjectInstanceID objid, bool verbose = true) const override; virtual std::vector getBlockingObjs(int3 pos)const; - virtual std::vector getVisitableObjs(int3 pos, bool verbose = true) const override; + std::vector getVisitableObjs(int3 pos, bool verbose = true) const override; virtual std::vector getFlaggableObjects(int3 pos) const; virtual const CGObjectInstance * getTopObj (int3 pos) const; virtual PlayerColor getOwner(ObjectInstanceID heroID) const; - virtual const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) //map virtual int3 guardingCreaturePosition (int3 pos) const; @@ -244,8 +249,6 @@ public: virtual int getResourceAmount(GameResID type) const; virtual TResources getResourceAmount() const; - virtual std::shared_ptr> getVisibilityMap() const; //returns visibility map - //virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index 619acd62b..005848079 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -243,9 +243,8 @@ void CAdventureAI::yourTacticPhase(const BattleID & battleID, int distance) battleAI->yourTacticPhase(battleID, distance); } -void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */ +void CAdventureAI::saveGame(BinarySerializer & h) /*saving */ { - LOG_TRACE_PARAMS(logAi, "version '%i'", version); bool hasBattleAI = static_cast(battleAI); h & hasBattleAI; if(hasBattleAI) @@ -254,9 +253,8 @@ void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */ } } -void CAdventureAI::loadGame(BinaryDeserializer & h, const int version) /*loading */ +void CAdventureAI::loadGame(BinaryDeserializer & h) /*loading */ { - LOG_TRACE_PARAMS(logAi, "version '%i'", version); bool hasBattleAI = false; h & hasBattleAI; if(hasBattleAI) diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 6aabc1dcc..2bfc0bc9a 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -111,8 +111,8 @@ public: virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; - virtual void saveGame(BinarySerializer & h, const int version) = 0; - virtual void loadGame(BinaryDeserializer & h, const int version) = 0; + virtual void saveGame(BinarySerializer & h) = 0; + virtual void loadGame(BinaryDeserializer & h) = 0; }; class DLL_LINKAGE CDynLibHandler @@ -144,26 +144,26 @@ public: virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used //battle interface - virtual void activeStack(const BattleID & battleID, const CStack * stack) override; - virtual void yourTacticPhase(const BattleID & battleID, int distance) override; + void activeStack(const BattleID & battleID, const CStack * stack) override; + void yourTacticPhase(const BattleID & battleID, int distance) override; - virtual void battleNewRound(const BattleID & battleID) override; - virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; - virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; - virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; - virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override; - virtual void battleNewRoundFirst(const BattleID & battleID) override; - virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override; - virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; - virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; - virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; - virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; - virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; - virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; - virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; + void battleNewRound(const BattleID & battleID) override; + void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; + void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; + void actionStarted(const BattleID & battleID, const BattleAction &action) override; + void battleNewRoundFirst(const BattleID & battleID) override; + void actionFinished(const BattleID & battleID, const BattleAction &action) override; + void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; + void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; + void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; + void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; + void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; - virtual void saveGame(BinarySerializer & h, const int version) override; - virtual void loadGame(BinaryDeserializer & h, const int version) override; + void saveGame(BinarySerializer & h) override; + void loadGame(BinaryDeserializer & h) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 4c6ba5c49..ba8888e1a 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -264,11 +264,14 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) { + assert(!vstd::contains(subContainers, &container)); subContainers.push_back(&container); } void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) { + assert(vstd::contains(subContainers, &container)); + subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); } @@ -414,6 +417,28 @@ void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const } } +TextContainerRegistrable::TextContainerRegistrable() +{ + VLC->generaltexth->addSubContainer(*this); +} + +TextContainerRegistrable::~TextContainerRegistrable() +{ + VLC->generaltexth->removeSubContainer(*this); +} + +TextContainerRegistrable::TextContainerRegistrable(const TextContainerRegistrable & other) + : TextLocalizationContainer(other) +{ + VLC->generaltexth->addSubContainer(*this); +} + +TextContainerRegistrable::TextContainerRegistrable(TextContainerRegistrable && other) noexcept + :TextLocalizationContainer(other) +{ + VLC->generaltexth->addSubContainer(*this); +} + void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) { CLegacyConfigParser parser(TextPath::builtin(sourceName)); @@ -477,7 +502,7 @@ CGeneralTextHandler::CGeneralTextHandler(): readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" ); - static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; + static const std::string QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 980a3c03e..f4fa874bf 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -135,7 +135,7 @@ protected: std::string modContext; template - void serialize(Handler & h, const int Version) + void serialize(Handler & h) { h & baseValue; h & baseLanguage; @@ -193,7 +193,7 @@ public: void jsonSerialize(JsonNode & dest) const; template - void serialize(Handler & h, const int Version) + void serialize(Handler & h) { std::string key; auto sz = stringsLocalizations.size(); @@ -218,6 +218,18 @@ public: } }; +class DLL_LINKAGE TextContainerRegistrable : public TextLocalizationContainer +{ +public: + TextContainerRegistrable(); + ~TextContainerRegistrable(); + + TextContainerRegistrable(const TextContainerRegistrable & other); + TextContainerRegistrable(TextContainerRegistrable && other) noexcept; + + TextContainerRegistrable& operator=(const TextContainerRegistrable & b) = default; +}; + /// Handles all text-related data in game class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer { diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 8bb900e12..e73f9bd0b 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -13,16 +13,18 @@ #include "CGeneralTextHandler.h" #include "filesystem/Filesystem.h" #include "VCMI_Lib.h" -#include "JsonNode.h" #include "constants/StringConstants.h" #include "battle/BattleHex.h" #include "CCreatureHandler.h" #include "GameSettings.h" +#include "CRandomGenerator.h" #include "CTownHandler.h" #include "CSkillHandler.h" #include "BattleFieldHandler.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" +#include "json/JsonBonus.h" +#include "json/JsonUtils.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "modding/IdentifierStorage.h" @@ -44,7 +46,7 @@ int32_t CHero::getIconIndex() const std::string CHero::getJsonKey() const { - return modScope + ':' + identifier;; + return modScope + ':' + identifier; } HeroTypeID CHero::getId() const @@ -123,24 +125,30 @@ void CHero::serializeJson(JsonSerializeFormat & handler) SecondarySkill CHeroClass::chooseSecSkill(const std::set & possibles, CRandomGenerator & rand) const //picks secondary skill out from given possibilities { + assert(!possibles.empty()); + + if (possibles.size() == 1) + return *possibles.begin(); + int totalProb = 0; + for(const auto & possible : possibles) + if (secSkillProbability.count(possible) != 0) + totalProb += secSkillProbability.at(possible); + + if (totalProb == 0) // may trigger if set contains only banned skills (0 probability) + return *RandomGeneratorUtil::nextItem(possibles, rand); + + auto ran = rand.nextInt(totalProb - 1); for(const auto & possible : possibles) { - totalProb += secSkillProbability[possible]; + if (secSkillProbability.count(possible) != 0) + ran -= secSkillProbability.at(possible); + + if(ran < 0) + return possible; } - if (totalProb != 0) // may trigger if set contains only banned skills (0 probability) - { - auto ran = rand.nextInt(totalProb - 1); - for(const auto & possible : possibles) - { - ran -= secSkillProbability[possible]; - if(ran < 0) - { - return possible; - } - } - } - // FIXME: select randomly? How H3 handles such rare situation? + + assert(0); // should not be possible return *possibles.begin(); } @@ -149,9 +157,17 @@ bool CHeroClass::isMagicHero() const return affinity == MAGIC; } +int CHeroClass::tavernProbability(FactionID targetFaction) const +{ + auto it = selectionProbability.find(targetFaction); + if (it != selectionProbability.end()) + return it->second; + return 0; +} + EAlignment CHeroClass::getAlignment() const { - return VLC->factions()->getByIndex(faction)->getAlignment(); + return VLC->factions()->getById(faction)->getAlignment(); } int32_t CHeroClass::getIndex() const @@ -166,7 +182,7 @@ int32_t CHeroClass::getIconIndex() const std::string CHeroClass::getJsonKey() const { - return modScope + ':' + identifier;; + return modScope + ':' + identifier; } HeroClassID CHeroClass::getId() const @@ -209,7 +225,7 @@ CHeroClass::CHeroClass(): void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const { - const auto & skillName = NPrimarySkill::names[static_cast(pSkill)]; + const auto & skillName = NPrimarySkill::names[pSkill.getNum()]; auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); //minimal value is 0 for attack and defense and 1 for spell power and knowledge auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1; @@ -251,7 +267,15 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js VLC->generaltexth->registerString(scope, heroClass->getNameTextID(), node["name"].String()); - heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); + if (vstd::contains(affinityStr, node["affinity"].String())) + { + heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); + } + else + { + logGlobal->error("Mod '%s', hero class '%s': invalid affinity '%s'! Expected 'might' or 'magic'!", scope, identifier, node["affinity"].String()); + heroClass->affinity = CHeroClass::MIGHT; + } fillPrimarySkillData(node, heroClass, PrimarySkill::ATTACK); fillPrimarySkillData(node, heroClass, PrimarySkill::DEFENSE); @@ -259,20 +283,18 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js fillPrimarySkillData(node, heroClass, PrimarySkill::KNOWLEDGE); auto percentSumm = std::accumulate(heroClass->primarySkillLowLevel.begin(), heroClass->primarySkillLowLevel.end(), 0); - if(percentSumm != 100) - logMod->error("Hero class %s has wrong lowLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); + if(percentSumm <= 0) + logMod->error("Hero class %s has wrong lowLevelChance values: must be above zero!", heroClass->identifier, percentSumm); percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); - if(percentSumm != 100) - logMod->error("Hero class %s has wrong highLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); + if(percentSumm <= 0) + logMod->error("Hero class %s has wrong highLevelChance values: must be above zero!", heroClass->identifier, percentSumm); for(auto skillPair : node["secondarySkills"].Struct()) { int probability = static_cast(skillPair.second.Integer()); - VLC->identifiers()->requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) + VLC->identifiers()->requestIdentifier(skillPair.second.getModScope(), "skill", skillPair.first, [heroClass, probability](si32 skillID) { - if(heroClass->secSkillProbability.size() <= skillID) - heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later heroClass->secSkillProbability[skillID] = probability; }); } @@ -280,7 +302,7 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js VLC->identifiers()->requestIdentifier ("creature", node["commander"], [=](si32 commanderID) { - heroClass->commander = VLC->creh->objects[commanderID]; + heroClass->commander = CreatureID(commanderID).toCreature(); }); heroClass->defaultTavernChance = static_cast(node["defaultTavern"].Float()); @@ -288,7 +310,7 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js { int value = static_cast(tavern.second.Float()); - VLC->identifiers()->requestIdentifier(tavern.second.meta, "faction", tavern.first, + VLC->identifiers()->requestIdentifier(tavern.second.getModScope(), "faction", tavern.first, [=](si32 factionID) { heroClass->selectionProbability[FactionID(factionID)] = value; @@ -305,7 +327,9 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js { JsonNode classConf = node["mapObject"]; classConf["heroClass"].String() = identifier; - classConf.setMeta(scope); + if (!node["compatibilityIdentifiers"].isNull()) + classConf["compatibilityIdentifiers"] = node["compatibilityIdentifiers"]; + classConf.setModScope(scope); VLC->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex()); }); @@ -357,9 +381,9 @@ std::vector CHeroClassHandler::loadLegacyData() void CHeroClassHandler::afterLoadFinalization() { // for each pair set selection probability if it was not set before in tavern entries - for(CHeroClass * heroClass : objects) + for(auto & heroClass : objects) { - for(CFaction * faction : VLC->townh->objects) + for(auto & faction : VLC->townh->objects) { if (!faction->town) continue; @@ -369,11 +393,11 @@ void CHeroClassHandler::afterLoadFinalization() auto chance = static_cast(heroClass->defaultTavernChance * faction->town->defaultTavernChance); heroClass->selectionProbability[faction->getId()] = static_cast(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it } + // set default probabilities for gaining secondary skills where not loaded previously - heroClass->secSkillProbability.resize(VLC->skillh->size(), -1); for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) { - if(heroClass->secSkillProbability[skillID] < 0) + if(heroClass->secSkillProbability.count(skillID) == 0) { const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)]; logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey()); @@ -382,9 +406,9 @@ void CHeroClassHandler::afterLoadFinalization() } } - for(CHeroClass * hc : objects) + for(const auto & hc : objects) { - if (!hc->imageMapMale.empty()) + if(!hc->imageMapMale.empty()) { JsonNode templ; templ["animation"].String() = hc->imageMapMale; @@ -393,11 +417,6 @@ void CHeroClassHandler::afterLoadFinalization() } } -std::vector CHeroClassHandler::getDefaultAllowed() const -{ - return std::vector(size(), true); -} - CHeroClassHandler::~CHeroClassHandler() = default; CHeroHandler::~CHeroHandler() = default; @@ -447,7 +466,7 @@ CHero * CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & n VLC->identifiers()->requestIdentifier("heroClass", node["class"], [=](si32 classID) { - hero->heroClass = classes[HeroClassID(classID)]; + hero->heroClass = HeroClassID(classID).toHeroClass(); }); return hero; @@ -466,7 +485,11 @@ void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const hero->initialArmy[i].minAmount = static_cast(source["min"].Float()); hero->initialArmy[i].maxAmount = static_cast(source["max"].Float()); - assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount); + if (hero->initialArmy[i].minAmount > hero->initialArmy[i].maxAmount) + { + logMod->error("Hero %s has minimal army size (%d) greater than maximal size (%d)!", hero->getJsonKey(), hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); + std::swap(hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); + } VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature) { @@ -521,9 +544,9 @@ static std::vector> createCreatureSpecialty(CreatureID ba { std::set oldTargets = targets; - for (auto const & upgradeSourceID : oldTargets) + for(const auto & upgradeSourceID : oldTargets) { - const CCreature * upgradeSource = VLC->creh->objects[upgradeSourceID]; + const CCreature * upgradeSource = upgradeSourceID.toCreature(); targets.insert(upgradeSource->upgrades.begin(), upgradeSource->upgrades.end()); } @@ -533,11 +556,11 @@ static std::vector> createCreatureSpecialty(CreatureID ba for(CreatureID cid : targets) { - const CCreature &specCreature = *VLC->creh->objects[cid]; + const auto & specCreature = *cid.toCreature(); int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; { - std::shared_ptr bonus = std::make_shared(); + auto bonus = std::make_shared(); bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->type = BonusType::STACKS_SPEED; bonus->val = 1; @@ -545,7 +568,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba } { - std::shared_ptr bonus = std::make_shared(); + auto bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; bonus->subtype = BonusSubtypeID(PrimarySkill::ATTACK); bonus->val = 0; @@ -555,7 +578,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba } { - std::shared_ptr bonus = std::make_shared(); + auto bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; bonus->subtype = BonusSubtypeID(PrimarySkill::DEFENSE); bonus->val = 0; @@ -593,7 +616,7 @@ void CHeroHandler::beforeValidate(JsonNode & object) void CHeroHandler::afterLoadFinalization() { - for (auto const & functor : callAfterLoadFinalization) + for(const auto & functor : callAfterLoadFinalization) functor(); callAfterLoadFinalization.clear(); @@ -655,14 +678,21 @@ void CHeroHandler::loadExperience() expPerLevel.push_back(24320); expPerLevel.push_back(28784); expPerLevel.push_back(34140); - while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2]) + + for (;;) { auto i = expPerLevel.size() - 1; - auto diff = expPerLevel[i] - expPerLevel[i-1]; - diff += diff / 5; - expPerLevel.push_back (expPerLevel[i] + diff); + auto currExp = expPerLevel[i]; + auto prevExp = expPerLevel[i-1]; + auto prevDiff = currExp - prevExp; + auto nextDiff = prevDiff + prevDiff / 5; + auto maxExp = std::numeric_limits::max(); + + if (currExp > maxExp - nextDiff) + break; // overflow point reached + + expPerLevel.push_back (currExp + nextDiff); } - expPerLevel.pop_back();//last value is broken } /// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider) @@ -729,6 +759,9 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod objects.emplace_back(object); registerObject(scope, "hero", name, object->getIndex()); + + for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) + registerObject(scope, "hero", compatID.String(), object->getIndex()); } void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -740,14 +773,16 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod objects[index] = object; registerObject(scope, "hero", name, object->getIndex()); + for(const auto & compatID : data["compatibilityIdentifiers"].Vector()) + registerObject(scope, "hero", compatID.String(), object->getIndex()); } -ui32 CHeroHandler::level (ui64 experience) const +ui32 CHeroHandler::level (TExpType experience) const { return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); } -ui64 CHeroHandler::reqExp (ui32 level) const +TExpType CHeroHandler::reqExp (ui32 level) const { if(!level) return 0; @@ -763,18 +798,20 @@ ui64 CHeroHandler::reqExp (ui32 level) const } } -std::vector CHeroHandler::getDefaultAllowed() const +ui32 CHeroHandler::maxSupportedLevel() const { - // Look Data/HOTRAITS.txt for reference - std::vector allowedHeroes; - allowedHeroes.reserve(size()); + return expPerLevel.size(); +} - for(const CHero * hero : objects) - { - allowedHeroes.push_back(hero && !hero->special); - } +std::set CHeroHandler::getDefaultAllowed() const +{ + std::set result; - return allowedHeroes; + for(auto & hero : objects) + if (hero && !hero->special) + result.insert(hero->getId()); + + return result; } VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index fbd5c24b3..1b066d029 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -31,11 +31,11 @@ class CRandomGenerator; class JsonSerializeFormat; class BattleField; -enum class EHeroGender : uint8_t +enum class EHeroGender : int8_t { + DEFAULT = -1, // from h3m, instance has same gender as hero type MALE = 0, FEMALE = 1, - DEFAULT = 0xff // from h3m, instance has same gender as hero type }; class DLL_LINKAGE CHero : public HeroType @@ -52,19 +52,12 @@ public: ui32 minAmount; ui32 maxAmount; CreatureID creature; - - template void serialize(Handler &h, const int version) - { - h & minAmount; - h & maxAmount; - h & creature; - } }; si32 imageIndex = 0; std::vector initialArmy; - CHeroClass * heroClass{}; + const CHeroClass * heroClass = nullptr; std::vector > secSkillsInit; //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert) BonusList specialty; std::set spells; @@ -104,29 +97,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & ID; - h & imageIndex; - h & initialArmy; - h & heroClass; - h & secSkillsInit; - h & specialty; - h & spells; - h & haveSpellBook; - h & gender; - h & special; - h & onlyOnWaterMap; - h & onlyOnMapWithoutWater; - h & iconSpecSmall; - h & iconSpecLarge; - h & portraitSmall; - h & portraitLarge; - h & identifier; - h & modScope; - h & battleImage; - } }; class DLL_LINKAGE CHeroClass : public HeroClass @@ -151,13 +121,13 @@ public: // resulting chance = sqrt(town.chance * heroClass.chance) ui32 defaultTavernChance; - CCreature * commander; + const CCreature * commander; std::vector primarySkillInitial; // initial primary skills std::vector primarySkillLowLevel; // probability (%) of getting point of primary skill when getting level std::vector primarySkillHighLevel;// same for high levels (> 10) - std::vector secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order + std::map secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order std::map selectionProbability; //probability of selection in towns @@ -183,32 +153,9 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler & h, const int version) - { - h & modScope; - h & identifier; - h & faction; - h & id; - h & defaultTavernChance; - h & primarySkillInitial; - h & primarySkillLowLevel; - h & primarySkillHighLevel; - h & secSkillProbability; - h & selectionProbability; - h & affinity; - h & commander; - h & imageBattleMale; - h & imageBattleFemale; - h & imageMapMale; - h & imageMapFemale; - - if(!h.saving) - { - for(int & i : secSkillProbability) - vstd::amax(i, 0); - } - } EAlignment getAlignment() const; + + int tavernProbability(FactionID faction) const; }; class DLL_LINKAGE CHeroClassHandler : public CHandlerBase @@ -220,15 +167,8 @@ public: void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; - ~CHeroClassHandler(); - template void serialize(Handler &h, const int version) - { - h & objects; - } - protected: const std::vector & getTypeNames() const override; CHeroClass * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; @@ -238,8 +178,8 @@ protected: class DLL_LINKAGE CHeroHandler : public CHandlerBase { /// expPerLEvel[i] is amount of exp needed to reach level i; - /// consists of 201 values. Any higher levels require experience larger that ui64 can hold - std::vector expPerLevel; + /// consists of 196 values. Any higher levels require experience larger that TExpType can hold + std::vector expPerLevel; /// helpers for loading to avoid huge load functions void loadHeroArmy(CHero * hero, const JsonNode & node) const; @@ -251,10 +191,9 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase> callAfterLoadFinalization; public: - CHeroClassHandler classes; - - ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount - ui64 reqExp(ui32 level) const; //calculates experience required for given level + ui32 level(TExpType experience) const; //calculates level corresponding to given experience amount + TExpType reqExp(ui32 level) const; //calculates experience required for given level + ui32 maxSupportedLevel() const; std::vector loadLegacyData() override; @@ -266,14 +205,7 @@ public: CHeroHandler(); ~CHeroHandler(); - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - h & classes; - h & objects; - h & expPerLevel; - } + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index b8b94e33e..566c2fdac 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,18 +1,786 @@ -if(ENABLE_STATIC_AI_LIBS) - add_main_lib(${VCMI_LIB_TARGET} STATIC) - target_compile_definitions(${VCMI_LIB_TARGET} PRIVATE STATIC_AI) - target_link_libraries(${VCMI_LIB_TARGET} PRIVATE +set(lib_SRCS + StdInc.cpp + + battle/AccessibilityInfo.cpp + battle/BattleAction.cpp + battle/BattleAttackInfo.cpp + battle/BattleHex.cpp + battle/BattleInfo.cpp + battle/BattleProxy.cpp + battle/BattleStateInfoForRetreat.cpp + battle/CBattleInfoCallback.cpp + battle/CBattleInfoEssentials.cpp + battle/CObstacleInstance.cpp + battle/CPlayerBattleCallback.cpp + battle/CUnitState.cpp + battle/DamageCalculator.cpp + battle/Destination.cpp + battle/IBattleState.cpp + battle/ReachabilityInfo.cpp + battle/SideInBattle.cpp + battle/SiegeInfo.cpp + battle/Unit.cpp + + bonuses/Bonus.cpp + bonuses/BonusEnum.cpp + bonuses/BonusList.cpp + bonuses/BonusParams.cpp + bonuses/BonusSelector.cpp + bonuses/BonusCustomTypes.cpp + bonuses/CBonusProxy.cpp + bonuses/CBonusSystemNode.cpp + bonuses/IBonusBearer.cpp + bonuses/Limiters.cpp + bonuses/Propagators.cpp + bonuses/Updaters.cpp + + campaign/CampaignHandler.cpp + campaign/CampaignState.cpp + + constants/EntityIdentifiers.cpp + + events/ApplyDamage.cpp + events/GameResumed.cpp + events/ObjectVisitEnded.cpp + events/ObjectVisitStarted.cpp + events/PlayerGotTurn.cpp + events/TurnStarted.cpp + + filesystem/AdapterLoaders.cpp + filesystem/CArchiveLoader.cpp + filesystem/CBinaryReader.cpp + filesystem/CCompressedStream.cpp + filesystem/CFileInputStream.cpp + filesystem/CFilesystemLoader.cpp + filesystem/CMemoryBuffer.cpp + filesystem/CMemoryStream.cpp + filesystem/CZipLoader.cpp + filesystem/CZipSaver.cpp + filesystem/FileInfo.cpp + filesystem/Filesystem.cpp + filesystem/MinizipExtensions.cpp + filesystem/ResourcePath.cpp + + json/JsonBonus.cpp + json/JsonNode.cpp + json/JsonParser.cpp + json/JsonRandom.cpp + json/JsonUtils.cpp + json/JsonValidator.cpp + json/JsonWriter.cpp + + gameState/CGameState.cpp + gameState/CGameStateCampaign.cpp + gameState/InfoAboutArmy.cpp + gameState/TavernHeroesPool.cpp + + logging/CBasicLogConfigurator.cpp + logging/CLogger.cpp + + mapObjectConstructors/AObjectTypeHandler.cpp + mapObjectConstructors/CBankInstanceConstructor.cpp + mapObjectConstructors/CObjectClassesHandler.cpp + mapObjectConstructors/CommonConstructors.cpp + mapObjectConstructors/CRewardableConstructor.cpp + mapObjectConstructors/DwellingInstanceConstructor.cpp + mapObjectConstructors/HillFortInstanceConstructor.cpp + mapObjectConstructors/ShipyardInstanceConstructor.cpp + + mapObjects/CArmedInstance.cpp + mapObjects/CBank.cpp + mapObjects/CGCreature.cpp + mapObjects/CGDwelling.cpp + mapObjects/CGHeroInstance.cpp + mapObjects/CGMarket.cpp + mapObjects/CGObjectInstance.cpp + mapObjects/CGPandoraBox.cpp + mapObjects/CGTownBuilding.cpp + mapObjects/CGTownInstance.cpp + mapObjects/CObjectHandler.cpp + mapObjects/CQuest.cpp + mapObjects/CRewardableObject.cpp + mapObjects/IMarket.cpp + mapObjects/IObjectInterface.cpp + mapObjects/MiscObjects.cpp + mapObjects/ObjectTemplate.cpp + + mapping/CDrawRoadsOperation.cpp + mapping/CMap.cpp + mapping/CMapHeader.cpp + mapping/CMapEditManager.cpp + mapping/CMapInfo.cpp + mapping/CMapOperation.cpp + mapping/CMapService.cpp + mapping/MapEditUtils.cpp + mapping/MapIdentifiersH3M.cpp + mapping/MapFeaturesH3M.cpp + mapping/MapFormatH3M.cpp + mapping/MapReaderH3M.cpp + mapping/MapFormatJson.cpp + mapping/ObstacleProxy.cpp + + modding/ActiveModsInSaveList.cpp + modding/CModHandler.cpp + modding/CModInfo.cpp + modding/CModVersion.cpp + modding/ContentTypeHandler.cpp + modding/IdentifierStorage.cpp + modding/ModUtility.cpp + + network/NetworkConnection.cpp + network/NetworkHandler.cpp + network/NetworkServer.cpp + + networkPacks/NetPacksLib.cpp + + pathfinder/CGPathNode.cpp + pathfinder/CPathfinder.cpp + pathfinder/NodeStorage.cpp + pathfinder/PathfinderOptions.cpp + pathfinder/PathfindingRules.cpp + pathfinder/TurnInfo.cpp + + rewardable/Configuration.cpp + rewardable/Info.cpp + rewardable/Interface.cpp + rewardable/Limiter.cpp + rewardable/Reward.cpp + + rmg/RmgArea.cpp + rmg/RmgObject.cpp + rmg/RmgPath.cpp + rmg/CMapGenerator.cpp + rmg/CMapGenOptions.cpp + rmg/CRmgTemplate.cpp + rmg/CRmgTemplateStorage.cpp + rmg/CZonePlacer.cpp + rmg/TileInfo.cpp + rmg/Zone.cpp + rmg/Functions.cpp + rmg/RmgMap.cpp + rmg/PenroseTiling.cpp + rmg/modificators/Modificator.cpp + rmg/modificators/ObjectManager.cpp + rmg/modificators/ObjectDistributor.cpp + rmg/modificators/RoadPlacer.cpp + rmg/modificators/TreasurePlacer.cpp + rmg/modificators/PrisonHeroPlacer.cpp + rmg/modificators/QuestArtifactPlacer.cpp + rmg/modificators/ConnectionsPlacer.cpp + rmg/modificators/WaterAdopter.cpp + rmg/modificators/MinePlacer.cpp + rmg/modificators/TownPlacer.cpp + rmg/modificators/WaterProxy.cpp + rmg/modificators/WaterRoutes.cpp + rmg/modificators/RockPlacer.cpp + rmg/modificators/RockFiller.cpp + rmg/modificators/ObstaclePlacer.cpp + rmg/modificators/RiverPlacer.cpp + rmg/modificators/TerrainPainter.cpp + rmg/threadpool/MapProxy.cpp + + serializer/BinaryDeserializer.cpp + serializer/BinarySerializer.cpp + serializer/CLoadFile.cpp + serializer/CMemorySerializer.cpp + serializer/Connection.cpp + serializer/CSaveFile.cpp + serializer/CSerializer.cpp + serializer/CTypeList.cpp + serializer/JsonDeserializer.cpp + serializer/JsonSerializeFormat.cpp + serializer/JsonSerializer.cpp + serializer/JsonUpdater.cpp + + spells/AbilityCaster.cpp + spells/AdventureSpellMechanics.cpp + spells/BattleSpellMechanics.cpp + spells/BonusCaster.cpp + spells/CSpellHandler.cpp + spells/ExternalCaster.cpp + spells/ISpellMechanics.cpp + spells/ObstacleCasterProxy.cpp + spells/Problem.cpp + spells/ProxyCaster.cpp + spells/TargetCondition.cpp + spells/ViewSpellInt.cpp + + spells/effects/Catapult.cpp + spells/effects/Clone.cpp + spells/effects/Damage.cpp + spells/effects/DemonSummon.cpp + spells/effects/Dispel.cpp + spells/effects/Effect.cpp + spells/effects/Effects.cpp + spells/effects/Heal.cpp + spells/effects/LocationEffect.cpp + spells/effects/Moat.cpp + spells/effects/Obstacle.cpp + spells/effects/Registry.cpp + spells/effects/UnitEffect.cpp + spells/effects/Summon.cpp + spells/effects/Teleport.cpp + spells/effects/Timed.cpp + spells/effects/RemoveObstacle.cpp + spells/effects/Sacrifice.cpp + + vstd/DateUtils.cpp + vstd/StringUtils.cpp + + ArtifactUtils.cpp + BasicTypes.cpp + BattleFieldHandler.cpp + CAndroidVMHelper.cpp + CArtHandler.cpp + CArtifactInstance.cpp + CBonusTypeHandler.cpp + CBuildingHandler.cpp + CConfigHandler.cpp + CConsoleHandler.cpp + CCreatureHandler.cpp + CCreatureSet.cpp + CGameInfoCallback.cpp + CGameInterface.cpp + CGeneralTextHandler.cpp + CHeroHandler.cpp + CPlayerState.cpp + CRandomGenerator.cpp + CScriptingModule.cpp + CSkillHandler.cpp + CStack.cpp + CThreadHelper.cpp + CTownHandler.cpp + GameSettings.cpp + IGameCallback.cpp + IHandlerBase.cpp + LoadProgress.cpp + LogicalExpression.cpp + MetaString.cpp + ObstacleHandler.cpp + StartInfo.cpp + ResourceSet.cpp + RiverHandler.cpp + RoadHandler.cpp + ScriptHandler.cpp + TerrainHandler.cpp + TextOperations.cpp + TurnTimerInfo.cpp + VCMIDirs.cpp + VCMI_Lib.cpp +) + +# Version.cpp is a generated file +if(ENABLE_GITVERSION) + list(APPEND lib_SRCS ${CMAKE_BINARY_DIR}/Version.cpp) + set_source_files_properties(${CMAKE_BINARY_DIR}/Version.cpp + PROPERTIES GENERATED TRUE + ) +endif() + +set(lib_HEADERS + ../include/vstd/CLoggerBase.h + ../Global.h + ../AUTHORS.h + StdInc.h + + ../include/vstd/ContainerUtils.h + ../include/vstd/RNG.h + ../include/vstd/DateUtils.h + ../include/vstd/StringUtils.h + + ../include/vcmi/events/AdventureEvents.h + ../include/vcmi/events/ApplyDamage.h + ../include/vcmi/events/BattleEvents.h + ../include/vcmi/events/Event.h + ../include/vcmi/events/EventBus.h + ../include/vcmi/events/GameResumed.h + ../include/vcmi/events/GenericEvents.h + ../include/vcmi/events/ObjectVisitEnded.h + ../include/vcmi/events/ObjectVisitStarted.h + ../include/vcmi/events/PlayerGotTurn.h + ../include/vcmi/events/SubscriptionRegistry.h + ../include/vcmi/events/TurnStarted.h + + ../include/vcmi/scripting/Service.h + + ../include/vcmi/spells/Caster.h + ../include/vcmi/spells/Magic.h + ../include/vcmi/spells/Service.h + ../include/vcmi/spells/Spell.h + + ../include/vcmi/Artifact.h + ../include/vcmi/ArtifactService.h + ../include/vcmi/Creature.h + ../include/vcmi/CreatureService.h + ../include/vcmi/Entity.h + ../include/vcmi/Environment.h + ../include/vcmi/Faction.h + ../include/vcmi/FactionService.h + ../include/vcmi/HeroClass.h + ../include/vcmi/HeroClassService.h + ../include/vcmi/HeroType.h + ../include/vcmi/HeroTypeService.h + ../include/vcmi/Metatype.h + ../include/vcmi/Player.h + ../include/vcmi/ServerCallback.h + ../include/vcmi/Services.h + ../include/vcmi/Skill.h + ../include/vcmi/SkillService.h + ../include/vcmi/Team.h + + battle/AccessibilityInfo.h + battle/AutocombatPreferences.h + battle/BattleAction.h + battle/BattleAttackInfo.h + battle/BattleHex.h + battle/BattleInfo.h + battle/BattleStateInfoForRetreat.h + battle/BattleProxy.h + battle/CBattleInfoCallback.h + battle/CBattleInfoEssentials.h + battle/CObstacleInstance.h + battle/CPlayerBattleCallback.h + battle/CUnitState.h + battle/DamageCalculator.h + battle/Destination.h + battle/IBattleInfoCallback.h + battle/IBattleState.h + battle/IUnitInfo.h + battle/PossiblePlayerBattleAction.h + battle/ReachabilityInfo.h + battle/SideInBattle.h + battle/SiegeInfo.h + battle/Unit.h + + bonuses/Bonus.h + bonuses/BonusEnum.h + bonuses/BonusList.h + bonuses/BonusParams.h + bonuses/BonusSelector.h + bonuses/BonusCustomTypes.h + bonuses/CBonusProxy.h + bonuses/CBonusSystemNode.h + bonuses/IBonusBearer.h + bonuses/Limiters.h + bonuses/Propagators.h + bonuses/Updaters.h + + campaign/CampaignConstants.h + campaign/CampaignHandler.h + campaign/CampaignScenarioPrologEpilog.h + campaign/CampaignState.h + + constants/EntityIdentifiers.h + constants/Enumerations.h + constants/IdentifierBase.h + constants/VariantIdentifier.h + constants/NumericConstants.h + constants/StringConstants.h + + events/ApplyDamage.h + events/GameResumed.h + events/ObjectVisitEnded.h + events/ObjectVisitStarted.h + events/PlayerGotTurn.h + events/TurnStarted.h + + filesystem/AdapterLoaders.h + filesystem/CArchiveLoader.h + filesystem/CBinaryReader.h + filesystem/CCompressedStream.h + filesystem/CFileInputStream.h + filesystem/CFilesystemLoader.h + filesystem/CInputOutputStream.h + filesystem/CInputStream.h + filesystem/CMemoryBuffer.h + filesystem/CMemoryStream.h + filesystem/COutputStream.h + filesystem/CStream.h + filesystem/CZipLoader.h + filesystem/CZipSaver.h + filesystem/FileInfo.h + filesystem/Filesystem.h + filesystem/ISimpleResourceLoader.h + filesystem/MinizipExtensions.h + filesystem/ResourcePath.h + + json/JsonBonus.h + json/JsonFormatException.h + json/JsonNode.h + json/JsonParser.h + json/JsonRandom.h + json/JsonUtils.h + json/JsonValidator.h + json/JsonWriter.h + + gameState/CGameState.h + gameState/CGameStateCampaign.h + gameState/EVictoryLossCheckResult.h + gameState/InfoAboutArmy.h + gameState/SThievesGuildInfo.h + gameState/TavernHeroesPool.h + gameState/TavernSlot.h + gameState/QuestInfo.h + + logging/CBasicLogConfigurator.h + logging/CLogger.h + + mapObjectConstructors/AObjectTypeHandler.h + mapObjectConstructors/CBankInstanceConstructor.h + mapObjectConstructors/CDefaultObjectTypeHandler.h + mapObjectConstructors/CObjectClassesHandler.h + mapObjectConstructors/CommonConstructors.h + mapObjectConstructors/CRewardableConstructor.h + mapObjectConstructors/DwellingInstanceConstructor.h + mapObjectConstructors/HillFortInstanceConstructor.h + mapObjectConstructors/IObjectInfo.h + mapObjectConstructors/RandomMapInfo.h + mapObjectConstructors/ShipyardInstanceConstructor.h + mapObjectConstructors/SObjectSounds.h + + mapObjects/CArmedInstance.h + mapObjects/CBank.h + mapObjects/CGCreature.h + mapObjects/CGDwelling.h + mapObjects/CGHeroInstance.h + mapObjects/CGMarket.h + mapObjects/CGObjectInstance.h + mapObjects/CGPandoraBox.h + mapObjects/CGTownBuilding.h + mapObjects/CGTownInstance.h + mapObjects/CObjectHandler.h + mapObjects/CQuest.h + mapObjects/CRewardableObject.h + mapObjects/IMarket.h + mapObjects/IObjectInterface.h + mapObjects/MapObjects.h + mapObjects/MiscObjects.h + mapObjects/ObjectTemplate.h + + mapping/CDrawRoadsOperation.h + mapping/CMapDefines.h + mapping/CMapEditManager.h + mapping/CMapHeader.h + mapping/CMap.h + mapping/CMapInfo.h + mapping/CMapOperation.h + mapping/CMapService.h + mapping/MapEditUtils.h + mapping/MapIdentifiersH3M.h + mapping/MapFeaturesH3M.h + mapping/MapFormatH3M.h + mapping/MapFormat.h + mapping/MapReaderH3M.h + mapping/MapFormatJson.h + mapping/ObstacleProxy.h + + modding/ActiveModsInSaveList.h + modding/CModHandler.h + modding/CModInfo.h + modding/CModVersion.h + modding/ContentTypeHandler.h + modding/IdentifierStorage.h + modding/ModIncompatibility.h + modding/ModScope.h + modding/ModUtility.h + modding/ModVerificationInfo.h + + network/NetworkConnection.h + network/NetworkDefines.h + network/NetworkHandler.h + network/NetworkInterface.h + network/NetworkServer.h + + networkPacks/ArtifactLocation.h + networkPacks/BattleChanges.h + networkPacks/Component.h + networkPacks/EInfoWindowMode.h + networkPacks/EntityChanges.h + networkPacks/EOpenWindowMode.h + networkPacks/NetPacksBase.h + networkPacks/NetPackVisitor.h + networkPacks/ObjProperty.h + networkPacks/PacksForClient.h + networkPacks/PacksForClientBattle.h + networkPacks/PacksForLobby.h + networkPacks/PacksForServer.h + networkPacks/SetStackEffect.h + networkPacks/StackLocation.h + networkPacks/TradeItem.h + + pathfinder/INodeStorage.h + pathfinder/CGPathNode.h + pathfinder/CPathfinder.h + pathfinder/NodeStorage.h + pathfinder/PathfinderOptions.h + pathfinder/PathfinderUtil.h + pathfinder/PathfindingRules.h + pathfinder/TurnInfo.h + + registerTypes/RegisterTypes.h + registerTypes/RegisterTypesClientPacks.h + registerTypes/RegisterTypesLobbyPacks.h + registerTypes/RegisterTypesMapObjects.h + registerTypes/RegisterTypesServerPacks.h + + rewardable/Configuration.h + rewardable/Info.h + rewardable/Interface.h + rewardable/Limiter.h + rewardable/Reward.h + + rmg/RmgArea.h + rmg/RmgObject.h + rmg/RmgPath.h + rmg/CMapGenerator.h + rmg/CMapGenOptions.h + rmg/CRmgTemplate.h + rmg/CRmgTemplateStorage.h + rmg/CZonePlacer.h + rmg/TileInfo.h + rmg/Zone.h + rmg/RmgMap.h + rmg/float3.h + rmg/Functions.h + rmg/PenroseTiling.h + rmg/modificators/Modificator.h + rmg/modificators/ObjectManager.h + rmg/modificators/ObjectDistributor.h + rmg/modificators/RoadPlacer.h + rmg/modificators/TreasurePlacer.h + rmg/modificators/PrisonHeroPlacer.h + rmg/modificators/QuestArtifactPlacer.h + rmg/modificators/ConnectionsPlacer.h + rmg/modificators/WaterAdopter.h + rmg/modificators/MinePlacer.h + rmg/modificators/TownPlacer.h + rmg/modificators/WaterProxy.h + rmg/modificators/WaterRoutes.h + rmg/modificators/RockPlacer.h + rmg/modificators/RockFiller.h + rmg/modificators/ObstaclePlacer.h + rmg/modificators/RiverPlacer.h + rmg/modificators/TerrainPainter.h + rmg/threadpool/BlockingQueue.h + rmg/threadpool/ThreadPool.h + rmg/threadpool/MapProxy.h + + serializer/BinaryDeserializer.h + serializer/BinarySerializer.h + serializer/CLoadFile.h + serializer/CMemorySerializer.h + serializer/Connection.h + serializer/CSaveFile.h + serializer/CSerializer.h + serializer/CTypeList.h + serializer/JsonDeserializer.h + serializer/JsonSerializeFormat.h + serializer/JsonSerializer.h + serializer/JsonUpdater.h + serializer/Cast.h + serializer/ESerializationVersion.h + + spells/AbilityCaster.h + spells/AdventureSpellMechanics.h + spells/BattleSpellMechanics.h + spells/BonusCaster.h + spells/CSpellHandler.h + spells/ExternalCaster.h + spells/ISpellMechanics.h + spells/ObstacleCasterProxy.h + spells/Problem.h + spells/ProxyCaster.h + spells/TargetCondition.h + spells/ViewSpellInt.h + + spells/effects/Catapult.h + spells/effects/Clone.h + spells/effects/Damage.h + spells/effects/DemonSummon.h + spells/effects/Dispel.h + spells/effects/Effect.h + spells/effects/Effects.h + spells/effects/EffectsFwd.h + spells/effects/Heal.h + spells/effects/LocationEffect.h + spells/effects/Obstacle.h + spells/effects/Registry.h + spells/effects/UnitEffect.h + spells/effects/Summon.h + spells/effects/Teleport.h + spells/effects/Timed.h + spells/effects/RemoveObstacle.h + spells/effects/Sacrifice.h + + AI_Base.h + ArtifactUtils.h + BattleFieldHandler.h + CAndroidVMHelper.h + CArtHandler.h + CArtifactInstance.h + CBonusTypeHandler.h + CBuildingHandler.h + CConfigHandler.h + CConsoleHandler.h + CCreatureHandler.h + CCreatureSet.h + CGameInfoCallback.h + CGameInterface.h + CGeneralTextHandler.h + CHeroHandler.h + CondSh.h + ConstTransitivePtr.h + Color.h + CPlayerState.h + CRandomGenerator.h + CScriptingModule.h + CSkillHandler.h + CSoundBase.h + CStack.h + CStopWatch.h + CThreadHelper.h + CTownHandler.h + ExtraOptionsInfo.h + FunctionList.h + GameCallbackHolder.h + GameConstants.h + GameSettings.h + IBonusTypeHandler.h + IGameCallback.h + IGameEventsReceiver.h + IHandlerBase.h + int3.h + Languages.h + LoadProgress.h + LogicalExpression.h + MetaString.h + ObstacleHandler.h + Point.h + Rect.h + Rect.cpp + ResourceSet.h + RiverHandler.h + RoadHandler.h + ScriptHandler.h + ScopeGuard.h + StartInfo.h + TerrainHandler.h + TextOperations.h + TurnTimerInfo.h + UnlockGuard.h + VCMIDirs.h + vcmi_endian.h + VCMI_Lib.h +) + +assign_source_group(${lib_SRCS} ${lib_HEADERS}) + +if(ENABLE_STATIC_LIBS) + add_library(vcmi STATIC ${lib_SRCS} ${lib_HEADERS}) +else() + add_library(vcmi SHARED ${lib_SRCS} ${lib_HEADERS}) +endif() +set_target_properties(vcmi PROPERTIES COMPILE_DEFINITIONS "VCMI_DLL=1") +target_link_libraries(vcmi PUBLIC + minizip::minizip ZLIB::ZLIB + ${SYSTEM_LIBS} Boost::boost Boost::thread Boost::filesystem Boost::program_options Boost::locale Boost::date_time +) + +if(ENABLE_STATIC_LIBS) + target_compile_definitions(vcmi PRIVATE STATIC_AI) + target_link_libraries(vcmi PRIVATE BattleAI EmptyAI StupidAI VCAI ) if(ENABLE_NULLKILLER_AI) - target_link_libraries(${VCMI_LIB_TARGET} PRIVATE Nullkiller) + target_link_libraries(vcmi PRIVATE Nullkiller) endif() -else() - add_main_lib(${VCMI_LIB_TARGET} SHARED) endif() -if(ENABLE_SINGLE_APP_BUILD) - target_compile_definitions(${VCMI_LIB_TARGET} PUBLIC VCMI_LIB_NAMESPACE=LIB_CLIENT) + +# no longer necessary, but might be useful to keep in future +# unfortunately at the moment tests do not support namespaced build, so enable only on some systems +if(APPLE_IOS OR ANDROID) + target_compile_definitions(vcmi PUBLIC VCMI_LIB_NAMESPACE=VCMI) +endif() + +if(APPLE_IOS) + target_link_libraries(vcmi PUBLIC iOS_utils) +endif() + +target_include_directories(vcmi + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} + PUBLIC ${CMAKE_SOURCE_DIR} + PUBLIC ${CMAKE_SOURCE_DIR}/include +) + +if(WIN32) + set_target_properties(vcmi + PROPERTIES + OUTPUT_NAME "VCMI_lib" + PROJECT_LABEL "VCMI_lib" + ) +endif() + +vcmi_set_output_dir(vcmi "") + +enable_pch(vcmi) + +# We want to deploy assets into build directory for easier debugging without install +if(COPY_CONFIG_ON_BUILD) + add_custom_command(TARGET vcmi POST_BUILD + COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config + COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods + ) +endif() + +# Update version before vcmi compiling +if(TARGET update_version) + add_dependencies(vcmi update_version) +endif() + +if(NOT ENABLE_STATIC_LIBS) + install(TARGETS vcmi RUNTIME DESTINATION ${LIB_DIR} LIBRARY DESTINATION ${LIB_DIR}) +endif() + +if(APPLE_IOS AND NOT USING_CONAN) + get_target_property(LINKED_LIBS vcmi LINK_LIBRARIES) + foreach(LINKED_LIB IN LISTS LINKED_LIBS) + if(NOT TARGET ${LINKED_LIB}) + if(LINKED_LIB MATCHES "\\${CMAKE_SHARED_LIBRARY_SUFFIX}$") + install(FILES ${LINKED_LIB} DESTINATION ${LIB_DIR}) + endif() + continue() + endif() + + get_target_property(LIB_TYPE ${LINKED_LIB} TYPE) + if(NOT LIB_TYPE STREQUAL "SHARED_LIBRARY") + continue() + endif() + + get_target_property(_aliased ${LINKED_LIB} ALIASED_TARGET) + if(_aliased) + set(LINKED_LIB_REAL ${_aliased}) + else() + set(LINKED_LIB_REAL ${LINKED_LIB}) + endif() + + get_target_property(_imported ${LINKED_LIB_REAL} IMPORTED) + if(_imported) + set(INSTALL_TYPE IMPORTED_RUNTIME_ARTIFACTS) + get_target_property(BOOST_DEPENDENCIES ${LINKED_LIB_REAL} INTERFACE_LINK_LIBRARIES) + foreach(BOOST_DEPENDENCY IN LISTS BOOST_DEPENDENCIES) + get_target_property(BOOST_DEPENDENCY_TYPE ${BOOST_DEPENDENCY} TYPE) + if(BOOST_DEPENDENCY_TYPE STREQUAL "SHARED_LIBRARY") + install(IMPORTED_RUNTIME_ARTIFACTS ${BOOST_DEPENDENCY} LIBRARY DESTINATION ${LIB_DIR}) + endif() + endforeach() + else() + set(INSTALL_TYPE TARGETS) + endif() + install(${INSTALL_TYPE} ${LINKED_LIB_REAL} LIBRARY DESTINATION ${LIB_DIR}) + endforeach() endif() diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index b59de08fa..2ac3e39db 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -23,26 +23,6 @@ PlayerState::PlayerState() setNodeType(PLAYER); } -PlayerState::PlayerState(PlayerState && other) noexcept: - CBonusSystemNode(std::move(other)), - color(other.color), - human(other.human), - team(other.team), - resources(other.resources), - cheated(other.cheated), - enteredWinningCheatCode(other.enteredWinningCheatCode), - enteredLosingCheatCode(other.enteredLosingCheatCode), - status(other.status), - daysWithoutCastle(other.daysWithoutCastle) -{ - std::swap(visitedObjects, other.visitedObjects); - std::swap(heroes, other.heroes); - std::swap(towns, other.towns); - std::swap(dwellings, other.dwellings); - std::swap(quests, other.quests); - std::swap(battleBonuses, other.battleBonuses); -} - PlayerState::~PlayerState() = default; std::string PlayerState::nodeName() const diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 66c70619a..0afeec09a 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -27,12 +27,37 @@ struct QuestInfo; struct DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player { + struct VisitedObjectGlobal + { + MapObjectID id; + MapObjectSubID subID; + + bool operator < (const VisitedObjectGlobal & other) const + { + if (id != other.id) + return id < other.id; + else + return subID < other.subID; + } + + template void serialize(Handler &h) + { + h & id; + subID.serializeIdentifier(h, id); + } + }; + public: PlayerColor color; bool human; //true if human controlled player, false for AI TeamID team; TResources resources; + + /// list of objects that were "destroyed" by player, either via simple pick-up (e.g. resources) or defeated heroes or wandering monsters + std::set destroyedObjects; + std::set visitedObjects; // as a std::set, since most accesses here will be from visited status checks + std::set visitedObjectsGlobal; std::vector > heroes; std::vector > towns; std::vector > dwellings; //used for town growth @@ -46,7 +71,6 @@ public: TurnTimerInfo turnTimer; PlayerState(); - PlayerState(PlayerState && other) noexcept; ~PlayerState(); std::string nodeName() const override; @@ -69,7 +93,7 @@ public: return heroes.empty() && towns.empty(); } - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & color; h & human; @@ -82,6 +106,7 @@ public: h & dwellings; h & quests; h & visitedObjects; + h & visitedObjectsGlobal; h & status; h & daysWithoutCastle; h & cheated; @@ -89,6 +114,8 @@ public: h & enteredLosingCheatCode; h & enteredWinningCheatCode; h & static_cast(*this); + if (h.version >= Handler::Version::DESTROYED_OBJECTS) + h & destroyedObjects; } }; @@ -98,12 +125,11 @@ public: TeamID id; //position in gameState::teams std::set players; // members of this team //TODO: boost::array, bool if possible - std::shared_ptr> fogOfWarMap; //[z][x][y] true - visible, false - hidden + std::unique_ptr> fogOfWarMap; //[z][x][y] true - visible, false - hidden TeamState(); - TeamState(TeamState && other) noexcept; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & id; h & players; diff --git a/lib/CRandomGenerator.cpp b/lib/CRandomGenerator.cpp index 657356411..22ff8c78e 100644 --- a/lib/CRandomGenerator.cpp +++ b/lib/CRandomGenerator.cpp @@ -37,12 +37,16 @@ void CRandomGenerator::resetSeed() TRandI CRandomGenerator::getIntRange(int lower, int upper) { - return std::bind(TIntDist(lower, upper), std::ref(rand)); + if (lower <= upper) + return std::bind(TIntDist(lower, upper), std::ref(rand)); + throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); } vstd::TRandI64 CRandomGenerator::getInt64Range(int64_t lower, int64_t upper) { - return std::bind(TInt64Dist(lower, upper), std::ref(rand)); + if(lower <= upper) + return std::bind(TInt64Dist(lower, upper), std::ref(rand)); + throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); } int CRandomGenerator::nextInt(int upper) @@ -62,7 +66,10 @@ int CRandomGenerator::nextInt() vstd::TRand CRandomGenerator::getDoubleRange(double lower, double upper) { - return std::bind(TRealDist(lower, upper), std::ref(rand)); + if(lower <= upper) + return std::bind(TRealDist(lower, upper), std::ref(rand)); + throw std::runtime_error("Invalid range provided: " + std::to_string(lower) + " ... " + std::to_string(upper)); + } double CRandomGenerator::nextDouble(double upper) diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index bfd75f9a7..265b716a7 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -14,7 +14,11 @@ VCMI_LIB_NAMESPACE_BEGIN -using TGenerator = std::mt19937; +/// Generator to use for all randomization in game +/// minstd_rand is selected due to following reasons: +/// 1. Its randomization quality is below mt_19937 however this is unlikely to be noticeable in game +/// 2. It has very low state size, leading to low overhead in size of saved games (due to large number of random generator instances in game) +using TGenerator = std::minstd_rand; using TIntDist = std::uniform_int_distribution; using TInt64Dist = std::uniform_int_distribution; using TRealDist = std::uniform_real_distribution; @@ -83,7 +87,7 @@ private: public: template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { if(h.saving) { diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 82338746c..619429dc7 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -16,12 +16,11 @@ #include "CGeneralTextHandler.h" #include "filesystem/Filesystem.h" +#include "json/JsonBonus.h" +#include "json/JsonUtils.h" #include "modding/IdentifierStorage.h" #include "modding/ModUtility.h" #include "modding/ModScope.h" - -#include "JsonNode.h" - #include "constants/StringConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -59,7 +58,7 @@ std::string CSkill::getNameTranslated() const std::string CSkill::getJsonKey() const { - return modScope + ':' + identifier;; + return modScope + ':' + identifier; } std::string CSkill::getDescriptionTextID(int level) const @@ -77,7 +76,7 @@ void CSkill::registerIcons(const IconRegistar & cb) const { for(int level = 1; level <= 3; level++) { - int frame = 2 + level + 3 * id; + int frame = 2 + level + 3 * id.getNum(); const LevelInfo & skillAtLevel = at(level); cb(frame, 0, "SECSK32", skillAtLevel.iconSmall); cb(frame, 0, "SECSKILL", skillAtLevel.iconMedium); @@ -120,7 +119,7 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInf DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill) { - out << "Skill(" << (int)skill.id << "," << skill.identifier << "): ["; + out << "Skill(" << skill.id.getNum() << "," << skill.identifier << "): ["; for(int i=0; i < skill.levels.size(); i++) out << (i ? "," : "") << skill.levels[i]; return out << "]"; @@ -169,7 +168,7 @@ std::vector CSkillHandler::loadLegacyData() std::vector legacyData; for(int id = 0; id < GameConstants::SKILL_QUANTITY; id++) { - JsonNode skillNode(JsonNode::JsonType::DATA_STRUCT); + JsonNode skillNode; skillNode["name"].String() = skillNames[id]; for(int level = 1; level < NSecondarySkill::levels.size(); level++) { @@ -194,7 +193,8 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & { assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); - bool major, minor; + bool major; + bool minor; major = json["obligatoryMajor"].Bool(); minor = json["obligatoryMinor"].Bool(); @@ -233,7 +233,7 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & skillAtLevel.iconMedium = levelNode["images"]["medium"].String(); skillAtLevel.iconLarge = levelNode["images"]["large"].String(); } - logMod->debug("loaded secondary skill %s(%d)", identifier, (int)skill->id); + logMod->debug("loaded secondary skill %s(%d)", identifier, skill->id.getNum()); return skill; } @@ -256,29 +256,14 @@ void CSkillHandler::beforeValidate(JsonNode & object) inheritNode("expert"); } -std::vector CSkillHandler::getDefaultAllowed() const +std::set CSkillHandler::getDefaultAllowed() const { - std::vector allowedSkills(objects.size(), true); - return allowedSkills; -} + std::set result; -si32 CSkillHandler::decodeSkill(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "skill", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} + for (auto const & skill : objects) + result.insert(skill->getId()); -std::string CSkillHandler::encodeSkill(const si32 index) -{ - return (*VLC->skillh)[SecondarySkill(index)]->identifier; -} - -std::string CSkillHandler::encodeSkillWithType(const si32 index) -{ - return ModUtility::makeFullIdentifier("", "skill", encodeSkill(index)); + return result; } VCMI_LIB_NAMESPACE_END diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index d8f84a3dd..fe699edcb 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -29,14 +29,6 @@ public: std::string iconMedium; std::string iconLarge; std::vector> effects; - - template void serialize(Handler & h, const int version) - { - h & iconSmall; - h & iconMedium; - h & iconLarge; - h & effects; - } }; private: @@ -82,17 +74,6 @@ public: bool onlyOnWaterMap; - template void serialize(Handler & h, const int version) - { - h & id; - h & identifier; - h & gainChance; - h & levels; - h & obligatoryMajor; - h & obligatoryMinor; - h & onlyOnWaterMap; - } - friend class CSkillHandler; friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info); @@ -109,17 +90,7 @@ public: void afterLoadFinalization() override; void beforeValidate(JsonNode & object) override; - std::vector getDefaultAllowed() const override; - - ///json serialization helpers - static si32 decodeSkill(const std::string & identifier); - static std::string encodeSkill(const si32 index); - static std::string encodeSkillWithType(const si32 index); - - template void serialize(Handler & h, const int version) - { - h & objects; - } + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 128b1e2bb..a84f8cd0f 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -72,7 +72,7 @@ void CStack::localInit(BattleInfo * battleInfo) CArmedInstance * army = battle->battleGetArmyObject(side); assert(army); attachTo(*army); - attachTo(const_cast(*type)); + attachToSource(*type); } nativeTerrain = getNativeTerrain(); //save nativeTerrain in the variable on the battle start to avoid dead lock CUnitState::localInit(this); //it causes execution of the CStack::isOnNativeTerrain where nativeTerrain will be considered diff --git a/lib/CStack.h b/lib/CStack.h index 78e8eb750..3252a722a 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -9,7 +9,6 @@ */ #pragma once -#include "JsonNode.h" #include "bonuses/Bonus.h" #include "bonuses/CBonusSystemNode.h" #include "CCreatureHandler.h" //todo: remove @@ -93,7 +92,7 @@ public: return this->owner; } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { //this assumes that stack objects is newly created //stackState is not serialized here diff --git a/lib/CStopWatch.h b/lib/CStopWatch.h index 08167eb3f..9b744a622 100644 --- a/lib/CStopWatch.h +++ b/lib/CStopWatch.h @@ -9,7 +9,7 @@ */ #pragma once -#ifdef __FreeBSD__ +#if defined(__FreeBSD__) || defined(__OpenBSD__) #include #include #include @@ -23,7 +23,9 @@ VCMI_LIB_NAMESPACE_BEGIN class CStopWatch { - si64 start, last, mem; + si64 start; + si64 last; + si64 mem; public: CStopWatch() @@ -55,7 +57,7 @@ public: private: si64 clock() { - #ifdef __FreeBSD__ // TODO: enable also for Apple? + #if defined(__FreeBSD__) || defined(__OpenBSD__) // TODO: enable also for Apple? struct rusage usage; getrusage(RUSAGE_SELF, &usage); return static_cast(usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; diff --git a/lib/CThreadHelper.cpp b/lib/CThreadHelper.cpp index 66f3bf7f9..21f8dc4bb 100644 --- a/lib/CThreadHelper.cpp +++ b/lib/CThreadHelper.cpp @@ -14,7 +14,8 @@ #include #elif defined(VCMI_HAIKU) #include -#elif !defined(VCMI_APPLE) && !defined(VCMI_FREEBSD) && !defined(VCMI_HURD) +#elif !defined(VCMI_APPLE) && !defined(VCMI_FREEBSD) && \ + !defined(VCMI_HURD) && !defined(VCMI_OPENBSD) #include #endif @@ -54,10 +55,25 @@ void CThreadHelper::processTasks() } } -// set name for this thread. -// NOTE: on *nix string will be trimmed to 16 symbols +static thread_local std::string threadNameForLogging; + +std::string getThreadName() +{ + if (!threadNameForLogging.empty()) + return threadNameForLogging; + + return boost::lexical_cast(boost::this_thread::get_id()); +} + +void setThreadNameLoggingOnly(const std::string &name) +{ + threadNameForLogging = name; +} + void setThreadName(const std::string &name) { + threadNameForLogging = name; + #ifdef VCMI_WINDOWS #ifndef __GNUC__ //follows http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx @@ -89,12 +105,12 @@ void setThreadName(const std::string &name) //not supported #endif -#elif defined(__linux__) - prctl(PR_SET_NAME, name.c_str(), 0, 0, 0); #elif defined(VCMI_APPLE) pthread_setname_np(name.c_str()); #elif defined(VCMI_HAIKU) rename_thread(find_thread(NULL), name.c_str()); +#elif defined(VCMI_UNIX) + prctl(PR_SET_NAME, name.c_str(), 0, 0, 0); #endif } diff --git a/lib/CThreadHelper.h b/lib/CThreadHelper.h index 86dcb7619..d9c125f25 100644 --- a/lib/CThreadHelper.h +++ b/lib/CThreadHelper.h @@ -22,7 +22,9 @@ public: void run(); private: boost::mutex rtinm; - int currentTask, amount, threads; + int currentTask; + int amount; + int threads; std::vector *tasks; @@ -60,7 +62,9 @@ public: } private: boost::mutex rtinm; - size_t currentTask, amount, threads; + size_t currentTask; + size_t amount; + size_t threads; Tasks * tasks; std::vector> context; @@ -81,7 +85,14 @@ private: } }; - +/// Sets thread name that will be used for both logs and debugger (if supported) +/// WARNING: on Unix-like systems this method should not be used for main thread since it will also change name of the process void DLL_LINKAGE setThreadName(const std::string &name); +/// Sets thread name for use in logging only +void DLL_LINKAGE setThreadNameLoggingOnly(const std::string &name); + +/// Returns human-readable thread name that was set before, or string form of system-provided thread ID if no human-readable name was set +std::string DLL_LINKAGE getThreadName(); + VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index a33205fb2..5eb5b2e4b 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -12,7 +12,6 @@ #include "VCMI_Lib.h" #include "CGeneralTextHandler.h" -#include "JsonNode.h" #include "constants/StringConstants.h" #include "CCreatureHandler.h" #include "CHeroHandler.h" @@ -23,6 +22,7 @@ #include "filesystem/Filesystem.h" #include "bonuses/Bonus.h" #include "bonuses/Propagators.h" +#include "json/JsonBonus.h" #include "ResourceSet.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" @@ -75,7 +75,7 @@ const BuildingTypeUniqueID CBuilding::getUniqueTypeID() const std::string CBuilding::getJsonKey() const { - return modScope + ':' + identifier;; + return modScope + ':' + identifier; } std::string CBuilding::getNameTranslated() const @@ -144,17 +144,17 @@ CFaction::~CFaction() int32_t CFaction::getIndex() const { - return index; + return index.getNum(); } int32_t CFaction::getIconIndex() const { - return index; //??? + return index.getNum(); //??? } std::string CFaction::getJsonKey() const { - return modScope + ':' + identifier;; + return modScope + ':' + identifier; } void CFaction::registerIcons(const IconRegistar & cb) const @@ -172,8 +172,8 @@ void CFaction::registerIcons(const IconRegistar & cb) const cb(info.icons[1][0] + 2, 0, "ITPA", info.iconSmall[1][0]); cb(info.icons[1][1] + 2, 0, "ITPA", info.iconSmall[1][1]); - cb(index, 1, "CPRSMALL", info.towerIconSmall); - cb(index, 1, "TWCRPORT", info.towerIconLarge); + cb(index.getNum(), 1, "CPRSMALL", info.towerIconSmall); + cb(index.getNum(), 1, "TWCRPORT", info.towerIconLarge); } } @@ -243,11 +243,6 @@ CTown::~CTown() str.dellNull(); } -std::string CTown::getRandomNameTranslated(size_t index) const -{ - return VLC->generaltexth->translate(getRandomNameTextID(index)); -} - std::string CTown::getRandomNameTextID(size_t index) const { return TextIdentifier("faction", faction->modScope, faction->identifier, "randomName", index).get(); @@ -317,7 +312,7 @@ CTownHandler::CTownHandler(): CTownHandler::~CTownHandler() { - delete randomTown; + delete randomFaction; // will also delete randomTown } JsonNode readBuilding(CLegacyConfigParser & parser) @@ -336,9 +331,9 @@ JsonNode readBuilding(CLegacyConfigParser & parser) return ret; } -TPropagatorPtr & CTownHandler::emptyPropagator() +const TPropagatorPtr & CTownHandler::emptyPropagator() { - static TPropagatorPtr emptyProp(nullptr); + static const TPropagatorPtr emptyProp(nullptr); return emptyProp; } @@ -534,7 +529,7 @@ R CTownHandler::getMappedValue(const JsonNode & node, const R defval, const std: void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const { std::shared_ptr b; - static TPropagatorPtr playerPropagator = std::make_shared(CBonusSystemNode::ENodeTypes::PLAYER); + static const TPropagatorPtr playerPropagator = std::make_shared(CBonusSystemNode::ENodeTypes::PLAYER); if(building->bid == BuildingID::TAVERN) { @@ -578,7 +573,7 @@ std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType ty return createBonus(build, type, val, subtype, emptyPropagator()); } -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, TPropagatorPtr & prop) const +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, const TPropagatorPtr & prop) const { std::ostringstream descr; descr << build->getNameTranslated(); @@ -589,7 +584,7 @@ std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building const FactionID & faction, BonusType type, int val, - TPropagatorPtr & prop, + const TPropagatorPtr & prop, const std::string & description, BonusSubtypeID subtype) const { @@ -622,7 +617,7 @@ void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source) { assert(stringID.find(':') == std::string::npos); - assert(!source.meta.empty()); + assert(!source.getModScope().empty()); auto * ret = new CBuilding(); ret->bid = getMappedValue(stringID, BuildingID::NONE, MappedKeys::BUILDING_NAMES_TO_TYPES, false); @@ -645,11 +640,11 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->height = getMappedValue(source["height"], CBuilding::HEIGHT_NO_TOWER, CBuilding::TOWER_TYPES); ret->identifier = stringID; - ret->modScope = source.meta; + ret->modScope = source.getModScope(); ret->town = town; - VLC->generaltexth->registerString(source.meta, ret->getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString(source.meta, ret->getDescriptionTextID(), source["description"].String()); + VLC->generaltexth->registerString(source.getModScope(), ret->getNameTextID(), source["name"].String()); + VLC->generaltexth->registerString(source.getModScope(), ret->getDescriptionTextID(), source["description"].String()); ret->resources = TResources(source["cost"]); ret->produce = TResources(source["produce"]); @@ -686,7 +681,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons //MODS COMPATIBILITY FOR 0.96 if(!ret->produce.nonZero()) { - switch (ret->bid) { + switch (ret->bid.toEnum()) { break; case BuildingID::VILLAGE_HALL: ret->produce[EGameResID::GOLD] = 500; break; case BuildingID::TOWN_HALL : ret->produce[EGameResID::GOLD] = 1000; break; case BuildingID::CITY_HALL : ret->produce[EGameResID::GOLD] = 2000; @@ -734,7 +729,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->town->buildings[ret->bid] = ret; - registerObject(source.meta, ret->town->getBuildingScope(), ret->identifier, ret->bid); + registerObject(source.getModScope(), ret->town->getBuildingScope(), ret->identifier, ret->bid.getNum()); } void CTownHandler::loadBuildings(CTown * town, const JsonNode & source) @@ -756,14 +751,14 @@ void CTownHandler::loadStructure(CTown &town, const std::string & stringID, cons ret->building = nullptr; ret->buildable = nullptr; - VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + VLC->identifiers()->tryRequestIdentifier( source.getModScope(), "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable { ret->building = town.buildings[BuildingID(identifier)]; }); if (source["builds"].isNull()) { - VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + VLC->identifiers()->tryRequestIdentifier( source.getModScope(), "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable { ret->building = town.buildings[BuildingID(identifier)]; }); @@ -949,7 +944,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) } else { - VLC->identifiers()->requestIdentifier( source.meta, "spell", "castleMoat", [=](si32 ability) + VLC->identifiers()->requestIdentifier( source.getModScope(), "spell", "castleMoat", [=](si32 ability) { town->moatAbility = SpellID(ability); }); @@ -989,9 +984,9 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) { int chance = static_cast(node.second.Float()); - VLC->identifiers()->requestIdentifier(node.second.meta, "heroClass",node.first, [=](si32 classID) + VLC->identifiers()->requestIdentifier(node.second.getModScope(), "heroClass",node.first, [=](si32 classID) { - VLC->heroh->classes[HeroClassID(classID)]->selectionProbability[town->faction->getId()] = chance; + VLC->heroclassesh->objects[classID]->selectionProbability[town->faction->getId()] = chance; }); } @@ -999,7 +994,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) { int chance = static_cast(node.second.Float()); - VLC->identifiers()->requestIdentifier(node.second.meta, "spell", node.first, [=](si32 spellID) + VLC->identifiers()->requestIdentifier(node.second.getModScope(), "spell", node.first, [=](si32 spellID) { VLC->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance; }); @@ -1115,19 +1110,19 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod if (object->town) { auto & info = object->town->clientInfo; - info.icons[0][0] = 8 + object->index * 4 + 0; - info.icons[0][1] = 8 + object->index * 4 + 1; - info.icons[1][0] = 8 + object->index * 4 + 2; - info.icons[1][1] = 8 + object->index * 4 + 3; + info.icons[0][0] = 8 + object->index.getNum() * 4 + 0; + info.icons[0][1] = 8 + object->index.getNum() * 4 + 1; + info.icons[1][0] = 8 + object->index.getNum() * 4 + 2; + info.icons[1][1] = 8 + object->index.getNum() * 4 + 3; VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) { // register town once objects are loaded JsonNode config = data["town"]["mapObject"]; config["faction"].String() = name; - config["faction"].meta = scope; - if (config.meta.empty())// MODS COMPATIBILITY FOR 0.96 - config.meta = scope; + config["faction"].setModScope(scope, false); + if (config.getModScope().empty())// MODS COMPATIBILITY FOR 0.96 + config.setModScope(scope, false); VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); // MODS COMPATIBILITY FOR 0.96 @@ -1142,7 +1137,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod }); } - registerObject(scope, "faction", name, object->index); + registerObject(scope, "faction", name, object->index.getNum()); } void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -1158,28 +1153,28 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod if (object->town) { auto & info = object->town->clientInfo; - info.icons[0][0] = (GameConstants::F_NUMBER + object->index) * 2 + 0; - info.icons[0][1] = (GameConstants::F_NUMBER + object->index) * 2 + 1; - info.icons[1][0] = object->index * 2 + 0; - info.icons[1][1] = object->index * 2 + 1; + info.icons[0][0] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 0; + info.icons[0][1] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 1; + info.icons[1][0] = object->index.getNum() * 2 + 0; + info.icons[1][1] = object->index.getNum() * 2 + 1; VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) { // register town once objects are loaded JsonNode config = data["town"]["mapObject"]; config["faction"].String() = name; - config["faction"].meta = scope; + config["faction"].setModScope(scope, false); VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); }); } - registerObject(scope, "faction", name, object->index); + registerObject(scope, "faction", name, object->index.getNum()); } void CTownHandler::loadRandomFaction() { JsonNode randomFactionJson(JsonPath::builtin("config/factions/random.json")); - randomFactionJson.setMeta(ModScope::scopeBuiltin(), true); + randomFactionJson.setModScope(ModScope::scopeBuiltin(), true); loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); } @@ -1206,7 +1201,7 @@ void CTownHandler::initializeRequirements() { logMod->error("Unexpected length of town buildings requirements: %d", node.Vector().size()); logMod->error("Entry contains: "); - logMod->error(node.toJson()); + logMod->error(node.toString()); } auto index = VLC->identifiers()->getIdentifier(requirement.town->getBuildingScope(), node[0]); @@ -1259,31 +1254,28 @@ void CTownHandler::initializeWarMachines() warMachinesToLoad.clear(); } -std::vector CTownHandler::getDefaultAllowed() const +std::set CTownHandler::getDefaultAllowed() const { - std::vector allowedFactions; - allowedFactions.reserve(objects.size()); + std::set allowedFactions; + for(auto town : objects) - { - allowedFactions.push_back(town->town != nullptr); - } + if (town->town != nullptr) + allowedFactions.insert(town->getId()); + return allowedFactions; } std::set CTownHandler::getAllowedFactions(bool withTown) const { - std::set allowedFactions; - std::vector allowed; if (withTown) - allowed = getDefaultAllowed(); - else - allowed.resize( objects.size(), true); + return getDefaultAllowed(); - for (size_t i=0; i(i)); + std::set result; + for(auto town : objects) + result.insert(town->getId()); + + return result; - return allowedFactions; } const std::vector & CTownHandler::getTypeNames() const diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 67c2a10c6..5af1d661b 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -123,25 +123,6 @@ public: void addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const; - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & town; - h & bid; - h & resources; - h & produce; - h & requirements; - h & upgrade; - h & mode; - h & subId; - h & height; - h & overrideBids; - h & buildingBonuses; - h & onVisitBonuses; - h & rewardableObjectInfo; - } - friend class CTownHandler; }; @@ -160,17 +141,6 @@ struct DLL_LINKAGE CStructure std::string identifier; bool hiddenUpgrade; // used only if "building" is upgrade, if true - structure on town screen will behave exactly like parent (mouse clicks, hover texts, etc) - template void serialize(Handler &h, const int version) - { - h & pos; - h & defName; - h & borderName; - h & areaName; - h & identifier; - h & building; - h & buildable; - h & hiddenUpgrade; - } }; struct DLL_LINKAGE SPuzzleInfo @@ -179,15 +149,6 @@ struct DLL_LINKAGE SPuzzleInfo si16 x, y; //position ui16 whenUncovered; //determines the sequnce of discovering (the lesser it is the sooner puzzle will be discovered) ImagePath filename; //file with graphic of this puzzle - - template void serialize(Handler &h, const int version) - { - h & number; - h & x; - h & y; - h & whenUncovered; - h & filename; - } }; class DLL_LINKAGE CFaction : public Faction @@ -238,20 +199,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & index; - h & nativeTerrain; - h & boatType; - h & alignment; - h & town; - h & creatureBg120; - h & creatureBg130; - h & puzzleMap; - } }; class DLL_LINKAGE CTown @@ -270,7 +217,6 @@ public: void setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const; //may affect only mutable field BuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; - std::string getRandomNameTranslated(size_t index) const; std::string getRandomNameTextID(size_t index) const; size_t getRandomNamesCount() const; @@ -323,44 +269,7 @@ public: std::string towerIconSmall; std::string towerIconLarge; - template void serialize(Handler &h, const int version) - { - h & icons; - h & iconSmall; - h & iconLarge; - h & tavernVideo; - h & musicTheme; - h & townBackground; - h & guildBackground; - h & guildWindow; - h & buildingsIcons; - h & hallBackground; - h & hallSlots; - h & structures; - h & siegePrefix; - h & siegePositions; - h & siegeShooter; - h & towerIconSmall; - h & towerIconLarge; - } } clientInfo; - - template void serialize(Handler &h, const int version) - { - h & namesCount; - h & faction; - h & creatures; - h & dwellings; - h & dwellingNames; - h & buildings; - h & hordeLvl; - h & mageLevel; - h & primaryRes; - h & warMachine; - h & clientInfo; - h & moatAbility; - h & defaultTavernChance; - } private: ///generated bonusing buildings messages for all towns of this type. @@ -380,7 +289,7 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase requirementsToLoad; std::vector overriddenBidsToLoad; //list of buildings, which bonuses should be overridden. - static TPropagatorPtr & emptyPropagator(); + static const TPropagatorPtr & emptyPropagator(); void initializeRequirements(); void initializeOverridden(); @@ -393,12 +302,12 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase createBonus(CBuilding * build, BonusType type, int val) const; std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, TPropagatorPtr & prop) const; + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, const TPropagatorPtr & prop) const; std::shared_ptr createBonusImpl(const BuildingID & building, const FactionID & faction, BonusType type, int val, - TPropagatorPtr & prop, + const TPropagatorPtr & prop, const std::string & description, BonusSubtypeID subtype) const; @@ -440,17 +349,11 @@ public: void loadCustom() override; void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; + std::set getDefaultAllowed() const; std::set getAllowedFactions(bool withTown = true) const; static void loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building); - template void serialize(Handler &h, const int version) - { - h & objects; - h & randomTown; - } - protected: const std::vector & getTypeNames() const override; CFaction * loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override; diff --git a/lib/Color.h b/lib/Color.h index 2c231f2b1..6ad89513a 100644 --- a/lib/Color.h +++ b/lib/Color.h @@ -50,7 +50,7 @@ public: {} template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & r; h & g; diff --git a/lib/ConstTransitivePtr.h b/lib/ConstTransitivePtr.h index e29ccbced..3cfa4ea2b 100644 --- a/lib/ConstTransitivePtr.h +++ b/lib/ConstTransitivePtr.h @@ -69,7 +69,7 @@ public: ptr = nullptr; } - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & ptr; } diff --git a/lib/ExtraOptionsInfo.cpp b/lib/ExtraOptionsInfo.cpp new file mode 100644 index 000000000..2411608b7 --- /dev/null +++ b/lib/ExtraOptionsInfo.cpp @@ -0,0 +1,21 @@ +/* + * ExtraOptionsInfo.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "ExtraOptionsInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool ExtraOptionsInfo::operator == (const ExtraOptionsInfo & other) const +{ + return cheatsAllowed == other.cheatsAllowed && + unlimitedReplay == other.unlimitedReplay; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/ExtraOptionsInfo.h b/lib/ExtraOptionsInfo.h new file mode 100644 index 000000000..cbbb42263 --- /dev/null +++ b/lib/ExtraOptionsInfo.h @@ -0,0 +1,30 @@ +/* + * ExtraOptionsInfo.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +struct DLL_LINKAGE ExtraOptionsInfo +{ + bool cheatsAllowed = true; + bool unlimitedReplay = false; + + bool operator == (const ExtraOptionsInfo & other) const; + + template + void serialize(Handler &h) + { + h & cheatsAllowed; + h & unlimitedReplay; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameCallbackHolder.h b/lib/GameCallbackHolder.h new file mode 100644 index 000000000..8888d3105 --- /dev/null +++ b/lib/GameCallbackHolder.h @@ -0,0 +1,26 @@ +/* + * GameCallbackHolder.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +class IGameCallback; + +class DLL_LINKAGE GameCallbackHolder +{ +public: + IGameCallback * const cb; + + explicit GameCallbackHolder(IGameCallback *cb): + cb(cb) + {} +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 15a593e1b..6f59f69bc 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -9,7 +9,7 @@ */ #include "StdInc.h" #include "GameSettings.h" -#include "JsonNode.h" +#include "json/JsonUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -74,6 +74,7 @@ void GameSettings::load(const JsonNode & input) {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, {EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" }, {EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" }, + {EGameSettings::HEROES_TAVERN_INVITE, "heroes", "tavernInvite" }, {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, @@ -95,11 +96,13 @@ void GameSettings::load(const JsonNode & input) {EGameSettings::TEXTS_ROAD, "textData", "road" }, {EGameSettings::TEXTS_SPELL, "textData", "spell" }, {EGameSettings::TEXTS_TERRAIN, "textData", "terrain" }, + {EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" }, {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, + {EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES, "pathfinder", "originalFlyRules" }, {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, }; @@ -118,11 +121,10 @@ void GameSettings::load(const JsonNode & input) const JsonNode & GameSettings::getValue(EGameSettings option) const { - assert(option < EGameSettings::OPTIONS_COUNT); auto index = static_cast(option); - assert(!gameSettings[index].isNull()); - return gameSettings[index]; + assert(!gameSettings.at(index).isNull()); + return gameSettings.at(index); } VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.h b/lib/GameSettings.h index 750c9abab..65265147f 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -38,6 +38,7 @@ enum class EGameSettings HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, HEROES_STARTING_STACKS_CHANCES, HEROES_BACKPACK_CAP, + HEROES_TAVERN_INVITE, MARKETS_BLACK_MARKET_RESTOCK_PERIOD, BANKS_SHOW_GUARDS_COMPOSITION, MODULE_COMMANDERS, @@ -60,10 +61,12 @@ enum class EGameSettings MAP_FORMAT_JSON_VCMI, MAP_FORMAT_IN_THE_WAKE_OF_GODS, PATHFINDER_USE_BOAT, + PATHFINDER_IGNORE_GUARDS, PATHFINDER_USE_MONOLITH_TWO_WAY, PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, PATHFINDER_USE_WHIRLPOOL, + PATHFINDER_ORIGINAL_FLY_RULES, TOWNS_BUILDINGS_PER_TURN_CAP, TOWNS_STARTING_DWELLING_CHANCES, COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, @@ -95,7 +98,7 @@ public: const JsonNode & getValue(EGameSettings option) const override; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & gameSettings; } diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index a2b7f8d3d..ae7df063b 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -16,19 +16,21 @@ #include "CBonusTypeHandler.h" #include "BattleFieldHandler.h" #include "ObstacleHandler.h" -#include "bonuses/CBonusSystemNode.h" #include "bonuses/Limiters.h" #include "bonuses/Propagators.h" #include "bonuses/Updaters.h" -#include "serializer/CSerializer.h" // for SAVEGAME_MAGIC -#include "serializer/BinaryDeserializer.h" -#include "serializer/BinarySerializer.h" -#include "serializer/CLoadIntegrityValidator.h" +#include "networkPacks/ArtifactLocation.h" +#include "serializer/CLoadFile.h" +#include "serializer/CSaveFile.h" #include "rmg/CMapGenOptions.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" +#include "mapObjects/CGMarket.h" +#include "mapObjects/CGTownInstance.h" #include "mapObjects/CObjectHandler.h" +#include "mapObjects/CQuest.h" +#include "mapObjects/MiscObjects.h" #include "mapObjects/ObjectTemplate.h" #include "campaign/CampaignState.h" #include "StartInfo.h" @@ -41,6 +43,7 @@ #include "modding/CModInfo.h" #include "modding/IdentifierStorage.h" #include "modding/CModVersion.h" +#include "modding/ActiveModsInSaveList.h" #include "CPlayerState.h" #include "GameSettings.h" #include "ScriptHandler.h" @@ -48,8 +51,6 @@ #include "RiverHandler.h" #include "TerrainHandler.h" -#include "serializer/Connection.h" - VCMI_LIB_NAMESPACE_BEGIN void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const @@ -145,14 +146,14 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std: } } -void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const +void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) { for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)]); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE).toArtifact()); for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR)]); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MINOR).toArtifact()); - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR)]); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MAJOR).toArtifact()); } void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) @@ -161,7 +162,7 @@ void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std:: { const spells::Spell * spell = SpellID(i).toSpell(); - if (!isAllowed(0, spell->getIndex())) + if (!isAllowed(spell->getId())) continue; if (level.has_value() && spell->getLevel() != level) @@ -184,6 +185,7 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) CMapHeader dum; StartInfo * si = nullptr; + ActiveModsInSaveList activeMods; logGlobal->info("\tReading header"); in.serializer & dum; @@ -191,8 +193,8 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) logGlobal->info("\tReading options"); in.serializer & si; - logGlobal->info("\tReading handlers"); - in.serializer & *VLC; + logGlobal->info("\tReading mod list"); + in.serializer & activeMods; logGlobal->info("\tReading gamestate"); in.serializer & gs; @@ -201,20 +203,21 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) template void CPrivilegedInfoCallback::saveCommonState(Saver & out) const { + ActiveModsInSaveList activeMods; + logGlobal->info("Saving lib part of game..."); out.putMagicBytes(SAVEGAME_MAGIC); logGlobal->info("\tSaving header"); out.serializer & static_cast(*gs->map); logGlobal->info("\tSaving options"); out.serializer & gs->scenarioOps; - logGlobal->info("\tSaving handlers"); - out.serializer & *VLC; + logGlobal->info("\tSaving mod list"); + out.serializer & activeMods; logGlobal->info("\tSaving gamestate"); out.serializer & gs; } // hardly memory usage for `-gdwarf-4` flag -template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadIntegrityValidator &); template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadFile &); template DLL_LINKAGE void CPrivilegedInfoCallback::saveCommonState(CSaveFile &) const; @@ -265,6 +268,32 @@ CArmedInstance * CNonConstInfoCallback::getArmyInstance(const ObjectInstanceID & return dynamic_cast(getObjInstance(oid)); } +CArtifactSet * CNonConstInfoCallback::getArtSet(const ArtifactLocation & loc) +{ + if(auto hero = getHero(loc.artHolder)) + { + if(loc.creature.has_value()) + { + if(loc.creature.value() == SlotID::COMMANDER_SLOT_PLACEHOLDER) + return hero->commander; + else + return hero->getStackPtr(loc.creature.value()); + } + else + { + return hero; + } + } + else if(auto market = dynamic_cast(getObjInstance(loc.artHolder))) + { + return market; + } + else + { + return nullptr; + } +} + bool IGameCallback::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) { //only server knows diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index dd80b9dde..95151d30e 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -12,7 +12,7 @@ #include #include "CGameInfoCallback.h" // for CGameInfoCallback -#include "CRandomGenerator.h" +#include "networkPacks/ObjProperty.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,6 +22,7 @@ struct BlockingDialog; struct TeleportDialog; struct StackLocation; struct ArtifactLocation; +class CRandomGenerator; class CCreatureSet; class CStackBasicDescriptor; class CGCreature; @@ -60,7 +61,7 @@ public: void getAllTiles(std::unordered_set &tiles, std::optional player, int level, std::function filter) const; //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant - void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const; + void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand); void getAllowedSpells(std::vector &out, std::optional level = std::nullopt); template @@ -73,15 +74,17 @@ public: class DLL_LINKAGE IGameEventCallback { public: - virtual void setObjProperty(ObjectInstanceID objid, int prop, si64 val) = 0; + virtual void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) = 0; + virtual void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) = 0; virtual void showInfoDialog(InfoWindow * iw) = 0; virtual void showInfoDialog(const std::string & msg, PlayerColor player) = 0; virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=0; virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; - virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) = 0; + virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) = 0; virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; + virtual void giveExperience(const CGHeroInstance * hero, TExpType val) =0; virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; virtual void showBlockingDialog(BlockingDialog *iw) =0; @@ -105,8 +108,7 @@ public: virtual void removeAfterVisit(const CGObjectInstance *object) = 0; //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! virtual bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) = 0; - virtual bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) = 0; - virtual void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) = 0; + virtual bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble = std::nullopt) = 0; virtual void removeArtifact(const ArtifactLocation &al) = 0; virtual bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) = 0; @@ -120,6 +122,7 @@ public: virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; virtual void giveHeroBonus(GiveBonus * bonus)=0; virtual void setMovePoints(SetMovePoints * smp)=0; + virtual void setMovePoints(ObjectInstanceID hid, int val, bool absolute)=0; virtual void setManaPoints(ObjectInstanceID hid, int val)=0; virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0; virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0; @@ -143,6 +146,7 @@ public: using CGameInfoCallback::getTile; using CGameInfoCallback::getArtInstance; using CGameInfoCallback::getObjInstance; + using CGameInfoCallback::getArtSet; PlayerState * getPlayerState(const PlayerColor & color, bool verbose = true); TeamState * getTeam(const TeamID & teamID); //get team by team ID @@ -153,6 +157,7 @@ public: CArtifactInstance * getArtInstance(const ArtifactInstanceID & aid); CGObjectInstance * getObjInstance(const ObjectInstanceID & oid); CArmedInstance * getArmyInstance(const ObjectInstanceID & oid); + CArtifactSet * getArtSet(const ArtifactLocation & loc); virtual void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) = 0; }; diff --git a/lib/IHandlerBase.h b/lib/IHandlerBase.h index f513f3593..1666acee1 100644 --- a/lib/IHandlerBase.h +++ b/lib/IHandlerBase.h @@ -44,18 +44,21 @@ public: /// allows handler to do post-loading step for validation or integration of loaded data virtual void afterLoadFinalization(){}; - /** - * Gets a list of objects that are allowed by default on maps - * - * @return a list of allowed objects, the index is the object id - */ - virtual std::vector getDefaultAllowed() const = 0; - - virtual ~IHandlerBase(){} + virtual ~IHandlerBase() = default; }; template class CHandlerBase : public _ServiceBase, public IHandlerBase { + const _Object * getObjectImpl(const int32_t index) const + { + if(index < 0 || index >= objects.size()) + { + logMod->error("%s id %d is invalid", getTypeNames()[0], index); + throw std::runtime_error("Attempt to access invalid index " + std::to_string(index) + " of type " + getTypeNames().front()); + } + return objects[index].get(); + } + public: virtual ~CHandlerBase() { @@ -63,22 +66,21 @@ public: { o.dellNull(); } - } const Entity * getBaseByIndex(const int32_t index) const override { - return getByIndex(index); + return getObjectImpl(index); } const _ObjectBase * getById(const _ObjectID & id) const override { - return (*this)[id].get(); + return getObjectImpl(id.getNum()); } const _ObjectBase * getByIndex(const int32_t index) const override { - return (*this)[_ObjectID(index)].get(); + return getObjectImpl(index); } void forEachBase(const std::function & cb) const override @@ -112,21 +114,14 @@ public: registerObject(scope, type_name, name, object->getIndex()); } - ConstTransitivePtr<_Object> operator[] (const _ObjectID id) const + const _Object * operator[] (const _ObjectID id) const { - const int32_t raw_id = id.getNum(); - return operator[](raw_id); + return getObjectImpl(id.getNum()); } - ConstTransitivePtr<_Object> operator[] (int32_t index) const + const _Object * operator[] (int32_t index) const { - if(index < 0 || index >= objects.size()) - { - logMod->error("%s id %d is invalid", getTypeNames()[0], index); - throw std::runtime_error("internal error"); - } - - return objects[index]; + return getObjectImpl(index); } void updateEntity(int32_t index, const JsonNode & data) diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp deleted file mode 100644 index 8eaee0a25..000000000 --- a/lib/JsonDetail.cpp +++ /dev/null @@ -1,1268 +0,0 @@ -/* - * JsonDetail.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ - -#include "StdInc.h" -#include "JsonDetail.h" - -#include "VCMI_Lib.h" -#include "TextOperations.h" - -#include "filesystem/Filesystem.h" -#include "modding/ModScope.h" -#include "modding/CModHandler.h" -#include "ScopeGuard.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static const JsonNode nullNode; - -template -void JsonWriter::writeContainer(Iterator begin, Iterator end) -{ - if (begin == end) - return; - - prefix += '\t'; - - writeEntry(begin++); - - while (begin != end) - { - out << (compactMode ? ", " : ",\n"); - writeEntry(begin++); - } - - out << (compactMode ? "" : "\n"); - prefix.resize(prefix.size()-1); -} - -void JsonWriter::writeEntry(JsonMap::const_iterator entry) -{ - if(!compactMode) - { - if (!entry->second.meta.empty()) - out << prefix << " // " << entry->second.meta << "\n"; - if(!entry->second.flags.empty()) - out << prefix << " // flags: " << boost::algorithm::join(entry->second.flags, ", ") << "\n"; - out << prefix; - } - writeString(entry->first); - out << " : "; - writeNode(entry->second); -} - -void JsonWriter::writeEntry(JsonVector::const_iterator entry) -{ - if(!compactMode) - { - if (!entry->meta.empty()) - out << prefix << " // " << entry->meta << "\n"; - if(!entry->flags.empty()) - out << prefix << " // flags: " << boost::algorithm::join(entry->flags, ", ") << "\n"; - out << prefix; - } - writeNode(*entry); -} - -void JsonWriter::writeString(const std::string &string) -{ - static const std::string escaped = "\"\\\b\f\n\r\t/"; - - static const std::array escaped_code = {'\"', '\\', 'b', 'f', 'n', 'r', 't', '/'}; - - out <<'\"'; - size_t pos = 0; - size_t start = 0; - for (; poswarn("File %s is not a valid JSON file!", fileName); - logMod->warn(errors); - } - return root; -} - -bool JsonParser::isValid() -{ - return errors.empty(); -} - -bool JsonParser::extractSeparator() -{ - if (!extractWhitespace()) - return false; - - if ( input[pos] !=':') - return error("Separator expected"); - - pos++; - return true; -} - -bool JsonParser::extractValue(JsonNode &node) -{ - if (!extractWhitespace()) - return false; - - switch (input[pos]) - { - case '\"': return extractString(node); - case 'n' : return extractNull(node); - case 't' : return extractTrue(node); - case 'f' : return extractFalse(node); - case '{' : return extractStruct(node); - case '[' : return extractArray(node); - case '-' : return extractFloat(node); - default: - { - if (input[pos] >= '0' && input[pos] <= '9') - return extractFloat(node); - return error("Value expected!"); - } - } -} - -bool JsonParser::extractWhitespace(bool verbose) -{ - while (true) - { - while(pos < input.size() && static_cast(input[pos]) <= ' ') - { - if (input[pos] == '\n') - { - lineCount++; - lineStart = pos+1; - } - pos++; - } - if (pos >= input.size() || input[pos] != '/') - break; - - pos++; - if (pos == input.size()) - break; - if (input[pos] == '/') - pos++; - else - error("Comments must consist from two slashes!", true); - - while (pos < input.size() && input[pos] != '\n') - pos++; - } - - if (pos >= input.size() && verbose) - return error("Unexpected end of file!"); - return true; -} - -bool JsonParser::extractEscaping(std::string &str) -{ - switch(input[pos]) - { - break; case '\"': str += '\"'; - break; case '\\': str += '\\'; - break; case 'b': str += '\b'; - break; case 'f': str += '\f'; - break; case 'n': str += '\n'; - break; case 'r': str += '\r'; - break; case 't': str += '\t'; - break; case '/': str += '/'; - break; default: return error("Unknown escape sequence!", true); - } - return true; -} - -bool JsonParser::extractString(std::string &str) -{ - if (input[pos] != '\"') - return error("String expected!"); - pos++; - - size_t first = pos; - - while (pos != input.size()) - { - if (input[pos] == '\"') // Correct end of string - { - str.append( &input[first], pos-first); - pos++; - return true; - } - if (input[pos] == '\\') // Escaping - { - str.append( &input[first], pos-first); - pos++; - if (pos == input.size()) - break; - extractEscaping(str); - first = pos + 1; - } - if (input[pos] == '\n') // end-of-line - { - str.append( &input[first], pos-first); - return error("Closing quote not found!", true); - } - if(static_cast(input[pos]) < ' ') // control character - { - str.append( &input[first], pos-first); - first = pos+1; - error("Illegal character in the string!", true); - } - pos++; - } - return error("Unterminated string!"); -} - -bool JsonParser::extractString(JsonNode &node) -{ - std::string str; - if (!extractString(str)) - return false; - - node.setType(JsonNode::JsonType::DATA_STRING); - node.String() = str; - return true; -} - -bool JsonParser::extractLiteral(const std::string &literal) -{ - if (literal.compare(0, literal.size(), &input[pos], literal.size()) != 0) - { - while (pos < input.size() && ((input[pos]>'a' && input[pos]<'z') - || (input[pos]>'A' && input[pos]<'Z'))) - pos++; - return error("Unknown literal found", true); - } - - pos += literal.size(); - return true; -} - -bool JsonParser::extractNull(JsonNode &node) -{ - if (!extractLiteral("null")) - return false; - - node.clear(); - return true; -} - -bool JsonParser::extractTrue(JsonNode &node) -{ - if (!extractLiteral("true")) - return false; - - node.Bool() = true; - return true; -} - -bool JsonParser::extractFalse(JsonNode &node) -{ - if (!extractLiteral("false")) - return false; - - node.Bool() = false; - return true; -} - -bool JsonParser::extractStruct(JsonNode &node) -{ - node.setType(JsonNode::JsonType::DATA_STRUCT); - pos++; - - if (!extractWhitespace()) - return false; - - //Empty struct found - if (input[pos] == '}') - { - pos++; - return true; - } - - while (true) - { - if (!extractWhitespace()) - return false; - - std::string key; - if (!extractString(key)) - return false; - - // split key string into actual key and meta-flags - std::vector keyAndFlags; - boost::split(keyAndFlags, key, boost::is_any_of("#")); - key = keyAndFlags[0]; - // check for unknown flags - helps with debugging - std::vector knownFlags = { "override" }; - for(int i = 1; i < keyAndFlags.size(); i++) - { - if(!vstd::contains(knownFlags, keyAndFlags[i])) - error("Encountered unknown flag #" + keyAndFlags[i], true); - } - - if (node.Struct().find(key) != node.Struct().end()) - error("Dublicated element encountered!", true); - - if (!extractSeparator()) - return false; - - if (!extractElement(node.Struct()[key], '}')) - return false; - - // flags from key string belong to referenced element - for(int i = 1; i < keyAndFlags.size(); i++) - node.Struct()[key].flags.push_back(keyAndFlags[i]); - - if (input[pos] == '}') - { - pos++; - return true; - } - } -} - -bool JsonParser::extractArray(JsonNode &node) -{ - pos++; - node.setType(JsonNode::JsonType::DATA_VECTOR); - - if (!extractWhitespace()) - return false; - - //Empty array found - if (input[pos] == ']') - { - pos++; - return true; - } - - while (true) - { - //NOTE: currently 50% of time is this vector resizing. - //May be useful to use list during parsing and then swap() all items to vector - node.Vector().resize(node.Vector().size()+1); - - if (!extractElement(node.Vector().back(), ']')) - return false; - - if (input[pos] == ']') - { - pos++; - return true; - } - } -} - -bool JsonParser::extractElement(JsonNode &node, char terminator) -{ - if (!extractValue(node)) - return false; - - if (!extractWhitespace()) - return false; - - bool comma = (input[pos] == ','); - if (comma ) - { - pos++; - if (!extractWhitespace()) - return false; - } - - if (input[pos] == terminator) - { - //FIXME: MOD COMPATIBILITY: Too many of these right now, re-enable later - //if (comma) - //error("Extra comma found!", true); - return true; - } - - if (!comma) - error("Comma expected!", true); - - return true; -} - -bool JsonParser::extractFloat(JsonNode &node) -{ - assert(input[pos] == '-' || (input[pos] >= '0' && input[pos] <= '9')); - bool negative=false; - double result=0; - si64 integerPart = 0; - bool isFloat = false; - - if (input[pos] == '-') - { - pos++; - negative = true; - } - - if (input[pos] < '0' || input[pos] > '9') - return error("Number expected!"); - - //Extract integer part - while (input[pos] >= '0' && input[pos] <= '9') - { - integerPart = integerPart*10+(input[pos]-'0'); - pos++; - } - - result = static_cast(integerPart); - - if (input[pos] == '.') - { - //extract fractional part - isFloat = true; - pos++; - double fractMult = 0.1; - if (input[pos] < '0' || input[pos] > '9') - return error("Decimal part expected!"); - - while (input[pos] >= '0' && input[pos] <= '9') - { - result = result + fractMult*(input[pos]-'0'); - fractMult /= 10; - pos++; - } - } - - if(input[pos] == 'e') - { - //extract exponential part - pos++; - isFloat = true; - bool powerNegative = false; - double power = 0; - - if(input[pos] == '-') - { - pos++; - powerNegative = true; - } - else if(input[pos] == '+') - { - pos++; - } - - if (input[pos] < '0' || input[pos] > '9') - return error("Exponential part expected!"); - - while (input[pos] >= '0' && input[pos] <= '9') - { - power = power*10 + (input[pos]-'0'); - pos++; - } - - if(powerNegative) - power = -power; - - result *= std::pow(10, power); - } - - if(isFloat) - { - if(negative) - result = -result; - - node.setType(JsonNode::JsonType::DATA_FLOAT); - node.Float() = result; - } - else - { - if(negative) - integerPart = -integerPart; - - node.setType(JsonNode::JsonType::DATA_INTEGER); - node.Integer() = integerPart; - } - - return true; -} - -bool JsonParser::error(const std::string &message, bool warning) -{ - std::ostringstream stream; - std::string type(warning?" warning: ":" error: "); - - stream << "At line " << lineCount << ", position "< stringToType = -{ - {"null", JsonNode::JsonType::DATA_NULL}, - {"boolean", JsonNode::JsonType::DATA_BOOL}, - {"number", JsonNode::JsonType::DATA_FLOAT}, - {"string", JsonNode::JsonType::DATA_STRING}, - {"array", JsonNode::JsonType::DATA_VECTOR}, - {"object", JsonNode::JsonType::DATA_STRUCT} -}; - -namespace -{ - namespace Common - { - std::string emptyCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - // check is not needed - e.g. incorporated into another check - return ""; - } - - std::string notImplementedCheck(Validation::ValidationData & validator, - const JsonNode & baseSchema, - const JsonNode & schema, - const JsonNode & data) - { - return "Not implemented entry in schema"; - } - - std::string schemaListCheck(Validation::ValidationData & validator, - const JsonNode & baseSchema, - const JsonNode & schema, - const JsonNode & data, - const std::string & errorMsg, - const std::function & isValid) - { - std::string errors = "\n"; - size_t result = 0; - - for(const auto & schemaEntry : schema.Vector()) - { - std::string error = check(schemaEntry, data, validator); - if (error.empty()) - { - result++; - } - else - { - errors += error; - errors += "\n"; - } - } - if (isValid(result)) - return ""; - else - return validator.makeErrorMessage(errorMsg) + errors; - } - - std::string allOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass all schemas", [&](size_t count) - { - return count == schema.Vector().size(); - }); - } - - std::string anyOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass any schema", [&](size_t count) - { - return count > 0; - }); - } - - std::string oneOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass exactly one schema", [&](size_t count) - { - return count == 1; - }); - } - - std::string notCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (check(schema, data, validator).empty()) - return validator.makeErrorMessage("Successful validation against negative check"); - return ""; - } - - std::string enumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - for(const auto & enumEntry : schema.Vector()) - { - if (data == enumEntry) - return ""; - } - return validator.makeErrorMessage("Key must have one of predefined values"); - } - - std::string typeCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - const auto & typeName = schema.String(); - auto it = stringToType.find(typeName); - if(it == stringToType.end()) - { - return validator.makeErrorMessage("Unknown type in schema:" + typeName); - } - - JsonNode::JsonType type = it->second; - - //FIXME: hack for integer values - if(data.isNumber() && type == JsonNode::JsonType::DATA_FLOAT) - return ""; - - if(type != data.getType() && data.getType() != JsonNode::JsonType::DATA_NULL) - return validator.makeErrorMessage("Type mismatch! Expected " + schema.String()); - return ""; - } - - std::string refCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string URI = schema.String(); - //node must be validated using schema pointed by this reference and not by data here - //Local reference. Turn it into more easy to handle remote ref - if (boost::algorithm::starts_with(URI, "#")) - { - const std::string name = validator.usedSchemas.back(); - const std::string nameClean = name.substr(0, name.find('#')); - URI = nameClean + URI; - } - return check(URI, data, validator); - } - - std::string formatCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - auto formats = Validation::getKnownFormats(); - std::string errors; - auto checker = formats.find(schema.String()); - if (checker != formats.end()) - { - if (data.isString()) - { - std::string result = checker->second(data); - if (!result.empty()) - errors += validator.makeErrorMessage(result); - } - else - { - errors += validator.makeErrorMessage("Format value must be string: " + schema.String()); - } - } - else - errors += validator.makeErrorMessage("Unsupported format type: " + schema.String()); - return errors; - } - } - - namespace String - { - std::string maxLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.String().size() > schema.Float()) - return validator.makeErrorMessage((boost::format("String is longer than %d symbols") % schema.Float()).str()); - return ""; - } - - std::string minLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.String().size() < schema.Float()) - return validator.makeErrorMessage((boost::format("String is shorter than %d symbols") % schema.Float()).str()); - return ""; - } - } - - namespace Number - { - - std::string maximumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (baseSchema["exclusiveMaximum"].Bool()) - { - if (data.Float() >= schema.Float()) - return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); - } - else - { - if (data.Float() > schema.Float()) - return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); - } - return ""; - } - - std::string minimumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (baseSchema["exclusiveMinimum"].Bool()) - { - if (data.Float() <= schema.Float()) - return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); - } - else - { - if (data.Float() < schema.Float()) - return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); - } - return ""; - } - - std::string multipleOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - double result = data.Float() / schema.Float(); - if (floor(result) != result) - return validator.makeErrorMessage((boost::format("Value is not divisible by %d") % schema.Float()).str()); - return ""; - } - } - - namespace Vector - { - std::string itemEntryCheck(Validation::ValidationData & validator, const JsonVector & items, const JsonNode & schema, size_t index) - { - validator.currentPath.emplace_back(); - validator.currentPath.back().Float() = static_cast(index); - auto onExit = vstd::makeScopeGuard([&]() - { - validator.currentPath.pop_back(); - }); - - if (!schema.isNull()) - return check(schema, items[index], validator); - return ""; - } - - std::string itemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for (size_t i=0; i i) - errors += itemEntryCheck(validator, data.Vector(), schema.Vector()[i], i); - } - else - { - errors += itemEntryCheck(validator, data.Vector(), schema, i); - } - } - return errors; - } - - std::string additionalItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - // "items" is struct or empty (defaults to empty struct) - validation always successful - const JsonNode & items = baseSchema["items"]; - if (items.getType() != JsonNode::JsonType::DATA_VECTOR) - return ""; - - for (size_t i=items.Vector().size(); i schema.Float()) - return validator.makeErrorMessage((boost::format("Length is bigger than %d") % schema.Float()).str()); - return ""; - } - - std::string uniqueItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (schema.Bool()) - { - for (auto itA = schema.Vector().begin(); itA != schema.Vector().end(); itA++) - { - auto itB = itA; - while (++itB != schema.Vector().end()) - { - if (*itA == *itB) - return validator.makeErrorMessage("List must consist from unique items"); - } - } - } - return ""; - } - } - - namespace Struct - { - std::string maxPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.Struct().size() > schema.Float()) - return validator.makeErrorMessage((boost::format("Number of entries is bigger than %d") % schema.Float()).str()); - return ""; - } - - std::string minPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.Struct().size() < schema.Float()) - return validator.makeErrorMessage((boost::format("Number of entries is less than %d") % schema.Float()).str()); - return ""; - } - - std::string uniquePropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - for (auto itA = data.Struct().begin(); itA != data.Struct().end(); itA++) - { - auto itB = itA; - while (++itB != data.Struct().end()) - { - if (itA->second == itB->second) - return validator.makeErrorMessage("List must consist from unique items"); - } - } - return ""; - } - - std::string requiredCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & required : schema.Vector()) - { - if (data[required.String()].isNull()) - errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); - } - return errors; - } - - std::string dependenciesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & deps : schema.Struct()) - { - if (!data[deps.first].isNull()) - { - if (deps.second.getType() == JsonNode::JsonType::DATA_VECTOR) - { - JsonVector depList = deps.second.Vector(); - for(auto & depEntry : depList) - { - if (data[depEntry.String()].isNull()) - errors += validator.makeErrorMessage("Property " + depEntry.String() + " required for " + deps.first + " is missing"); - } - } - else - { - if (!check(deps.second, data, validator).empty()) - errors += validator.makeErrorMessage("Requirements for " + deps.first + " are not fulfilled"); - } - } - } - return errors; - } - - std::string propertyEntryCheck(Validation::ValidationData & validator, const JsonNode &node, const JsonNode & schema, const std::string & nodeName) - { - validator.currentPath.emplace_back(); - validator.currentPath.back().String() = nodeName; - auto onExit = vstd::makeScopeGuard([&]() - { - validator.currentPath.pop_back(); - }); - - // there is schema specifically for this item - if (!schema.isNull()) - return check(schema, node, validator); - return ""; - } - - std::string propertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - - for(const auto & entry : data.Struct()) - errors += propertyEntryCheck(validator, entry.second, schema[entry.first], entry.first); - return errors; - } - - std::string additionalPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & entry : data.Struct()) - { - if (baseSchema["properties"].Struct().count(entry.first) == 0) - { - // try generic additionalItems schema - if (schema.getType() == JsonNode::JsonType::DATA_STRUCT) - errors += propertyEntryCheck(validator, entry.second, schema, entry.first); - - // or, additionalItems field can be bool which indicates if such items are allowed - else if(!schema.isNull() && !schema.Bool()) // present and set to false - error - errors += validator.makeErrorMessage("Unknown entry found: " + entry.first); - } - } - return errors; - } - } - - namespace Formats - { - bool testFilePresence(const std::string & scope, const ResourcePath & resource) - { - std::set allowedScopes; - if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies - { - //NOTE: recursive dependencies are not allowed at the moment - update code if this changes - bool found = true; - allowedScopes = VLC->modh->getModDependencies(scope, found); - - if(!found) - return false; - - allowedScopes.insert(ModScope::scopeBuiltin()); // all mods can use H3 files - } - allowedScopes.insert(scope); // mods can use their own files - - for(const auto & entry : allowedScopes) - { - if (CResourceHandler::get(entry)->existsResource(resource)) - return true; - } - return false; - } - - #define TEST_FILE(scope, prefix, file, type) \ - if (testFilePresence(scope, ResourcePath(prefix + file, type))) \ - return "" - - std::string testAnimation(const std::string & path, const std::string & scope) - { - TEST_FILE(scope, "Sprites/", path, EResType::ANIMATION); - TEST_FILE(scope, "Sprites/", path, EResType::JSON); - return "Animation file \"" + path + "\" was not found"; - } - - std::string textFile(const JsonNode & node) - { - TEST_FILE(node.meta, "", node.String(), EResType::JSON); - return "Text file \"" + node.String() + "\" was not found"; - } - - std::string musicFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Music/", node.String(), EResType::SOUND); - TEST_FILE(node.meta, "", node.String(), EResType::SOUND); - return "Music file \"" + node.String() + "\" was not found"; - } - - std::string soundFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Sounds/", node.String(), EResType::SOUND); - return "Sound file \"" + node.String() + "\" was not found"; - } - - std::string defFile(const JsonNode & node) - { - return testAnimation(node.String(), node.meta); - } - - std::string animationFile(const JsonNode & node) - { - return testAnimation(node.String(), node.meta); - } - - std::string imageFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Data/", node.String(), EResType::IMAGE); - TEST_FILE(node.meta, "Sprites/", node.String(), EResType::IMAGE); - if (node.String().find(':') != std::string::npos) - return testAnimation(node.String().substr(0, node.String().find(':')), node.meta); - return "Image file \"" + node.String() + "\" was not found"; - } - - std::string videoFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Video/", node.String(), EResType::VIDEO); - return "Video file \"" + node.String() + "\" was not found"; - } - - #undef TEST_FILE - } - - Validation::TValidatorMap createCommonFields() - { - Validation::TValidatorMap ret; - - ret["format"] = Common::formatCheck; - ret["allOf"] = Common::allOfCheck; - ret["anyOf"] = Common::anyOfCheck; - ret["oneOf"] = Common::oneOfCheck; - ret["enum"] = Common::enumCheck; - ret["type"] = Common::typeCheck; - ret["not"] = Common::notCheck; - ret["$ref"] = Common::refCheck; - - // fields that don't need implementation - ret["title"] = Common::emptyCheck; - ret["$schema"] = Common::emptyCheck; - ret["default"] = Common::emptyCheck; - ret["description"] = Common::emptyCheck; - ret["definitions"] = Common::emptyCheck; - return ret; - } - - Validation::TValidatorMap createStringFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["maxLength"] = String::maxLengthCheck; - ret["minLength"] = String::minLengthCheck; - - ret["pattern"] = Common::notImplementedCheck; - return ret; - } - - Validation::TValidatorMap createNumberFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["maximum"] = Number::maximumCheck; - ret["minimum"] = Number::minimumCheck; - ret["multipleOf"] = Number::multipleOfCheck; - - ret["exclusiveMaximum"] = Common::emptyCheck; - ret["exclusiveMinimum"] = Common::emptyCheck; - return ret; - } - - Validation::TValidatorMap createVectorFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["items"] = Vector::itemsCheck; - ret["minItems"] = Vector::minItemsCheck; - ret["maxItems"] = Vector::maxItemsCheck; - ret["uniqueItems"] = Vector::uniqueItemsCheck; - ret["additionalItems"] = Vector::additionalItemsCheck; - return ret; - } - - Validation::TValidatorMap createStructFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["additionalProperties"] = Struct::additionalPropertiesCheck; - ret["uniqueProperties"] = Struct::uniquePropertiesCheck; - ret["maxProperties"] = Struct::maxPropertiesCheck; - ret["minProperties"] = Struct::minPropertiesCheck; - ret["dependencies"] = Struct::dependenciesCheck; - ret["properties"] = Struct::propertiesCheck; - ret["required"] = Struct::requiredCheck; - - ret["patternProperties"] = Common::notImplementedCheck; - return ret; - } - - Validation::TFormatMap createFormatMap() - { - Validation::TFormatMap ret; - ret["textFile"] = Formats::textFile; - ret["musicFile"] = Formats::musicFile; - ret["soundFile"] = Formats::soundFile; - ret["defFile"] = Formats::defFile; - ret["animationFile"] = Formats::animationFile; - ret["imageFile"] = Formats::imageFile; - ret["videoFile"] = Formats::videoFile; - - return ret; - } -} - -namespace Validation -{ - std::string ValidationData::makeErrorMessage(const std::string &message) - { - std::string errors; - errors += "At "; - if (!currentPath.empty()) - { - for(const JsonNode &path : currentPath) - { - errors += "/"; - if (path.getType() == JsonNode::JsonType::DATA_STRING) - errors += path.String(); - else - errors += std::to_string(static_cast(path.Float())); - } - } - else - errors += ""; - errors += "\n\t Error: " + message + "\n"; - return errors; - } - - std::string check(const std::string & schemaName, const JsonNode & data) - { - ValidationData validator; - return check(schemaName, data, validator); - } - - std::string check(const std::string & schemaName, const JsonNode & data, ValidationData & validator) - { - validator.usedSchemas.push_back(schemaName); - auto onscopeExit = vstd::makeScopeGuard([&]() - { - validator.usedSchemas.pop_back(); - }); - return check(JsonUtils::getSchema(schemaName), data, validator); - } - - std::string check(const JsonNode & schema, const JsonNode & data, ValidationData & validator) - { - const TValidatorMap & knownFields = getKnownFieldsFor(data.getType()); - std::string errors; - for(const auto & entry : schema.Struct()) - { - auto checker = knownFields.find(entry.first); - if (checker != knownFields.end()) - errors += checker->second(validator, schema, entry.second, data); - //else - // errors += validator.makeErrorMessage("Unknown entry in schema " + entry.first); - } - return errors; - } - - const TValidatorMap & getKnownFieldsFor(JsonNode::JsonType type) - { - static const TValidatorMap commonFields = createCommonFields(); - static const TValidatorMap numberFields = createNumberFields(); - static const TValidatorMap stringFields = createStringFields(); - static const TValidatorMap vectorFields = createVectorFields(); - static const TValidatorMap structFields = createStructFields(); - - switch (type) - { - case JsonNode::JsonType::DATA_FLOAT: - case JsonNode::JsonType::DATA_INTEGER: - return numberFields; - case JsonNode::JsonType::DATA_STRING: return stringFields; - case JsonNode::JsonType::DATA_VECTOR: return vectorFields; - case JsonNode::JsonType::DATA_STRUCT: return structFields; - default: return commonFields; - } - } - - const TFormatMap & getKnownFormats() - { - static TFormatMap knownFormats = createFormatMap(); - return knownFormats; - } - -} // Validation namespace - -VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonDetail.h b/lib/JsonDetail.h deleted file mode 100644 index b817b8961..000000000 --- a/lib/JsonDetail.h +++ /dev/null @@ -1,133 +0,0 @@ -/* - * JsonDetail.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "JsonNode.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonWriter -{ - //prefix for each line (tabulation) - std::string prefix; - std::ostream & out; - //sets whether compact nodes are written in single-line format - bool compact; - //tracks whether we are currently using single-line format - bool compactMode = false; -public: - template - void writeContainer(Iterator begin, Iterator end); - void writeEntry(JsonMap::const_iterator entry); - void writeEntry(JsonVector::const_iterator entry); - void writeString(const std::string & string); - void writeNode(const JsonNode & node); - JsonWriter(std::ostream & output, bool compact = false); -}; - -//Tiny string class that uses const char* as data for speed, members are private -//for ease of debugging and some compatibility with std::string -class constString -{ - const char *data; - const size_t datasize; - -public: - constString(const char * inputString, size_t stringSize): - data(inputString), - datasize(stringSize) - { - } - - inline size_t size() const - { - return datasize; - }; - - inline const char& operator[] (size_t position) - { - assert (position < datasize); - return data[position]; - } -}; - -//Internal class for string -> JsonNode conversion -class DLL_LINKAGE JsonParser -{ - std::string errors; // Contains description of all encountered errors - constString input; // Input data - ui32 lineCount; // Currently parsed line, starting from 1 - size_t lineStart; // Position of current line start - size_t pos; // Current position of parser - - //Helpers - bool extractEscaping(std::string &str); - bool extractLiteral(const std::string &literal); - bool extractString(std::string &string); - bool extractWhitespace(bool verbose = true); - bool extractSeparator(); - bool extractElement(JsonNode &node, char terminator); - - //Methods for extracting JSON data - bool extractArray(JsonNode &node); - bool extractFalse(JsonNode &node); - bool extractFloat(JsonNode &node); - bool extractNull(JsonNode &node); - bool extractString(JsonNode &node); - bool extractStruct(JsonNode &node); - bool extractTrue(JsonNode &node); - bool extractValue(JsonNode &node); - - //Add error\warning message to list - bool error(const std::string &message, bool warning=false); - -public: - JsonParser(const char * inputString, size_t stringSize); - - /// do actual parsing. filename is name of file that will printed to console if any errors were found - JsonNode parse(const std::string & fileName); - - /// returns true if parsing was successful - bool isValid(); -}; - -//Internal class for Json validation. Mostly compilant with json-schema v4 draft -namespace Validation -{ - /// struct used to pass data around during validation - struct ValidationData - { - /// path from root node to current one. - /// JsonNode is used as variant - either string (name of node) or as float (index in list) - std::vector currentPath; - - /// Stack of used schemas. Last schema is the one used currently. - /// May contain multiple items in case if remote references were found - std::vector usedSchemas; - - /// generates error message - std::string makeErrorMessage(const std::string &message); - }; - - using TFormatValidator = std::function; - using TFormatMap = std::unordered_map; - using TFieldValidator = std::function; - using TValidatorMap = std::unordered_map; - - /// map of known fields in schema - const TValidatorMap & getKnownFieldsFor(JsonNode::JsonType type); - const TFormatMap & getKnownFormats(); - - std::string check(const std::string & schemaName, const JsonNode & data); - std::string check(const std::string & schemaName, const JsonNode & data, ValidationData & validator); - std::string check(const JsonNode & schema, const JsonNode & data, ValidationData & validator); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonNode.h b/lib/JsonNode.h deleted file mode 100644 index 91eddce76..000000000 --- a/lib/JsonNode.h +++ /dev/null @@ -1,323 +0,0 @@ -/* - * JsonNode.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once -#include "GameConstants.h" -#include "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonNode; -using JsonMap = std::map; -using JsonVector = std::vector; - -struct Bonus; -class CSelector; -class CAddInfo; -class ILimiter; - -class DLL_LINKAGE JsonNode -{ -public: - enum class JsonType - { - DATA_NULL, - DATA_BOOL, - DATA_FLOAT, - DATA_STRING, - DATA_VECTOR, - DATA_STRUCT, - DATA_INTEGER - }; - -private: - using JsonData = std::variant; - - JsonData data; - -public: - /// free to use metadata fields - std::string meta; - // meta-flags like override - std::vector flags; - - //Create empty node - JsonNode(JsonType Type = JsonType::DATA_NULL); - //Create tree from Json-formatted input - explicit JsonNode(const char * data, size_t datasize); - //Create tree from JSON file - explicit JsonNode(const JsonPath & fileURI); - explicit JsonNode(const std::string & modName, const JsonPath & fileURI); - explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); - - bool operator == (const JsonNode &other) const; - bool operator != (const JsonNode &other) const; - - void setMeta(const std::string & metadata, bool recursive = true); - - /// Convert node to another type. Converting to nullptr will clear all data - void setType(JsonType Type); - JsonType getType() const; - - bool isNull() const; - bool isNumber() const; - bool isString() const; - bool isVector() const; - bool isStruct() const; - /// true if node contains not-null data that cannot be extended via merging - /// used for generating common base node from multiple nodes (e.g. bonuses) - bool containsBaseData() const; - bool isCompact() const; - /// removes all data from node and sets type to null - void clear(); - - /// returns bool or bool equivalent of string value if 'success' is true, or false otherwise - bool TryBoolFromString(bool & success) const; - - /// non-const accessors, node will change type on type mismatch - bool & Bool(); - double & Float(); - si64 & Integer(); - std::string & String(); - JsonVector & Vector(); - JsonMap & Struct(); - - /// const accessors, will cause assertion failure on type mismatch - bool Bool() const; - ///float and integer allowed - double Float() const; - ///only integer allowed - si64 Integer() const; - const std::string & String() const; - const JsonVector & Vector() const; - const JsonMap & Struct() const; - - /// returns resolved "json pointer" (string in format "/path/to/node") - const JsonNode & resolvePointer(const std::string & jsonPointer) const; - JsonNode & resolvePointer(const std::string & jsonPointer); - - /// convert json tree into specified type. Json tree must have same type as Type - /// Valid types: bool, string, any numeric, map and vector - /// example: convertTo< std::map< std::vector > >(); - template - Type convertTo() const; - - //operator [], for structs only - get child node by name - JsonNode & operator[](const std::string & child); - const JsonNode & operator[](const std::string & child) const; - - JsonNode & operator[](size_t child); - const JsonNode & operator[](size_t child) const; - - std::string toJson(bool compact = false) const; - - template void serialize(Handler &h, const int version) - { - h & meta; - h & flags; - h & data; - } -}; - -namespace JsonUtils -{ - DLL_LINKAGE std::shared_ptr parseBonus(const JsonVector & ability_vec); - DLL_LINKAGE std::shared_ptr parseBonus(const JsonNode & ability); - DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const FactionID & faction, const BuildingID & building, const std::string & description); - DLL_LINKAGE bool parseBonus(const JsonNode & ability, Bonus * placement); - DLL_LINKAGE std::shared_ptr parseLimiter(const JsonNode & limiter); - DLL_LINKAGE CSelector parseSelector(const JsonNode &ability); - DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node); - - /** - * @brief recursively merges source into dest, replacing identical fields - * struct : recursively calls this function - * arrays : each entry will be merged recursively - * values : value in source will replace value in dest - * null : if value in source is present but set to null it will delete entry in dest - * @note this function will destroy data in source - */ - DLL_LINKAGE void merge(JsonNode & dest, JsonNode & source, bool ignoreOverride = false, bool copyMeta = false); - - /** - * @brief recursively merges source into dest, replacing identical fields - * struct : recursively calls this function - * arrays : each entry will be merged recursively - * values : value in source will replace value in dest - * null : if value in source is present but set to null it will delete entry in dest - * @note this function will preserve data stored in source by creating copy - */ - DLL_LINKAGE void mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride = false, bool copyMeta = false); - - /** @brief recursively merges descendant into copy of base node - * Result emulates inheritance semantic - */ - DLL_LINKAGE void inherit(JsonNode & descendant, const JsonNode & base); - - /** - * @brief construct node representing the common structure of input nodes - * @param pruneEmpty - omit common properties whose intersection is empty - * different types: null - * struct: recursive intersect on common properties - * other: input if equal, null otherwise - */ - DLL_LINKAGE JsonNode intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty = true); - DLL_LINKAGE JsonNode intersect(const std::vector & nodes, bool pruneEmpty = true); - - /** - * @brief construct node representing the difference "node - base" - * merging difference with base gives node - */ - DLL_LINKAGE JsonNode difference(const JsonNode & node, const JsonNode & base); - - /** - * @brief generate one Json structure from multiple files - * @param files - list of filenames with parts of json structure - */ - DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files); - DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, bool & isValid); - - /// This version loads all files with same name (overridden by mods) - DLL_LINKAGE JsonNode assembleFromFiles(const std::string & filename); - - /** - * @brief removes all nodes that are identical to default entry in schema - * @param node - JsonNode to minimize - * @param schemaName - name of schema to use - * @note for minimizing data must be valid against given schema - */ - DLL_LINKAGE void minimize(JsonNode & node, const std::string & schemaName); - /// opposed to minimize, adds all missing, required entries that have default value - DLL_LINKAGE void maximize(JsonNode & node, const std::string & schemaName); - - /** - * @brief validate node against specified schema - * @param node - JsonNode to check - * @param schemaName - name of schema to use - * @param dataName - some way to identify data (printed in console in case of errors) - * @returns true if data in node fully compilant with schema - */ - DLL_LINKAGE bool validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName); - - /// get schema by json URI: vcmi:# - /// example: schema "vcmi:settings" is used to check user settings - DLL_LINKAGE const JsonNode & getSchema(const std::string & URI); - - /// for easy construction of JsonNodes; helps with inserting primitives into vector node - DLL_LINKAGE JsonNode boolNode(bool value); - DLL_LINKAGE JsonNode floatNode(double value); - DLL_LINKAGE JsonNode stringNode(const std::string & value); - DLL_LINKAGE JsonNode intNode(si64 value); -} - -namespace JsonDetail -{ - // conversion helpers for JsonNode::convertTo (partial template function instantiation is illegal in c++) - - template - struct JsonConvImpl; - - template - struct JsonConvImpl - { - static T convertImpl(const JsonNode & node) - { - return T((int)node.Float()); - } - }; - - template - struct JsonConvImpl - { - static T convertImpl(const JsonNode & node) - { - return T(node.Float()); - } - }; - - template - struct JsonConverter - { - static Type convert(const JsonNode & node) - { - ///this should be triggered only for numeric types and enums - static_assert(boost::mpl::or_, std::is_enum, boost::is_class >::value, "Unsupported type for JsonNode::convertTo()!"); - return JsonConvImpl, boost::is_class >::value >::convertImpl(node); - - } - }; - - template - struct JsonConverter > - { - static std::map convert(const JsonNode & node) - { - std::map ret; - for (const JsonMap::value_type & entry : node.Struct()) - { - ret.insert(entry.first, entry.second.convertTo()); - } - return ret; - } - }; - - template - struct JsonConverter > - { - static std::set convert(const JsonNode & node) - { - std::set ret; - for(const JsonVector::value_type & entry : node.Vector()) - { - ret.insert(entry.convertTo()); - } - return ret; - } - }; - - template - struct JsonConverter > - { - static std::vector convert(const JsonNode & node) - { - std::vector ret; - for (const JsonVector::value_type & entry: node.Vector()) - { - ret.push_back(entry.convertTo()); - } - return ret; - } - }; - - template<> - struct JsonConverter - { - static std::string convert(const JsonNode & node) - { - return node.String(); - } - }; - - template<> - struct JsonConverter - { - static bool convert(const JsonNode & node) - { - return node.Bool(); - } - }; -} - -template -Type JsonNode::convertTo() const -{ - return JsonDetail::JsonConverter::convert(*this); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h deleted file mode 100644 index 804c87e3e..000000000 --- a/lib/JsonRandom.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * JsonRandom.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "GameConstants.h" -#include "ResourceSet.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonNode; -using JsonVector = std::vector; -class CRandomGenerator; - -struct Bonus; -struct Component; -class CStackBasicDescriptor; - -namespace JsonRandom -{ - using Variables = std::map; - - struct DLL_LINKAGE RandomStackInfo - { - std::vector allowedCreatures; - si32 minAmount; - si32 maxAmount; - }; - - DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue = 0); - - DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - - DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - - DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - - DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value, const Variables & variables); - - DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); - - DLL_LINKAGE std::vector loadBonuses(const JsonNode & value); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/Languages.h b/lib/Languages.h index cfd47021d..c2fd3cf67 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -12,6 +12,17 @@ namespace Languages { +enum class EPluralForms +{ + NONE, + VI_1, // Single plural form, (Vietnamese) + EN_2, // Two forms, singular used for one only (English) + FR_2, // Two forms, singular used for zero and one (French) + UK_3, // Three forms, special cases for numbers ending in 1 and 2, 3, 4, except those ending in 1[1-4] (Ukrainian) + CZ_3, // Three forms, special cases for 1 and 2, 3, 4 (Czech) + PL_3, // Three forms, special case for one and some numbers ending in 2, 3, or 4 (Polish) +}; + enum class ELanguages { CZECH, @@ -57,6 +68,12 @@ struct Options /// primary IETF language tag std::string tagIETF; + /// DateTime format + std::string dateTimeFormat; + + /// Ruleset for plural forms in this language + EPluralForms pluralForms = EPluralForms::NONE; + /// VCMI supports translations into this language bool hasTranslation = false; }; @@ -65,27 +82,27 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", true }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", true }, - { "finnish", "Finnish", "Suomi", "CP1252", "fi", true }, - { "french", "French", "Français", "CP1252", "fr", true }, - { "german", "German", "Deutsch", "CP1252", "de", true }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", true }, - { "italian", "Italian", "Italiano", "CP1250", "it", true }, - { "korean", "Korean", "한국어", "CP949", "ko", true }, - { "polish", "Polish", "Polski", "CP1250", "pl", true }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", true }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", true }, - { "spanish", "Spanish", "Español", "CP1252", "es", true }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", "%d.%m.%Y %T", EPluralForms::CZ_3, true }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", "%F %T", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", "%F %T", EPluralForms::EN_2, true }, // English uses international date/time format here + { "finnish", "Finnish", "Suomi", "CP1252", "fi", "%d.%m.%Y %T", EPluralForms::EN_2, true }, + { "french", "French", "Français", "CP1252", "fr", "%d/%m/%Y %T", EPluralForms::FR_2, true }, + { "german", "German", "Deutsch", "CP1252", "de", "%d.%m.%Y %T", EPluralForms::EN_2, true }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "%Y. %m. %d. %T", EPluralForms::EN_2, true }, + { "italian", "Italian", "Italiano", "CP1250", "it", "%d/%m/%Y %T", EPluralForms::EN_2, true }, + { "korean", "Korean", "한국어", "CP949", "ko", "%F %T", EPluralForms::VI_1, true }, + { "polish", "Polish", "Polski", "CP1250", "pl", "%d.%m.%Y %T", EPluralForms::PL_3, true }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", "%d/%m/%Y %T", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", "%d.%m.%Y %T", EPluralForms::UK_3, true }, + { "spanish", "Spanish", "Español", "CP1252", "es", "%d/%m/%Y %T", EPluralForms::EN_2, true }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", "%F %T", EPluralForms::EN_2, true }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "%d.%m.%Y %T", EPluralForms::EN_2, true }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "%d.%m.%Y %T", EPluralForms::UK_3, true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "%d/%m/%Y %T", EPluralForms::VI_1, true }, // Fan translation uses special encoding - { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, - { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, - { "other_cp1252", "Other (West European)", "", "CP1252", "", false } + { "other_cp1250", "Other (East European)", "", "CP1250", "", "", EPluralForms::NONE, false }, + { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", EPluralForms::NONE, false }, + { "other_cp1252", "Other (West European)", "", "CP1252", "", "", EPluralForms::NONE, false } } }; static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); @@ -109,4 +126,50 @@ inline const Options & getLanguageOptions(const std::string & language) return emptyValue; } +template +inline constexpr int getPluralFormIndex(EPluralForms form, Numeric value) +{ + // Based on https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html + switch(form) + { + case EPluralForms::NONE: + case EPluralForms::VI_1: + return 0; + case EPluralForms::EN_2: + if (value == 1) + return 1; + return 2; + case EPluralForms::FR_2: + if (value == 1 || value == 0) + return 1; + return 2; + case EPluralForms::UK_3: + if (value % 10 == 1 && value % 100 != 11) + return 1; + if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20)) + return 2; + return 0; + case EPluralForms::CZ_3: + if (value == 1) + return 1; + if (value>=2 && value<=4) + return 2; + return 0; + case EPluralForms::PL_3: + if (value == 1) + return 1; + if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20)) + return 2; + return 0; + } + throw std::runtime_error("Invalid plural form enumeration received!"); +} + +template +inline std::string getPluralFormTextID(std::string languageName, Numeric value, std::string textID) +{ + int formIndex = getPluralFormIndex(getLanguageOptions(languageName).pluralForms, value); + return textID + '.' + std::to_string(formIndex); +} + } diff --git a/lib/LoadProgress.cpp b/lib/LoadProgress.cpp index 70eadbdbb..0c6b6821e 100644 --- a/lib/LoadProgress.cpp +++ b/lib/LoadProgress.cpp @@ -13,14 +13,16 @@ using namespace Load; -Progress::Progress(): _progress(std::numeric_limits::min()) -{ - setupSteps(100); -} +Progress::Progress() + : Progress(100) +{} -Progress::Progress(int steps): _progress(std::numeric_limits::min()) +Progress::Progress(int steps) + : _progress(std::numeric_limits::min()) + , _target(std::numeric_limits::max()) + , _step(std::numeric_limits::min()) + , _maxSteps(steps) { - setupSteps(steps); } Type Progress::get() const diff --git a/lib/LoadProgress.h b/lib/LoadProgress.h index 1c3c21b0d..34e673ed9 100644 --- a/lib/LoadProgress.h +++ b/lib/LoadProgress.h @@ -68,8 +68,10 @@ public: void step(int count = 1); private: - std::atomic _progress, _target; - std::atomic _step, _maxSteps; + std::atomic _progress; + std::atomic _target; + std::atomic _step; + std::atomic _maxSteps; friend class ProgressAccumulator; }; @@ -87,7 +89,8 @@ public: private: mutable boost::mutex _mx; - long long _accumulated = 0, _steps = 0; + long long _accumulated = 0; + long long _steps = 0; std::vector> _progress; }; diff --git a/lib/LogicalExpression.h b/lib/LogicalExpression.h index 6bc3f36fa..673662cef 100644 --- a/lib/LogicalExpression.h +++ b/lib/LogicalExpression.h @@ -9,8 +9,7 @@ */ #pragma once -//FIXME: move some of code into .cpp to avoid this include? -#include "JsonNode.h" +#include "json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -57,7 +56,7 @@ namespace LogicalExpressionDetail } template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & expressions; } @@ -614,7 +613,7 @@ public: } template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & data; } diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 93665a0e9..8ee3806a7 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -51,8 +51,11 @@ void MetaString::appendRawString(const std::string & value) void MetaString::appendTextID(const std::string & value) { - message.push_back(EMessage::APPEND_TEXTID_STRING); - stringsTextID.push_back(value); + if (!value.empty()) + { + message.push_back(EMessage::APPEND_TEXTID_STRING); + stringsTextID.push_back(value); + } } void MetaString::appendNumber(int64_t value) @@ -112,74 +115,12 @@ std::string MetaString::getLocalString(const std::pair & txt) c switch(type) { - case EMetaText::ART_NAMES: - { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); - if(art) - return art->getNameTranslated(); - return "#!#"; - } - case EMetaText::ART_DESCR: - { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); - if(art) - return art->getDescriptionTranslated(); - return "#!#"; - } - case EMetaText::ART_EVNTS: - { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); - if(art) - return art->getEventTranslated(); - return "#!#"; - } - case EMetaText::CRE_PL_NAMES: - { - const auto * cre = CreatureID(ser).toCreature(VLC->creatures()); - if(cre) - return cre->getNamePluralTranslated(); - return "#!#"; - } - case EMetaText::CRE_SING_NAMES: - { - const auto * cre = CreatureID(ser).toCreature(VLC->creatures()); - if(cre) - return cre->getNameSingularTranslated(); - return "#!#"; - } - case EMetaText::MINE_NAMES: - { - return VLC->generaltexth->translate("core.minename", ser); - } - case EMetaText::MINE_EVNTS: - { - return VLC->generaltexth->translate("core.mineevnt", ser); - } - case EMetaText::SPELL_NAME: - { - const auto * spell = SpellID(ser).toSpell(VLC->spells()); - if(spell) - return spell->getNameTranslated(); - return "#!#"; - } - case EMetaText::OBJ_NAMES: - return VLC->objtypeh->getObjectName(ser, 0); - case EMetaText::SEC_SKILL_NAME: - return VLC->skillh->getByIndex(ser)->getNameTranslated(); case EMetaText::GENERAL_TXT: return VLC->generaltexth->translate("core.genrltxt", ser); - case EMetaText::RES_NAMES: - return VLC->generaltexth->translate("core.restypes", ser); case EMetaText::ARRAY_TXT: return VLC->generaltexth->translate("core.arraytxt", ser); - case EMetaText::CREGENS: - return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR1, ser); - case EMetaText::CREGENS4: - return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR4, ser); case EMetaText::ADVOB_TXT: return VLC->generaltexth->translate("core.advevent", ser); - case EMetaText::COLOR: - return VLC->generaltexth->translate("vcmi.capitalColors", ser); case EMetaText::JK_TXT: return VLC->generaltexth->translate("core.jktext", ser); default: @@ -227,7 +168,10 @@ DLL_LINKAGE std::string MetaString::toString() const boost::replace_first(dst, "%d", std::to_string(numbers[nums++])); break; case EMessage::REPLACE_POSITIVE_NUMBER: - boost::replace_first(dst, "%+d", '+' + std::to_string(numbers[nums++])); + if (dst.find("%+d") != std::string::npos) + boost::replace_first(dst, "%+d", '+' + std::to_string(numbers[nums++])); + else + boost::replace_first(dst, "%d", std::to_string(numbers[nums++])); break; default: logGlobal->error("MetaString processing error! Received message of type %d", static_cast(elem)); @@ -287,20 +231,6 @@ DLL_LINKAGE std::string MetaString::buildList() const return lista; } -void MetaString::replaceCreatureName(const CreatureID & id, TQuantity count) //adds sing or plural name; -{ - if (count == 1) - replaceLocalString (EMetaText::CRE_SING_NAMES, id); - else - replaceLocalString (EMetaText::CRE_PL_NAMES, id); -} - -void MetaString::replaceCreatureName(const CStackBasicDescriptor & stack) -{ - assert(stack.type); //valid type - replaceCreatureName(stack.type->getId(), stack.count); -} - bool MetaString::operator == (const MetaString & other) const { return message == other.message && localStrings == other.localStrings && exactStrings == other.exactStrings && stringsTextID == other.stringsTextID && numbers == other.numbers; @@ -395,4 +325,90 @@ void MetaString::serializeJson(JsonSerializeFormat & handler) jsonDeserialize(handler.getCurrent()); } +void MetaString::appendName(const ArtifactID & id) +{ + appendTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::appendName(const SpellID & id) +{ + appendTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::appendName(const PlayerColor & id) +{ + appendTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); +} + +void MetaString::appendName(const CreatureID & id, TQuantity count) +{ + if(count == 1) + appendNameSingular(id); + else + appendNamePlural(id); +} + +void MetaString::appendNameSingular(const CreatureID & id) +{ + appendTextID(id.toEntity(VLC)->getNameSingularTextID()); +} + +void MetaString::appendNamePlural(const CreatureID & id) +{ + appendTextID(id.toEntity(VLC)->getNamePluralTextID()); +} + +void MetaString::replaceName(const ArtifactID & id) +{ + replaceTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::replaceName(const MapObjectID& id) +{ + replaceTextID(VLC->objtypeh->getObjectName(id, 0)); +} + +void MetaString::replaceName(const PlayerColor & id) +{ + replaceTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); +} + +void MetaString::replaceName(const SecondarySkill & id) +{ + replaceTextID(VLC->skillh->getById(id)->getNameTextID()); +} + +void MetaString::replaceName(const SpellID & id) +{ + replaceTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::replaceName(const GameResID& id) +{ + replaceTextID(TextIdentifier("core.restypes", id.getNum()).get()); +} + +void MetaString::replaceNameSingular(const CreatureID & id) +{ + replaceTextID(id.toEntity(VLC)->getNameSingularTextID()); +} + +void MetaString::replaceNamePlural(const CreatureID & id) +{ + replaceTextID(id.toEntity(VLC)->getNamePluralTextID()); +} + +void MetaString::replaceName(const CreatureID & id, TQuantity count) //adds sing or plural name; +{ + if(count == 1) + replaceNameSingular(id); + else + replaceNamePlural(id); +} + +void MetaString::replaceName(const CStackBasicDescriptor & stack) +{ + replaceName(stack.type->getId(), stack.count); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/MetaString.h b/lib/MetaString.h index 2c6da6961..773e33967 100644 --- a/lib/MetaString.h +++ b/lib/MetaString.h @@ -12,31 +12,24 @@ VCMI_LIB_NAMESPACE_BEGIN class JsonNode; +class ArtifactID; class CreatureID; class CStackBasicDescriptor; class JsonSerializeFormat; +class MapObjectID; +class MapObjectSubID; +class PlayerColor; +class SecondarySkill; +class SpellID; +class GameResID; using TQuantity = si32; /// Strings classes that can be used as replacement in MetaString enum class EMetaText : uint8_t { GENERAL_TXT = 1, - OBJ_NAMES, - RES_NAMES, - ART_NAMES, ARRAY_TXT, - CRE_PL_NAMES, - CREGENS, - MINE_NAMES, - MINE_EVNTS, ADVOB_TXT, - ART_EVNTS, - SPELL_NAME, - SEC_SKILL_NAME, - CRE_SING_NAMES, - CREGENS4, - COLOR, - ART_DESCR, JK_TXT }; @@ -82,6 +75,13 @@ public: /// Appends specified number to resulting string void appendNumber(int64_t value); + void appendName(const ArtifactID& id); + void appendName(const SpellID& id); + void appendName(const PlayerColor& id); + void appendName(const CreatureID & id, TQuantity count); + void appendNameSingular(const CreatureID & id); + void appendNamePlural(const CreatureID & id); + /// Replaces first '%s' placeholder in string with specified local string void replaceLocalString(EMetaText type, ui32 serial); /// Replaces first '%s' placeholder in string with specified fixed, untranslated string @@ -93,10 +93,19 @@ public: /// Replaces first '%+d' placeholder in string with specified number using '+' sign as prefix void replacePositiveNumber(int64_t txt); + void replaceName(const ArtifactID & id); + void replaceName(const MapObjectID& id); + void replaceName(const PlayerColor& id); + void replaceName(const SecondarySkill& id); + void replaceName(const SpellID& id); + void replaceName(const GameResID& id); + /// Replaces first '%s' placeholder with singular or plural name depending on creatures count - void replaceCreatureName(const CreatureID & id, TQuantity count); + void replaceName(const CreatureID & id, TQuantity count); + void replaceNameSingular(const CreatureID & id); + void replaceNamePlural(const CreatureID & id); /// Replaces first '%s' placeholder with singular or plural name depending on creatures count - void replaceCreatureName(const CStackBasicDescriptor & stack); + void replaceName(const CStackBasicDescriptor & stack); /// erases any existing content in the string void clear(); @@ -117,7 +126,7 @@ public: void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & exactStrings; h & localStrings; diff --git a/lib/ObstacleHandler.cpp b/lib/ObstacleHandler.cpp index fbfc7efe2..d7b613959 100644 --- a/lib/ObstacleHandler.cpp +++ b/lib/ObstacleHandler.cpp @@ -10,8 +10,8 @@ #include "StdInc.h" #include "ObstacleHandler.h" #include "BattleFieldHandler.h" +#include "json/JsonNode.h" #include "modding/IdentifierStorage.h" -#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -106,8 +106,6 @@ ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const Js info->isAbsoluteObstacle = json["absolute"].Bool(); info->isForegroundObstacle = json["foreground"].Bool(); - objects.emplace_back(info); - return info; } @@ -116,11 +114,6 @@ std::vector ObstacleHandler::loadLegacyData() return {}; } -std::vector ObstacleHandler::getDefaultAllowed() const -{ - return {}; -} - const std::vector & ObstacleHandler::getTypeNames() const { static const std::vector types = { "obstacle" }; diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index 958e46e62..840016542 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -40,7 +40,8 @@ public: bool isAbsoluteObstacle; //there may only one such obstacle in battle and its position is always the same bool isForegroundObstacle; - si32 width, height; //how much space to the right and up is needed to place obstacle (affects only placement algorithm) + si32 width; //how much space to the right and up is needed to place obstacle (affects only placement algorithm) + si32 height; std::vector blockedTiles; //offsets relative to obstacle position (that is its left bottom corner) int32_t getIndex() const override; @@ -54,23 +55,6 @@ public: std::vector getBlocked(BattleHex hex) const; //returns vector of hexes blocked by obstacle when it's placed on hex 'hex' bool isAppropriate(const TerrainId terrainType, const BattleField & specialBattlefield) const; - - template void serialize(Handler &h, const int version) - { - h & obstacle; - h & iconIndex; - h & identifier; - h & animation; - h & appearAnimation; - h & appearSound; - h & allowedTerrains; - h & allowedSpecialBfields; - h & isAbsoluteObstacle; - h & isForegroundObstacle; - h & width; - h & height; - h & blockedTiles; - } }; class DLL_LINKAGE ObstacleService : public EntityServiceT @@ -88,12 +72,6 @@ public: const std::vector & getTypeNames() const override; std::vector loadLegacyData() override; - std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/Point.h b/lib/Point.h index 36f7a330c..fd3bde33c 100644 --- a/lib/Point.h +++ b/lib/Point.h @@ -122,7 +122,7 @@ public: } template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & x; h & y; diff --git a/lib/Rect.h b/lib/Rect.h index 9aaa3578e..11213d2d0 100644 --- a/lib/Rect.h +++ b/lib/Rect.h @@ -163,7 +163,7 @@ public: DLL_LINKAGE Rect include(const Rect & other) const; template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & x; h & y; diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index e9fa85243..71db0db46 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -12,32 +12,20 @@ #include "GameConstants.h" #include "ResourceSet.h" #include "constants/StringConstants.h" -#include "JsonNode.h" #include "serializer/JsonSerializeFormat.h" #include "mapObjects/CObjectHandler.h" #include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN +ResourceSet::ResourceSet() = default; + ResourceSet::ResourceSet(const JsonNode & node) { for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) container[i] = static_cast(node[GameConstants::RESOURCE_NAMES[i]].Float()); } -ResourceSet::ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal, - TResource gems, TResource gold, TResource mithril) -{ - container[GameResID(EGameResID::WOOD)] = wood; - container[GameResID(EGameResID::MERCURY)] = mercury; - container[GameResID(EGameResID::ORE)] = ore; - container[GameResID(EGameResID::SULFUR)] = sulfur; - container[GameResID(EGameResID::CRYSTAL)] = crystal; - container[GameResID(EGameResID::GEMS)] = gems; - container[GameResID(EGameResID::GOLD)] = gold; - container[GameResID(EGameResID::MITHRIL)] = mithril; -} - void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) { if(handler.saving && !nonZero()) diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index 676ee3730..4d9a0e695 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -26,12 +26,11 @@ class ResourceSet; class ResourceSet { private: - std::array container; + std::array container = {}; public: // read resources set from json. Format example: { "gold": 500, "wood":5 } DLL_LINKAGE ResourceSet(const JsonNode & node); - DLL_LINKAGE ResourceSet(TResource wood = 0, TResource mercury = 0, TResource ore = 0, TResource sulfur = 0, TResource crystal = 0, - TResource gems = 0, TResource gold = 0, TResource mithril = 0); + DLL_LINKAGE ResourceSet(); #define scalarOperator(OPSIGN) \ @@ -181,7 +180,7 @@ public: // return true; // } - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & container; } diff --git a/lib/RiverHandler.cpp b/lib/RiverHandler.cpp index 3cc04eb37..7ed62a7a5 100644 --- a/lib/RiverHandler.cpp +++ b/lib/RiverHandler.cpp @@ -12,13 +12,13 @@ #include "RiverHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" -#include "JsonNode.h" +#include "json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN RiverTypeHandler::RiverTypeHandler() { - objects.push_back(new RiverType); + objects.push_back(new RiverType()); VLC->generaltexth->registerString("core", objects[0]->getNameTextID(), ""); } @@ -68,11 +68,6 @@ std::vector RiverTypeHandler::loadLegacyData() return {}; } -std::vector RiverTypeHandler::getDefaultAllowed() const -{ - return {}; -} - std::string RiverType::getJsonKey() const { return modScope + ":" + identifier; diff --git a/lib/RiverHandler.h b/lib/RiverHandler.h index dddd81d41..c13736e76 100644 --- a/lib/RiverHandler.h +++ b/lib/RiverHandler.h @@ -24,12 +24,6 @@ struct DLL_LINKAGE RiverPaletteAnimation int32_t start; /// total numbers of colors to cycle int32_t length; - - template void serialize(Handler& h, const int version) - { - h & start; - h & length; - } }; class DLL_LINKAGE RiverType : public EntityT @@ -57,16 +51,6 @@ public: std::vector paletteAnimation; RiverType(); - - template void serialize(Handler& h, const int version) - { - h & tilesFilename; - h & identifier; - h & modScope; - h & deltaName; - h & id; - h & paletteAnimation; - } }; class DLL_LINKAGE RiverTypeService : public EntityServiceT @@ -85,14 +69,8 @@ public: RiverTypeHandler(); - virtual const std::vector & getTypeNames() const override; - virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } + const std::vector & getTypeNames() const override; + std::vector loadLegacyData() override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/RoadHandler.cpp b/lib/RoadHandler.cpp index 458d1588d..5ebbe54f1 100644 --- a/lib/RoadHandler.cpp +++ b/lib/RoadHandler.cpp @@ -12,13 +12,13 @@ #include "RoadHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" -#include "JsonNode.h" +#include "json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN RoadTypeHandler::RoadTypeHandler() { - objects.push_back(new RoadType); + objects.push_back(new RoadType()); VLC->generaltexth->registerString("core", objects[0]->getNameTextID(), ""); } @@ -59,11 +59,6 @@ std::vector RoadTypeHandler::loadLegacyData() return {}; } -std::vector RoadTypeHandler::getDefaultAllowed() const -{ - return {}; -} - std::string RoadType::getJsonKey() const { return modScope + ":" + identifier; diff --git a/lib/RoadHandler.h b/lib/RoadHandler.h index e9e1d9df6..5f31d1858 100644 --- a/lib/RoadHandler.h +++ b/lib/RoadHandler.h @@ -41,15 +41,6 @@ public: ui8 movementCost; RoadType(); - - template void serialize(Handler& h, const int version) - { - h & tilesFilename; - h & identifier; - h & modScope; - h & id; - h & movementCost; - } }; class DLL_LINKAGE RoadTypeService : public EntityServiceT @@ -68,14 +59,8 @@ public: RoadTypeHandler(); - virtual const std::vector & getTypeNames() const override; - virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } + const std::vector & getTypeNames() const override; + std::vector loadLegacyData() override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/ScopeGuard.h b/lib/ScopeGuard.h index bd8e0f6b5..de9b06a95 100644 --- a/lib/ScopeGuard.h +++ b/lib/ScopeGuard.h @@ -33,7 +33,7 @@ namespace vstd explicit ScopeGuard(Func && f): fire(true), - f(std::forward(f)) + f(std::move(f)) {} ~ScopeGuard() { diff --git a/lib/ScriptHandler.cpp b/lib/ScriptHandler.cpp index d672c74dd..e043879a6 100644 --- a/lib/ScriptHandler.cpp +++ b/lib/ScriptHandler.cpp @@ -217,11 +217,6 @@ const Script * ScriptHandler::resolveScript(const std::string & name) const } } -std::vector ScriptHandler::getDefaultAllowed() const -{ - return std::vector(); -} - std::vector ScriptHandler::loadLegacyData() { return std::vector(); @@ -230,7 +225,7 @@ std::vector ScriptHandler::loadLegacyData() ScriptPtr ScriptHandler::loadFromJson(vstd::CLoggerBase * logger, const std::string & scope, const JsonNode & json, const std::string & identifier) const { - ScriptPtr ret = std::make_shared(this); + auto ret = std::make_shared(this); JsonDeserializer handler(nullptr, json); ret->identifier = identifier; @@ -284,7 +279,7 @@ void ScriptHandler::loadState(const JsonNode & state) const JsonNode & scriptData = keyValue.second; - ScriptPtr script = std::make_shared(this); + auto script = std::make_shared(this); JsonDeserializer handler(nullptr, scriptData); script->serializeJsonState(handler); diff --git a/lib/ScriptHandler.h b/lib/ScriptHandler.h index e2ff3a7e4..57f84b0f8 100644 --- a/lib/ScriptHandler.h +++ b/lib/ScriptHandler.h @@ -13,7 +13,7 @@ #if SCRIPTING_ENABLED #include #include "IHandlerBase.h" -#include "JsonNode.h" +#include "json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -97,7 +97,6 @@ public: const Script * resolveScript(const std::string & name) const; - std::vector getDefaultAllowed() const override; std::vector loadLegacyData() override; ScriptPtr loadFromJson(vstd::CLoggerBase * logger, const std::string & scope, const JsonNode & json, const std::string & identifier) const; @@ -109,7 +108,7 @@ public: void run(std::shared_ptr pool) const override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { JsonNode state; if(h.saving) diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index b9e9cfb4b..73b896716 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -32,7 +32,7 @@ FactionID PlayerSettings::getCastleValidated() const { if (!castle.isValid()) return FactionID(0); - if (castle.getNum() < VLC->townh->size()) + if (castle.getNum() < VLC->townh->size() && castle.toEntity(VLC)->hasTown()) return castle; return FactionID(0); @@ -111,7 +111,7 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const if(i == si->playerInfos.cend() && !ignoreNoHuman) throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.530")); - if(si->mapGenOptions && si->mode == StartInfo::NEW_GAME) + if(si->mapGenOptions && si->mode == EStartMode::NEW_GAME) { if(!si->mapGenOptions->checkOptions()) throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.751")); @@ -123,7 +123,12 @@ bool LobbyInfo::isClientHost(int clientId) const return clientId == hostClientId; } -std::set LobbyInfo::getAllClientPlayers(int clientId) +bool LobbyInfo::isPlayerHost(const PlayerColor & color) const +{ + return vstd::contains(getAllClientPlayers(hostClientId), color); +} + +std::set LobbyInfo::getAllClientPlayers(int clientId) const { std::set players; for(auto & elem : si->playerInfos) diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 925df407c..4067124a8 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -13,6 +13,7 @@ #include "GameConstants.h" #include "TurnTimerInfo.h" +#include "ExtraOptionsInfo.h" #include "campaign/CampaignConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -30,10 +31,17 @@ struct DLL_LINKAGE SimturnsInfo /// Maximum number of turns that might be played simultaneously unless contact is detected int optionalTurns = 0; /// If set to true, human and 1 AI can act at the same time - bool allowHumanWithAI = true; + bool allowHumanWithAI = false; + + bool operator == (const SimturnsInfo & other) const + { + return requiredTurns == other.requiredTurns && + optionalTurns == other.optionalTurns && + allowHumanWithAI == other.allowHumanWithAI; + } template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & requiredTurns; h & optionalTurns; @@ -68,7 +76,7 @@ struct DLL_LINKAGE PlayerSettings std::set connectedPlayerIDs; //Empty - AI, or connectrd player ids bool compOnly; //true if this player is a computer only player; required for RMG template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & castle; h & hero; @@ -90,12 +98,18 @@ struct DLL_LINKAGE PlayerSettings HeroTypeID getHeroValidated() const; }; +enum class EStartMode : int32_t +{ + NEW_GAME, + LOAD_GAME, + CAMPAIGN, + INVALID = 255 +}; + /// Struct which describes the difficulty, the turn time,.. of a heroes match. struct DLL_LINKAGE StartInfo { - enum EMode {NEW_GAME, LOAD_GAME, CAMPAIGN, INVALID = 255}; - - EMode mode; + EStartMode mode; ui8 difficulty; //0=easy; 4=impossible using TPlayerInfos = std::map; @@ -108,6 +122,7 @@ struct DLL_LINKAGE StartInfo std::string fileURI; SimturnsInfo simturnsInfo; TurnTimerInfo turnTimerInfo; + ExtraOptionsInfo extraOptionsInfo; std::string mapname; // empty for random map, otherwise name of the map or savegame bool createRandomMap() const { return mapGenOptions != nullptr; } std::shared_ptr mapGenOptions; @@ -122,7 +137,7 @@ struct DLL_LINKAGE StartInfo std::string getCampaignName() const; template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & mode; h & difficulty; @@ -134,12 +149,16 @@ struct DLL_LINKAGE StartInfo h & fileURI; h & simturnsInfo; h & turnTimerInfo; + if(h.version >= Handler::Version::HAS_EXTRA_OPTIONS) + h & extraOptionsInfo; + else + extraOptionsInfo = ExtraOptionsInfo(); h & mapname; h & mapGenOptions; h & campState; } - StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), + StartInfo() : mode(EStartMode::INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(nullptr))), fileURI("") { @@ -151,7 +170,7 @@ struct ClientPlayer int connection; std::string name; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & connection; h & name; @@ -171,7 +190,7 @@ struct DLL_LINKAGE LobbyState LobbyState() : si(new StartInfo()), hostClientId(-1), campaignMap(CampaignScenarioID::NONE), campaignBonus(-1) {} - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & si; h & mi; @@ -184,7 +203,6 @@ struct DLL_LINKAGE LobbyState struct DLL_LINKAGE LobbyInfo : public LobbyState { - boost::mutex stateMutex; std::string uuid; LobbyInfo() {} @@ -192,7 +210,8 @@ struct DLL_LINKAGE LobbyInfo : public LobbyState void verifyStateBeforeStart(bool ignoreNoHuman = false) const; bool isClientHost(int clientId) const; - std::set getAllClientPlayers(int clientId); + bool isPlayerHost(const PlayerColor & color) const; + std::set getAllClientPlayers(int clientId) const; std::vector getConnectedPlayerIdsForClient(int clientId) const; // Helpers for lobby state access diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index f7a3eb96f..6980d98b8 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -12,7 +12,7 @@ #include "TerrainHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" -#include "JsonNode.h" +#include "json/JsonNode.h" #include "modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -140,11 +140,6 @@ std::vector TerrainTypeHandler::loadLegacyData() return result; } -std::vector TerrainTypeHandler::getDefaultAllowed() const -{ - return {}; -} - bool TerrainType::isLand() const { return !isWater(); diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index b94e4427d..59c6ab62b 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -26,7 +26,7 @@ struct DLL_LINKAGE TerrainPaletteAnimation /// total numbers of colors to cycle int32_t length; - template void serialize(Handler& h, const int version) + template void serialize(Handler& h) { h & start; h & length; @@ -91,30 +91,6 @@ public: bool isSurface() const; bool isUnderground() const; bool isTransitionRequired() const; - - template void serialize(Handler &h, const int version) - { - h & battleFields; - h & prohibitTransitions; - h & minimapBlocked; - h & minimapUnblocked; - h & modScope; - h & identifier; - h & musicFilename; - h & tilesFilename; - h & shortIdentifier; - h & terrainViewPatterns; - h & rockTerrain; - h & river; - h & paletteAnimation; - - h & id; - h & moveCost; - h & horseSound; - h & horseSoundPenalty; - h & passabilityType; - h & transitionRequired; - } }; class DLL_LINKAGE TerrainTypeService : public EntityServiceT @@ -131,14 +107,8 @@ public: const std::string & identifier, size_t index) override; - virtual const std::vector & getTypeNames() const override; - virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } + const std::vector & getTypeNames() const override; + std::vector loadLegacyData() override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/TextOperations.cpp b/lib/TextOperations.cpp index df4771210..d234a848c 100644 --- a/lib/TextOperations.cpp +++ b/lib/TextOperations.cpp @@ -11,6 +11,10 @@ #include "TextOperations.h" #include "CGeneralTextHandler.h" +#include "Languages.h" +#include "CConfigHandler.h" + +#include #include @@ -210,4 +214,15 @@ std::string TextOperations::escapeString(std::string input) return input; } +std::string TextOperations::getFormattedDateTimeLocal(std::time_t dt) +{ + return vstd::getFormattedDateTime(dt, Languages::getLanguageOptions(settings["general"]["language"].String()).dateTimeFormat); +} + +std::string TextOperations::getFormattedTimeLocal(std::time_t dt) +{ + return vstd::getFormattedDateTime(dt, "%H:%M"); +} + + VCMI_LIB_NAMESPACE_END diff --git a/lib/TextOperations.h b/lib/TextOperations.h index 73768cef3..8c0f827f6 100644 --- a/lib/TextOperations.h +++ b/lib/TextOperations.h @@ -56,6 +56,12 @@ namespace TextOperations /// replaces all symbols that normally need escaping with appropriate escape sequences std::string escapeString(std::string input); + + /// get formatted DateTime depending on the language selected + DLL_LINKAGE std::string getFormattedDateTimeLocal(std::time_t dt); + + /// get formatted time (without date) + DLL_LINKAGE std::string getFormattedTimeLocal(std::time_t dt); }; diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp index 0594fad65..7bf0b63d5 100644 --- a/lib/TurnTimerInfo.cpp +++ b/lib/TurnTimerInfo.cpp @@ -19,7 +19,44 @@ bool TurnTimerInfo::isEnabled() const bool TurnTimerInfo::isBattleEnabled() const { - return creatureTimer > 0 || battleTimer > 0; + return turnTimer > 0 || baseTimer > 0 || unitTimer > 0 || battleTimer > 0; +} + +void TurnTimerInfo::substractTimer(int timeMs) +{ + auto const & substractTimer = [&timeMs](int & targetTimer) + { + if (targetTimer > timeMs) + { + targetTimer -= timeMs; + timeMs = 0; + } + else + { + timeMs -= targetTimer; + targetTimer = 0; + } + }; + + substractTimer(unitTimer); + substractTimer(battleTimer); + substractTimer(turnTimer); + substractTimer(baseTimer); +} + +int TurnTimerInfo::valueMs() const +{ + return baseTimer + turnTimer + battleTimer + unitTimer; +} + +bool TurnTimerInfo::operator == (const TurnTimerInfo & other) const +{ + return turnTimer == other.turnTimer && + baseTimer == other.baseTimer && + battleTimer == other.battleTimer && + unitTimer == other.unitTimer && + accumulatingTurnTimer == other.accumulatingTurnTimer && + accumulatingUnitTimer == other.accumulatingUnitTimer; } VCMI_LIB_NAMESPACE_END diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h index c708345b4..697c24831 100644 --- a/lib/TurnTimerInfo.h +++ b/lib/TurnTimerInfo.h @@ -17,21 +17,31 @@ struct DLL_LINKAGE TurnTimerInfo int turnTimer = 0; //in ms, counts down when player is making his turn on adventure map int baseTimer = 0; //in ms, counts down only when turn timer runs out int battleTimer = 0; //in ms, counts down during battles when creature timer runs out - int creatureTimer = 0; //in ms, counts down when player is choosing action in battle + int unitTimer = 0; //in ms, counts down when player is choosing action in battle + + bool accumulatingTurnTimer = false; + bool accumulatingUnitTimer = false; bool isActive = false; //is being counting down bool isBattle = false; //indicator for current timer mode bool isEnabled() const; bool isBattleEnabled() const; - + + void substractTimer(int timeMs); + int valueMs() const; + + bool operator == (const TurnTimerInfo & other) const; + template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & turnTimer; h & baseTimer; h & battleTimer; - h & creatureTimer; + h & unitTimer; + h & accumulatingTurnTimer; + h & accumulatingUnitTimer; h & isActive; h & isBattle; } diff --git a/lib/VCMIDirs.cpp b/lib/VCMIDirs.cpp index 3c0692885..83bf7ee68 100644 --- a/lib/VCMIDirs.cpp +++ b/lib/VCMIDirs.cpp @@ -368,11 +368,7 @@ bool IVCMIDirsUNIX::developmentMode() const { // We want to be able to run VCMI from single directory. E.g to run from build output directory const bool result = bfs::exists("AI") && bfs::exists("config") && bfs::exists("Mods") && bfs::exists("vcmiclient"); -#if SINGLE_PROCESS_APP return result; -#else - return result && bfs::exists("vcmiserver"); -#endif } bfs::path IVCMIDirsUNIX::clientPath() const { return binaryPath() / "vcmiclient"; } diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 3a4dea264..f1b0a3cfd 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -47,14 +47,14 @@ VCMI_LIB_NAMESPACE_BEGIN LibClasses * VLC = nullptr; -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives) +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool extractArchives) { console = Console; VLC = new LibClasses(); VLC->loadFilesystem(extractArchives); settings.init("config/settings.json", "vcmi:settings"); persistentStorage.init("config/persistentStorage.json", ""); - VLC->loadModFilesystem(onlyEssential); + VLC->loadModFilesystem(); } @@ -65,54 +65,54 @@ DLL_LINKAGE void loadDLLClasses(bool onlyEssential) const ArtifactService * LibClasses::artifacts() const { - return arth; + return arth.get(); } const CreatureService * LibClasses::creatures() const { - return creh; + return creh.get(); } const FactionService * LibClasses::factions() const { - return townh; + return townh.get(); } const HeroClassService * LibClasses::heroClasses() const { - return &heroh->classes; + return heroclassesh.get(); } const HeroTypeService * LibClasses::heroTypes() const { - return heroh; + return heroh.get(); } #if SCRIPTING_ENABLED const scripting::Service * LibClasses::scripts() const { - return scriptHandler; + return scriptHandler.get(); } #endif const spells::Service * LibClasses::spells() const { - return spellh; + return spellh.get(); } const SkillService * LibClasses::skills() const { - return skillh; + return skillh.get(); } const IBonusTypeHandler * LibClasses::getBth() const { - return bth; + return bth.get(); } const CIdentifierStorage * LibClasses::identifiers() const { - return identifiersHandler; + return identifiersHandler.get(); } const spells::effects::Registry * LibClasses::spellEffects() const @@ -127,17 +127,17 @@ spells::effects::Registry * LibClasses::spellEffects() const BattleFieldService * LibClasses::battlefields() const { - return battlefieldsHandler; + return battlefieldsHandler.get(); } const ObstacleService * LibClasses::obstacles() const { - return obstacleHandler; + return obstacleHandler.get(); } const IGameSettings * LibClasses::settings() const { - return settingsHandler; + return settingsHandler.get(); } void LibClasses::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) @@ -154,7 +154,7 @@ void LibClasses::updateEntity(Metatype metatype, int32_t index, const JsonNode & townh->updateEntity(index, data); break; case Metatype::HERO_CLASS: - heroh->classes.updateEntity(index, data); + heroclassesh->updateEntity(index, data); break; case Metatype::HERO_TYPE: heroh->updateEntity(index, data); @@ -182,12 +182,12 @@ void LibClasses::loadFilesystem(bool extractArchives) logGlobal->info("\tData loading: %d ms", loadTime.getDiff()); } -void LibClasses::loadModFilesystem(bool onlyEssential) +void LibClasses::loadModFilesystem() { CStopWatch loadTime; - modh = new CModHandler(); - identifiersHandler = new CIdentifierStorage(); - modh->loadMods(onlyEssential); + modh = std::make_unique(); + identifiersHandler = std::make_unique(); + modh->loadMods(); logGlobal->info("\tMod handler: %d ms", loadTime.getDiff()); modh->loadModFilesystems(); @@ -199,9 +199,9 @@ static void logHandlerLoaded(const std::string & name, CStopWatch & timer) logGlobal->info("\t\t %s handler: %d ms", name, timer.getDiff()); } -template void createHandler(Handler *&handler, const std::string &name, CStopWatch &timer) +template void createHandler(std::shared_ptr & handler, const std::string &name, CStopWatch &timer) { - handler = new Handler(); + handler = std::make_shared(); logHandlerLoaded(name, timer); } @@ -219,6 +219,7 @@ void LibClasses::init(bool onlyEssential) createHandler(riverTypeHandler, "River", pomtime); createHandler(terrainTypeHandler, "Terrain", pomtime); createHandler(heroh, "Hero", pomtime); + createHandler(heroclassesh, "Hero classes", pomtime); createHandler(arth, "Artifact", pomtime); createHandler(creh, "Creature", pomtime); createHandler(townh, "Town", pomtime); @@ -239,67 +240,6 @@ void LibClasses::init(bool onlyEssential) modh->afterLoad(onlyEssential); } -void LibClasses::clear() -{ - delete heroh; - delete arth; - delete creh; - delete townh; - delete objh; - delete objtypeh; - delete spellh; - delete skillh; - delete modh; - delete bth; - delete tplh; - delete terviewh; -#if SCRIPTING_ENABLED - delete scriptHandler; -#endif - delete battlefieldsHandler; - delete generaltexth; - delete identifiersHandler; - makeNull(); -} - -void LibClasses::makeNull() -{ - generaltexth = nullptr; - heroh = nullptr; - arth = nullptr; - creh = nullptr; - townh = nullptr; - objh = nullptr; - objtypeh = nullptr; - spellh = nullptr; - skillh = nullptr; - modh = nullptr; - bth = nullptr; - tplh = nullptr; - terviewh = nullptr; -#if SCRIPTING_ENABLED - scriptHandler = nullptr; -#endif - battlefieldsHandler = nullptr; - identifiersHandler = nullptr; -} - -LibClasses::LibClasses() -{ - //init pointers to handlers - makeNull(); -} - -void LibClasses::callWhenDeserializing() -{ - //FIXME: check if any of these are needed - //generaltexth = new CGeneralTextHandler(); - //generaltexth->load(); - //arth->load(true); - //modh->recreateHandlers(); - //modh->loadConfigFromFile ("defaultMods"); //TODO: remember last saved config -} - #if SCRIPTING_ENABLED void LibClasses::scriptsLoaded() { @@ -307,10 +247,8 @@ void LibClasses::scriptsLoaded() } #endif -LibClasses::~LibClasses() -{ - clear(); -} +LibClasses::LibClasses() = default; +LibClasses::~LibClasses() = default; std::shared_ptr LibClasses::getContent() const { diff --git a/lib/VCMI_Lib.h b/lib/VCMI_Lib.h index c6cd971f9..10cdaecba 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CConsoleHandler; class CArtHandler; class CHeroHandler; +class CHeroClassHandler; class CCreatureHandler; class CSpellHandler; class CSkillHandler; @@ -47,20 +48,15 @@ namespace scripting } #endif - /// Loads and constructs several handlers -class DLL_LINKAGE LibClasses : public Services +class DLL_LINKAGE LibClasses final : public Services { - CBonusTypeHandler * bth; + std::shared_ptr bth; - void callWhenDeserializing(); //should be called only by serialize !!! - void makeNull(); //sets all handler pointers to null std::shared_ptr getContent() const; void setContent(std::shared_ptr content); public: - bool IS_AI_ENABLED = false; //unused? - const ArtifactService * artifacts() const override; const CreatureService * creatures() const override; const FactionService * factions() const override; @@ -83,93 +79,47 @@ public: const IBonusTypeHandler * getBth() const; //deprecated const CIdentifierStorage * identifiers() const; - CArtHandler * arth; - CHeroHandler * heroh; - CCreatureHandler * creh; - CSpellHandler * spellh; - CSkillHandler * skillh; - CObjectHandler * objh; - CObjectClassesHandler * objtypeh; - CTownHandler * townh; - CGeneralTextHandler * generaltexth; - CModHandler * modh; + std::shared_ptr arth; + std::shared_ptr heroh; + std::shared_ptr heroclassesh; + std::shared_ptr creh; + std::shared_ptr spellh; + std::shared_ptr skillh; + std::shared_ptr objh; + std::shared_ptr objtypeh; + std::shared_ptr townh; + std::shared_ptr generaltexth; + std::shared_ptr modh; + std::shared_ptr terrainTypeHandler; + std::shared_ptr roadTypeHandler; + std::shared_ptr riverTypeHandler; + std::shared_ptr identifiersHandler; + std::shared_ptr terviewh; + std::shared_ptr tplh; + std::shared_ptr battlefieldsHandler; + std::shared_ptr obstacleHandler; + std::shared_ptr settingsHandler; - TerrainTypeHandler * terrainTypeHandler; - RoadTypeHandler * roadTypeHandler; - RiverTypeHandler * riverTypeHandler; - CIdentifierStorage * identifiersHandler; - - CTerrainViewPatternConfig * terviewh; - CRmgTemplateStorage * tplh; - BattleFieldHandler * battlefieldsHandler; - ObstacleHandler * obstacleHandler; - GameSettings * settingsHandler; #if SCRIPTING_ENABLED - scripting::ScriptHandler * scriptHandler; + std::shared_ptr scriptHandler; #endif LibClasses(); //c-tor, loads .lods and NULLs handlers ~LibClasses(); void init(bool onlyEssential); //uses standard config file - void clear(); //deletes all handlers and its data // basic initialization. should be called before init(). Can also extract original H3 archives void loadFilesystem(bool extractArchives); - void loadModFilesystem(bool onlyEssential); + void loadModFilesystem(); #if SCRIPTING_ENABLED void scriptsLoaded(); #endif - - template void serialize(Handler &h, const int version) - { - h & identifiersHandler; // must be first - identifiers registry is used for handlers loading -#if SCRIPTING_ENABLED - h & scriptHandler;//must be first (or second after identifiers), it can modify factories other handlers depends on - if(!h.saving) - { - scriptsLoaded(); - } -#endif - - h & settingsHandler; - h & heroh; - h & arth; - h & creh; - h & townh; - h & objh; - h & objtypeh; - h & spellh; - h & skillh; - h & battlefieldsHandler; - h & obstacleHandler; - h & roadTypeHandler; - h & riverTypeHandler; - h & terrainTypeHandler; - - if(!h.saving) - { - //modh will be changed and modh->content will be empty after deserialization - auto content = getContent(); - h & modh; - setContent(content); - } - else - h & modh; - - h & IS_AI_ENABLED; - h & bth; - - if(!h.saving) - { - callWhenDeserializing(); - } - } }; extern DLL_LINKAGE LibClasses * VLC; -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false); +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool extractArchives); DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false); diff --git a/lib/battle/BattleAction.h b/lib/battle/BattleAction.h index 8fc5d0bfc..40253b2a5 100644 --- a/lib/battle/BattleAction.h +++ b/lib/battle/BattleAction.h @@ -55,7 +55,7 @@ public: battle::Target getTarget(const CBattleInfoCallback * cb) const; void setTarget(const battle::Target & target_); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & side; h & stackNumber; @@ -70,7 +70,7 @@ private: int32_t unitValue; BattleHex hexValue; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & unitValue; h & hexValue; diff --git a/lib/battle/BattleHex.h b/lib/battle/BattleHex.h index 69de2b811..3b06b4828 100644 --- a/lib/battle/BattleHex.h +++ b/lib/battle/BattleHex.h @@ -105,7 +105,7 @@ struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class f static BattleHex getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities); //TODO: vector or set? copying one to another is bad template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & hex; } diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index fcccfafe2..dfa02115d 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -12,6 +12,7 @@ #include "CObstacleInstance.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" +#include "../CRandomGenerator.h" #include "../CStack.h" #include "../CHeroHandler.h" #include "../filesystem/Filesystem.h" @@ -20,6 +21,7 @@ #include "../BattleFieldHandler.h" #include "../ObstacleHandler.h" + //TODO: remove #include "../IGameCallback.h" @@ -154,7 +156,8 @@ struct RangeGenerator return ret; } - int min, remainingCount; + int min; + int remainingCount; std::vector remaining; std::function myRand; }; @@ -239,7 +242,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const { try { - RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); + RangeGenerator obidgen(0, VLC->obstacleHandler->size() - 1, ourRand); auto obstPtr = std::make_shared(); obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE; obstPtr->ID = obidgen.getSuchNumber(appropriateAbsoluteObstacle); @@ -261,7 +264,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const { while(tilesToBlock > 0) { - RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); + RangeGenerator obidgen(0, VLC->obstacleHandler->size() - 1, ourRand); auto tileAccessibility = curB->getAccesibility(); const int obid = obidgen.getSuchNumber(appropriateUsualObstacle); const ObstacleInfo &obi = *Obstacle(obid).getInfo(); @@ -440,7 +443,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const } //native terrain bonuses - static auto nativeTerrain = std::make_shared(); + auto nativeTerrain = std::make_shared(); curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID())->addLimiter(nativeTerrain)); curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); @@ -562,14 +565,14 @@ int32_t BattleInfo::getActiveStackID() const return activeStack; } -TStacks BattleInfo::getStacksIf(TStackFilter predicate) const +TStacks BattleInfo::getStacksIf(const TStackFilter & predicate) const { TStacks ret; vstd::copy_if(stacks, std::back_inserter(ret), predicate); return ret; } -battle::Units BattleInfo::getUnitsIf(battle::UnitFilter predicate) const +battle::Units BattleInfo::getUnitsIf(const battle::UnitFilter & predicate) const { battle::Units ret; vstd::copy_if(stacks, std::back_inserter(ret), predicate); @@ -956,14 +959,14 @@ void BattleInfo::setWallState(EWallPart partOfWall, EWallState state) void BattleInfo::addObstacle(const ObstacleChanges & changes) { - std::shared_ptr obstacle = std::make_shared(); + auto obstacle = std::make_shared(); obstacle->fromInfo(changes); obstacles.push_back(obstacle); } void BattleInfo::updateObstacle(const ObstacleChanges& changes) { - std::shared_ptr changedObstacle = std::make_shared(); + auto changedObstacle = std::make_shared(); changedObstacle->fromInfo(changes); for(auto & obstacle : obstacles) @@ -1008,7 +1011,7 @@ scripting::Pool * BattleInfo::getContextPool() const { //this is real battle, use global scripting context pool //TODO: make this line not ugly - return IObjectInterface::cb->getGlobalContextPool(); + return battleGetFightingHero(0)->cb->getGlobalContextPool(); } #endif diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 030f56051..589e435f7 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -34,7 +34,8 @@ public: DEFENDER }; std::array sides; //sides[0] - attacker, sides[1] - defender - si32 round, activeStack; + si32 round; + si32 activeStack; const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege) int3 tile; //for background and bonuses bool creatureBank; //auxilary field, do not serialize @@ -49,7 +50,7 @@ public: ui8 tacticsSide; //which side is requested to play tactics phase ui8 tacticDistance; //how many hexes we can go forward (1 = only hexes adjacent to margin line) - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & battleID; h & sides; @@ -65,10 +66,7 @@ public: h & tacticsSide; h & tacticDistance; h & static_cast(*this); - if (version > 824) - h & replayAllowed; - else - replayAllowed = false; + h & replayAllowed; } ////////////////////////////////////////////////////////////////////////// @@ -85,9 +83,9 @@ public: int32_t getActiveStackID() const override; - TStacks getStacksIf(TStackFilter predicate) const override; + TStacks getStacksIf(const TStackFilter & predicate) const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; BattleField getBattlefieldType() const override; TerrainId getTerrainType() const override; diff --git a/lib/battle/BattleProxy.cpp b/lib/battle/BattleProxy.cpp index 9ec4339d4..6bc34a77e 100644 --- a/lib/battle/BattleProxy.cpp +++ b/lib/battle/BattleProxy.cpp @@ -40,12 +40,12 @@ int32_t BattleProxy::getActiveStackID() const return -1; } -TStacks BattleProxy::getStacksIf(TStackFilter predicate) const +TStacks BattleProxy::getStacksIf(const TStackFilter & predicate) const { return subject->battleGetStacksIf(predicate); } -battle::Units BattleProxy::getUnitsIf(battle::UnitFilter predicate) const +battle::Units BattleProxy::getUnitsIf(const battle::UnitFilter & predicate) const { return subject->battleGetUnitsIf(predicate); } diff --git a/lib/battle/BattleProxy.h b/lib/battle/BattleProxy.h index d52d4231e..c43ede14d 100644 --- a/lib/battle/BattleProxy.h +++ b/lib/battle/BattleProxy.h @@ -29,9 +29,9 @@ public: int32_t getActiveStackID() const override; - TStacks getStacksIf(TStackFilter predicate) const override; + TStacks getStacksIf(const TStackFilter & predicate) const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; BattleField getBattlefieldType() const override; TerrainId getTerrainType() const override; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 28fbf09ee..1e34d06e5 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -25,6 +25,7 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../BattleFieldHandler.h" #include "../Rect.h" +#include "../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -267,7 +268,7 @@ std::vector CBattleInfoCallback::getClientActionsFor allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere - if(stack->canMove() && stack->speed(0, true)) //probably no reason to try move war machines or bound stacks + if(stack->canMove() && stack->getMovementRange(0)) //probably no reason to try move war machines or bound stacks allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); const auto * siegedTown = battleGetDefendedTown(); @@ -324,22 +325,6 @@ std::set CBattleInfoCallback::battleGetAttackedHexes(const battle::Un return attackedHexes; } -SpellID CBattleInfoCallback::battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const -{ - switch (mode) - { - case RANDOM_GENIE: - return getRandomBeneficialSpell(rand, stack); //target - break; - case RANDOM_AIMED: - return getRandomCastedSpell(rand, stack); //caster - break; - default: - logGlobal->error("Incorrect mode of battleGetRandomSpell (%d)", static_cast(mode)); - return SpellID::NONE; - } -} - const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const { RETURN_IF_NOT_BATTLE(nullptr); @@ -357,7 +342,7 @@ const battle::Unit * CBattleInfoCallback::battleGetUnitByPos(BattleHex pos, bool auto ret = battleGetUnitsIf([=](const battle::Unit * unit) { return !unit->isGhost() - && vstd::contains(battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide()), pos) + && unit->coversPos(pos) && (!onlyAlive || unit->alive()); }); @@ -586,7 +571,7 @@ std::vector CBattleInfoCallback::battleGetAvailableHexes(const Reacha if(!unit->getPosition().isValid()) //turrets return ret; - auto unitSpeed = unit->speed(0, true); + auto unitSpeed = unit->getMovementRange(0); const bool tacticsPhase = battleTacticDist() && battleGetTacticsSide() == unit->unitSide(); @@ -757,15 +742,15 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * { RETURN_IF_NOT_BATTLE({}); auto reachability = battleGetDistances(attacker, attacker->getPosition()); - int movementDistance = reachability[attackerPosition]; - return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); + int getMovementRange = reachability[attackerPosition]; + return battleEstimateDamage(attacker, defender, getMovementRange, retaliationDmg); } -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg) const { RETURN_IF_NOT_BATTLE({}); const bool shooting = battleCanShoot(attacker, defender->getPosition()); - const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); + const BattleAttackInfo bai(attacker, defender, getMovementRange, shooting); return battleEstimateDamage(bai, retaliationDmg); } @@ -812,7 +797,7 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInf std::vector> CBattleInfoCallback::battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking) const { - std::vector> obstacles = std::vector>(); + auto obstacles = std::vector>(); RETURN_IF_NOT_BATTLE(obstacles); for(auto & obs : battleGetAllObstacles()) { @@ -883,9 +868,10 @@ bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & s auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, (BattlePerspective::BattlePerspective)side); const auto * hero = battleGetFightingHero(spellObstacle->casterSide); auto caster = spells::ObstacleCasterProxy(getBattle()->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); - const auto * sp = obstacle->getTrigger().toSpell(); - if(obstacle->triggersEffects() && sp) + + if(obstacle->triggersEffects() && obstacle->getTrigger().hasValue()) { + const auto * sp = obstacle->getTrigger().toSpell(); auto cast = spells::BattleCast(this, &caster, spells::Mode::PASSIVE, sp); spells::detail::ProblemImpl ignored; auto target = spells::Target(1, spells::Destination(&unit)); @@ -1155,7 +1141,7 @@ std::pair CBattleInfoCallback::getNearestStack( BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const { - bool twoHex = VLC->creh->objects[creID]->isDoubleWide(); + bool twoHex = VLC->creatures()->getById(creID)->isDoubleWide(); int pos; if (initialPos > -1) @@ -1610,7 +1596,7 @@ std::set CBattleInfoCallback::battleAdjacentUnits(const ba return ret; } -SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const +SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const battle::Unit * caster, const battle::Unit * subject) const { RETURN_IF_NOT_BATTLE(SpellID::NONE); //This is complete list. No spells from mods. @@ -1658,12 +1644,22 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c std::stringstream cachingStr; cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; - if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)), Selector::all, cachingStr.str()) - //TODO: this ability has special limitations - || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) + if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)), Selector::all, cachingStr.str())) continue; - switch (spellID) + auto spellPtr = spellID.toSpell(); + spells::Target target; + target.emplace_back(subject); + + spells::BattleCast cast(this, caster, spells::Mode::CREATURE_ACTIVE, spellPtr); + + auto m = spellPtr->battleMechanics(&cast); + spells::detail::ProblemImpl problem; + + if (!m->canBeCastAt(target, problem)) + continue; + + switch (spellID.toEnum()) { case SpellID::SHIELD: case SpellID::FIRE_SHIELD: // not if all enemy units are shooters @@ -1703,7 +1699,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c case SpellID::CURE: //only damaged units { //do not cast on affected by debuffs - if(!subject->canBeHealed()) + if(subject->getFirstHPleft() == subject->getMaxHealth()) continue; } break; diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index a05027a5d..b1a16ef1f 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -36,7 +36,7 @@ struct DLL_LINKAGE AttackableTiles { std::set hostileCreaturePositions; std::set friendlyCreaturePositions; //for Dragon Breath - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & hostileCreaturePositions; h & friendlyCreaturePositions; @@ -52,11 +52,6 @@ struct DLL_LINKAGE BattleClientInterfaceData class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials { public: - enum ERandomSpell - { - RANDOM_GENIE, RANDOM_AIMED - }; - std::optional battleIsFinished() const override; //return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw std::vector> battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking = true) const override; @@ -103,7 +98,7 @@ public: /// returns pair DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const; DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const; - DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg = nullptr) const; bool battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const; bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; @@ -121,8 +116,7 @@ public: int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell - SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const; - SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const; + SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const battle::Unit * caster, const battle::Unit * target) const; SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon std::vector getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data); diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index c113dddef..f3ee409c0 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -109,13 +109,13 @@ TStacks CBattleInfoEssentials::battleGetAllStacks(bool includeTurrets) const }); } -TStacks CBattleInfoEssentials::battleGetStacksIf(TStackFilter predicate) const +TStacks CBattleInfoEssentials::battleGetStacksIf(const TStackFilter & predicate) const { RETURN_IF_NOT_BATTLE(TStacks()); return getBattle()->getStacksIf(std::move(predicate)); } -battle::Units CBattleInfoEssentials::battleGetUnitsIf(battle::UnitFilter predicate) const +battle::Units CBattleInfoEssentials::battleGetUnitsIf(const battle::UnitFilter & predicate) const { RETURN_IF_NOT_BATTLE(battle::Units()); return getBattle()->getUnitsIf(predicate); @@ -282,7 +282,7 @@ bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const return false; //we are besieged defender - if(side == BattleSide::DEFENDER && battleGetSiegeLevel()) + if(side == BattleSide::DEFENDER && getBattle()->getDefendedTown() != nullptr) { const auto * town = battleGetDefendedTown(); if(!town->hasBuilt(BuildingSubID::ESCAPE_TUNNEL)) @@ -357,7 +357,7 @@ bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const const auto side = playerToSide(player); if(!side) return false; - bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && battleGetSiegeLevel()); + bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && getBattle()->getDefendedTown() != nullptr); //conditions like for fleeing (except escape tunnel presence) + enemy must have a hero return battleCanFlee(player) && !iAmSiegeDefender && battleHasHero(otherSide(side.value())); } @@ -401,10 +401,9 @@ PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) con PlayerColor initialOwner = getBattle()->getSidePlayer(unit->unitSide()); - static CSelector selector = Selector::type()(BonusType::HYPNOTIZED); - static std::string cachingString = "type_103s-1"; + static const CSelector selector = Selector::type()(BonusType::HYPNOTIZED); - if(unit->hasBonus(selector, cachingString)) + if(unit->hasBonus(selector)) return otherPlayer(initialOwner); else return initialOwner; diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index 0dc3cab1b..3af7c1b50 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -52,7 +52,7 @@ public: BattleField battleGetBattlefieldType() const override; int32_t battleGetEnchanterCounter(ui8 side) const; - std::vector > battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield + std::vector> battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield std::shared_ptr battleGetObstacleByID(uint32_t ID) const; @@ -62,8 +62,8 @@ public: * @return filtered stacks * */ - TStacks battleGetStacksIf(TStackFilter predicate) const; //deprecated - battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const override; + TStacks battleGetStacksIf(const TStackFilter & predicate) const; //deprecated + battle::Units battleGetUnitsIf(const battle::UnitFilter & predicate) const override; const battle::Unit * battleGetUnitByID(uint32_t ID) const override; const battle::Unit * battleActiveUnit() const override; diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index cf8bc157f..c0607b031 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -63,7 +63,7 @@ struct DLL_LINKAGE CObstacleInstance virtual void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & ID; h & pos; @@ -118,7 +118,7 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance void serializeJson(JsonSerializeFormat & handler) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & turnsRemaining; diff --git a/lib/battle/CPlayerBattleCallback.cpp b/lib/battle/CPlayerBattleCallback.cpp index c1256e531..c0457b89e 100644 --- a/lib/battle/CPlayerBattleCallback.cpp +++ b/lib/battle/CPlayerBattleCallback.cpp @@ -64,7 +64,7 @@ TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyA int CPlayerBattleCallback::battleGetSurrenderCost() const { - RETURN_IF_NOT_BATTLE(-3) + RETURN_IF_NOT_BATTLE(-3); ASSERT_IF_CALLED_WITH_PLAYER return CBattleInfoCallback::battleGetSurrenderCost(*getPlayerID()); } diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index ac8b30413..eb026acc4 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -46,7 +46,7 @@ int32_t CAmmo::available() const bool CAmmo::canUse(int32_t amount) const { - return !isLimited() || (available() - amount >= 0); + return (available() - amount >= 0) || !isLimited(); } bool CAmmo::isLimited() const @@ -99,7 +99,7 @@ CShots & CShots::operator=(const CShots & other) bool CShots::isLimited() const { - return !env->unitHasAmmoCart(owner) || !shooter.getHasBonus(); + return !shooter.getHasBonus() || !env->unitHasAmmoCart(owner); } void CShots::setEnv(const IUnitEnvironment * env_) @@ -428,7 +428,7 @@ const CGHeroInstance * CUnitState::getHeroCaster() const return nullptr; } -int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const +int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const { int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spell->getId()))); vstd::abetween(skill, 0, 3); @@ -485,7 +485,7 @@ void CUnitState::getCastDescription(const spells::Spell * spell, const std::vect text.appendLocalString(EMetaText::GENERAL_TXT, 565);//The %s casts %s //todo: use text 566 for single creature getCasterName(text); - text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex()); + text.replaceName(spell->getId()); } int32_t CUnitState::manaLimit() const diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index e4597b4ad..9e0fb41c9 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -182,7 +182,7 @@ public: int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const spells::Spell * spell) const override; int64_t getSpellBonus(const spells::Spell * spell, int64_t base, const Unit * affectedStack) const override; diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index dfed47953..58279e118 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" + #include "DamageCalculator.h" #include "CBattleInfoCallback.h" #include "Unit.h" @@ -124,7 +125,19 @@ int DamageCalculator::getActorAttackBase() const int DamageCalculator::getActorAttackEffective() const { - return getActorAttackBase() + getActorAttackSlayer(); + return getActorAttackBase() + getActorAttackSlayer() + getActorAttackIgnored(); +} + +int DamageCalculator::getActorAttackIgnored() const +{ + int multAttackReductionPercent = battleBonusValue(info.defender, Selector::type()(BonusType::ENEMY_ATTACK_REDUCTION)); + + if(multAttackReductionPercent > 0) + { + int reduction = (getActorAttackBase() * multAttackReductionPercent + 49) / 100; //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + return -std::min(reduction, getActorAttackBase()); + } + return 0; } int DamageCalculator::getActorAttackSlayer() const @@ -132,6 +145,9 @@ int DamageCalculator::getActorAttackSlayer() const const std::string cachingStrSlayer = "type_SLAYER"; static const auto selectorSlayer = Selector::type()(BonusType::SLAYER); + if (!info.defender->hasBonusOfType(BonusType::KING)) + return 0; + auto slayerEffects = info.attacker->getBonuses(selectorSlayer, cachingStrSlayer); auto slayerAffected = info.defender->unitType()->valOfBonuses(Selector::type()(BonusType::KING)); @@ -263,6 +279,20 @@ double DamageCalculator::getAttackHateFactor() const return allHateEffects->valOfBonuses(Selector::subtype()(BonusSubtypeID(info.defender->creatureId()))) / 100.0; } +double DamageCalculator::getAttackRevengeFactor() const +{ + if(info.attacker->hasBonusOfType(BonusType::REVENGE)) //HotA Haspid ability + { + int totalStackCount = info.attacker->unitBaseAmount(); + int currentStackHealth = info.attacker->getAvailableHealth(); + int creatureHealth = info.attacker->getMaxHealth(); + + return sqrt(static_cast((totalStackCount + 1) * creatureHealth) / (currentStackHealth + creatureHealth) - 1); + } + + return 0.0; +} + double DamageCalculator::getDefenseSkillFactor() const { int defenseAdvantage = getTargetDefenseEffective() - getActorAttackEffective(); @@ -430,7 +460,8 @@ std::vector DamageCalculator::getAttackFactors() const getAttackJoustingFactor(), getAttackDeathBlowFactor(), getAttackDoubleDamageFactor(), - getAttackHateFactor() + getAttackHateFactor(), + getAttackRevengeFactor() }; } @@ -500,12 +531,10 @@ DamageEstimation DamageCalculator::calculateDmgRange() const for (auto & factor : defenseFactors) { assert(factor >= 0.0); - defenseFactorTotal *= ( 1 - std::min(1.0, factor)); + defenseFactorTotal *= (1 - std::min(1.0, factor)); } - double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal); - - info.defender->getTotalHealth(); + double resultingFactor = attackFactorTotal * defenseFactorTotal; DamageRange damageDealt { std::max( 1.0, std::floor(damageBase.min * resultingFactor)), diff --git a/lib/battle/DamageCalculator.h b/lib/battle/DamageCalculator.h index 9548f950f..d7f91e0b5 100644 --- a/lib/battle/DamageCalculator.h +++ b/lib/battle/DamageCalculator.h @@ -38,6 +38,7 @@ class DLL_LINKAGE DamageCalculator int getActorAttackBase() const; int getActorAttackEffective() const; int getActorAttackSlayer() const; + int getActorAttackIgnored() const; int getTargetDefenseBase() const; int getTargetDefenseEffective() const; int getTargetDefenseIgnored() const; @@ -50,6 +51,7 @@ class DLL_LINKAGE DamageCalculator double getAttackDeathBlowFactor() const; double getAttackDoubleDamageFactor() const; double getAttackHateFactor() const; + double getAttackRevengeFactor() const; double getDefenseSkillFactor() const; double getDefenseArmorerFactor() const; diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index 5996d5bda..b5b9ce583 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -15,7 +15,7 @@ #include -#define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; } +#define RETURN_IF_NOT_BATTLE(...) do { if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; } } while (false) VCMI_LIB_NAMESPACE_BEGIN @@ -72,7 +72,7 @@ public: virtual uint32_t battleNextUnitId() const = 0; - virtual battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const = 0; + virtual battle::Units battleGetUnitsIf(const battle::UnitFilter & predicate) const = 0; virtual const battle::Unit * battleGetUnitByID(uint32_t ID) const = 0; virtual const battle::Unit * battleGetUnitByPos(BattleHex pos, bool onlyAlive = true) const = 0; diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index 8af3ad429..fd643c56c 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -42,9 +42,9 @@ public: virtual int32_t getActiveStackID() const = 0; - virtual TStacks getStacksIf(TStackFilter predicate) const = 0; + virtual TStacks getStacksIf(const TStackFilter & predicate) const = 0; - virtual battle::Units getUnitsIf(battle::UnitFilter predicate) const = 0; + virtual battle::Units getUnitsIf(const battle::UnitFilter & predicate) const = 0; virtual BattleField getBattlefieldType() const = 0; virtual TerrainId getTerrainType() const = 0; diff --git a/lib/battle/SideInBattle.cpp b/lib/battle/SideInBattle.cpp index 25fe84127..bfdcafb95 100644 --- a/lib/battle/SideInBattle.cpp +++ b/lib/battle/SideInBattle.cpp @@ -18,7 +18,7 @@ void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army hero = Hero; armyObject = Army; - switch(armyObject->ID) + switch(armyObject->ID.toEnum()) { case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR2: diff --git a/lib/battle/SideInBattle.h b/lib/battle/SideInBattle.h index 152b79361..f1c218526 100644 --- a/lib/battle/SideInBattle.h +++ b/lib/battle/SideInBattle.h @@ -28,7 +28,7 @@ struct DLL_LINKAGE SideInBattle void init(const CGHeroInstance * Hero, const CArmedInstance * Army); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & color; h & hero; diff --git a/lib/battle/SiegeInfo.h b/lib/battle/SiegeInfo.h index 6f69d8041..6ce0b34e1 100644 --- a/lib/battle/SiegeInfo.h +++ b/lib/battle/SiegeInfo.h @@ -23,7 +23,7 @@ struct DLL_LINKAGE SiegeInfo // return EWallState decreased by value of damage points static EWallState applyDamage(EWallState state, unsigned int value); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & wallState; h & gateState; diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index 7e0c9be2d..c08713f43 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -187,11 +187,11 @@ void Unit::addText(MetaString & text, EMetaText type, int32_t serial, const boos void Unit::addNameReplacement(MetaString & text, const boost::logic::tribool & plural) const { if(boost::logic::indeterminate(plural)) - text.replaceCreatureName(creatureId(), getCount()); + text.replaceName(creatureId(), getCount()); else if(plural) - text.replaceLocalString(EMetaText::CRE_PL_NAMES, creatureIndex()); + text.replaceNamePlural(creatureIndex()); else - text.replaceLocalString(EMetaText::CRE_SING_NAMES, creatureIndex()); + text.replaceNameSingular(creatureIndex()); } std::string Unit::formatGeneralMessage(const int32_t baseTextId) const @@ -200,7 +200,7 @@ std::string Unit::formatGeneralMessage(const int32_t baseTextId) const MetaString text; text.appendLocalString(EMetaText::GENERAL_TXT, textId); - text.replaceCreatureName(creatureId(), getCount()); + text.replaceName(creatureId(), getCount()); return text.toString(); } @@ -218,7 +218,7 @@ int Unit::getRawSurrenderCost() const void UnitInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("count", count); - handler.serializeId("type", type, CreatureID::NONE); + handler.serializeId("type", type, CreatureID(CreatureID::NONE)); handler.serializeInt("side", side); handler.serializeInt("position", position); handler.serializeBool("summoned", summoned); diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index fb45cbb46..35bc72c72 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -10,7 +10,6 @@ #include "StdInc.h" #include "Bonus.h" -#include "CBonusSystemNode.h" #include "Limiters.h" #include "Updaters.h" #include "Propagators.h" @@ -72,26 +71,26 @@ si32 CAddInfo::operator[](size_type pos) const std::string CAddInfo::toString() const { - return toJsonNode().toJson(true); + return toJsonNode().toCompactString(); } JsonNode CAddInfo::toJsonNode() const { if(size() < 2) { - return JsonUtils::intNode(operator[](0)); + return JsonNode(operator[](0)); } else { - JsonNode node(JsonNode::JsonType::DATA_VECTOR); + JsonNode node; for(si32 value : *this) - node.Vector().push_back(JsonUtils::intNode(value)); + node.Vector().emplace_back(value); return node; } } std::string Bonus::Description(std::optional customValue) const { - std::ostringstream str; + std::string str; if(description.empty()) { @@ -100,38 +99,42 @@ std::string Bonus::Description(std::optional customValue) const switch(source) { case BonusSource::ARTIFACT: - str << sid.as().toArtifact(VLC->artifacts())->getNameTranslated(); + str = sid.as().toEntity(VLC)->getNameTranslated(); break; case BonusSource::SPELL_EFFECT: - str << sid.as().toSpell(VLC->spells())->getNameTranslated(); + str = sid.as().toEntity(VLC)->getNameTranslated(); break; case BonusSource::CREATURE_ABILITY: - str << sid.as().toCreature(VLC->creatures())->getNamePluralTranslated(); + str = sid.as().toEntity(VLC)->getNamePluralTranslated(); break; case BonusSource::SECONDARY_SKILL: - str << VLC->skills()->getById(sid.as())->getNameTranslated(); + str = VLC->skills()->getById(sid.as())->getNameTranslated(); break; case BonusSource::HERO_SPECIAL: - str << VLC->heroTypes()->getById(sid.as())->getNameTranslated(); + str = VLC->heroTypes()->getById(sid.as())->getNameTranslated(); break; default: //todo: handle all possible sources - str << "Unknown"; + str = "Unknown"; break; } } else - str << stacking; + str = stacking; } else { - str << description; + str = description; } - if(auto value = customValue.value_or(val)) - str << " " << std::showpos << value; + if(auto value = customValue.value_or(val)) { + //arraytxt already contains +-value + std::string valueString = boost::str(boost::format(" %+d") % value); + if(!boost::algorithm::ends_with(str, valueString)) + str += valueString; + } - return str.str(); + return str; } static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) @@ -139,7 +142,7 @@ static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) switch(type) { case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); + return JsonNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); default: return addInfo.toJsonNode(); } @@ -147,7 +150,7 @@ static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) JsonNode Bonus::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; // only add values that might reasonably be found in config files root["type"].String() = vstd::findKey(bonusNameMap, type); if(subtype != BonusSubtypeID()) diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 5f4a63608..53693917e 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -86,7 +86,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype, BonusValueType ValType); Bonus() = default; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & duration; h & type; @@ -162,6 +162,11 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this auto set = hb->duration & BonusDuration::COMMANDER_KILLED; return set.any(); } + static bool UntilOwnAttack(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::UNTIL_OWN_ATTACK; + return set.any(); + } inline bool operator == (const BonusType & cf) const { return type == cf; diff --git a/lib/bonuses/BonusCustomTypes.cpp b/lib/bonuses/BonusCustomTypes.cpp index 4e793ac21..6f6ac83b0 100644 --- a/lib/bonuses/BonusCustomTypes.cpp +++ b/lib/bonuses/BonusCustomTypes.cpp @@ -23,6 +23,10 @@ const BonusCustomSubtype BonusCustomSubtype::heroMovementLand(1); const BonusCustomSubtype BonusCustomSubtype::heroMovementSea(0); const BonusCustomSubtype BonusCustomSubtype::deathStareGorgon(0); const BonusCustomSubtype BonusCustomSubtype::deathStareCommander(1); +const BonusCustomSubtype BonusCustomSubtype::deathStareNoRangePenalty(2); +const BonusCustomSubtype BonusCustomSubtype::deathStareRangePenalty(3); +const BonusCustomSubtype BonusCustomSubtype::deathStareObstaclePenalty(4); +const BonusCustomSubtype BonusCustomSubtype::deathStareRangeObstaclePenalty(5); const BonusCustomSubtype BonusCustomSubtype::rebirthRegular(0); const BonusCustomSubtype BonusCustomSubtype::rebirthSpecial(1); const BonusCustomSubtype BonusCustomSubtype::visionsMonsters(0); diff --git a/lib/bonuses/BonusCustomTypes.h b/lib/bonuses/BonusCustomTypes.h index 2a2f3df48..824561929 100644 --- a/lib/bonuses/BonusCustomTypes.h +++ b/lib/bonuses/BonusCustomTypes.h @@ -13,10 +13,10 @@ VCMI_LIB_NAMESPACE_BEGIN -class DLL_LINKAGE BonusCustomSource : public Identifier +class DLL_LINKAGE BonusCustomSource : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static std::string encode(int32_t index); static si32 decode(const std::string & identifier); @@ -24,10 +24,10 @@ public: static const BonusCustomSource undeadMoraleDebuff; // -2 }; -class DLL_LINKAGE BonusCustomSubtype : public Identifier +class DLL_LINKAGE BonusCustomSubtype : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static std::string encode(int32_t index); static si32 decode(const std::string & identifier); @@ -45,6 +45,10 @@ public: static const BonusCustomSubtype deathStareGorgon; // 0 static const BonusCustomSubtype deathStareCommander; + static const BonusCustomSubtype deathStareNoRangePenalty; + static const BonusCustomSubtype deathStareRangePenalty; + static const BonusCustomSubtype deathStareObstaclePenalty; + static const BonusCustomSubtype deathStareRangeObstaclePenalty; static const BonusCustomSubtype rebirthRegular; // 0 static const BonusCustomSubtype rebirthSpecial; // 1 diff --git a/lib/bonuses/BonusEnum.cpp b/lib/bonuses/BonusEnum.cpp index 326211b9a..e48d15dcf 100644 --- a/lib/bonuses/BonusEnum.cpp +++ b/lib/bonuses/BonusEnum.cpp @@ -7,13 +7,10 @@ * Full text of license available in license.txt file, in main folder * */ - - #include "StdInc.h" #include "BonusEnum.h" - -#include "../JsonNode.h" +#include "../json/JsonUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -44,6 +41,7 @@ const std::map bonusDurationMap = BONUS_ITEM(UNTIL_ATTACK) BONUS_ITEM(STACK_GETS_TURN) BONUS_ITEM(COMMANDER_KILLED) + BONUS_ITEM(UNTIL_OWN_ATTACK) { "UNITL_BEING_ATTACKED", BonusDuration::UNTIL_BEING_ATTACKED }//typo, but used in some mods }; #undef BONUS_ITEM @@ -69,16 +67,16 @@ namespace BonusDuration } if(durationNames.size() == 1) { - return JsonUtils::stringNode(durationNames[0]); + return JsonNode(durationNames[0]); } else { - JsonNode node(JsonNode::JsonType::DATA_VECTOR); + JsonNode node; for(const std::string & dur : durationNames) - node.Vector().push_back(JsonUtils::stringNode(dur)); + node.Vector().emplace_back(dur); return node; } } } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 6902508a9..f6cb258e0 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -48,7 +48,7 @@ class JsonNode; BONUS_NAME(FLYING) \ BONUS_NAME(SHOOTER) \ BONUS_NAME(CHARGE_IMMUNITY) \ - BONUS_NAME(ADDITIONAL_ATTACK) \ + BONUS_NAME(ADDITIONAL_ATTACK) /*val: number of additional attacks to perform*/ \ BONUS_NAME(UNLIMITED_RETALIATIONS) \ BONUS_NAME(NO_MELEE_PENALTY) \ BONUS_NAME(JOUSTING) /*for champions*/ \ @@ -57,8 +57,8 @@ class JsonNode; BONUS_NAME(MAGIC_RESISTANCE) /*in % (value)*/ \ BONUS_NAME(CHANGES_SPELL_COST_FOR_ALLY) /*in mana points (value) , eg. mage*/ \ BONUS_NAME(CHANGES_SPELL_COST_FOR_ENEMY) /*in mana points (value) , eg. pegasus */ \ - BONUS_NAME(SPELL_AFTER_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only] */ \ - BONUS_NAME(SPELL_BEFORE_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only] */ \ + BONUS_NAME(SPELL_AFTER_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only], addInfo[2] -> spell layer for multiple SPELL_AFTER_ATTACK bonuses (default none [-1]) */ \ + BONUS_NAME(SPELL_BEFORE_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only], addInfo[2] -> spell layer for multiple SPELL_BEFORE_ATTACK bonuses (default none [-1]) */ \ BONUS_NAME(SPELL_RESISTANCE_AURA) /*eg. unicorns, value - resistance bonus in % for adjacent creatures*/ \ BONUS_NAME(LEVEL_SPELL_IMMUNITY) /*creature is immune to all spell with level below or equal to value of this bonus */ \ BONUS_NAME(BLOCK_MAGIC_ABOVE) /*blocks casting spells of the level > value */ \ @@ -169,7 +169,13 @@ class JsonNode; BONUS_NAME(MAX_LEARNABLE_SPELL_LEVEL) /*This can work as wisdom before. val = max learnable spell level*/\ BONUS_NAME(SPELL_SCHOOL_IMMUNITY) /*This bonus will work as spell school immunity for all spells, subtype - spell school: 0 - air, 1 - fire, 2 - water, 3 - earth. Any is not handled for reducing overlap from LEVEL_SPELL_IMMUNITY*/\ BONUS_NAME(NEGATIVE_EFFECTS_IMMUNITY) /*This bonus will work as spell school immunity for negative effects from spells of school, subtype - spell school: -1 - any, 0 - air, 1 - fire, 2 - water, 3 - earth*/\ - BONUS_NAME(TERRAIN_NATIVE) + BONUS_NAME(TERRAIN_NATIVE) \ + BONUS_NAME(UNLIMITED_MOVEMENT) /*cheat bonus*/ \ + BONUS_NAME(MAX_MORALE) /*cheat bonus*/ \ + BONUS_NAME(MAX_LUCK) /*cheat bonus*/ \ + BONUS_NAME(FEROCITY) /*extra attacks, only if at least some creatures killed while attacking target unit, val = amount of additional attacks, additional info = amount of creatures killed to trigger (default 1)*/ \ + BONUS_NAME(ENEMY_ATTACK_REDUCTION) /*in % (value) eg. Nix (HotA)*/ \ + BONUS_NAME(REVENGE) /*additional damage based on how many units in stack died - formula: sqrt((number of creatures at battle start + 1) * creature health) / (total health now + 1 creature health) - 1) * 100% */ \ /* end of list */ @@ -212,7 +218,7 @@ enum class BonusType }; namespace BonusDuration //when bonus is automatically removed { - using Type = std::bitset<10>; + using Type = std::bitset<11>; extern JsonNode toJson(const Type & duration); constexpr Type PERMANENT = 1 << 0; constexpr Type ONE_BATTLE = 1 << 1; //at the end of battle @@ -224,6 +230,7 @@ namespace BonusDuration //when bonus is automatically removed constexpr Type UNTIL_ATTACK = 1 << 7; /*removed after attack and counterattacks are performed*/ constexpr Type STACK_GETS_TURN = 1 << 8; /*removed when stack gets its turn - used for defensive stance*/ constexpr Type COMMANDER_KILLED = 1 << 9; + constexpr Type UNTIL_OWN_ATTACK = 1 << 10; /*removed after attack is performed (not counterattack)*/; }; enum class BonusSource { diff --git a/lib/bonuses/BonusList.cpp b/lib/bonuses/BonusList.cpp index 4920881b3..752335044 100644 --- a/lib/bonuses/BonusList.cpp +++ b/lib/bonuses/BonusList.cpp @@ -10,8 +10,7 @@ #include "StdInc.h" #include "CBonusSystemNode.h" - -#include "../JsonNode.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -214,7 +213,7 @@ int BonusList::valOfBonuses(const CSelector &select) const JsonNode BonusList::toJsonNode() const { - JsonNode node(JsonNode::JsonType::DATA_VECTOR); + JsonNode node; for(const std::shared_ptr & b : bonuses) node.Vector().push_back(b->toJsonNode()); return node; diff --git a/lib/bonuses/BonusList.h b/lib/bonuses/BonusList.h index eef5a8777..3629cdf9c 100644 --- a/lib/bonuses/BonusList.h +++ b/lib/bonuses/BonusList.h @@ -91,7 +91,7 @@ public: void insert(TInternalContainer::iterator position, TInternalContainer::size_type n, const std::shared_ptr & x); template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & static_cast(bonuses); } diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index 7274c0d9e..5b2765678 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -353,7 +353,7 @@ const JsonNode & BonusParams::toJson() ret["targetSourceType"].String() = vstd::findKey(bonusSourceMap, *targetType); jsonCreated = true; } - ret.setMeta(ModScope::scopeGame()); + ret.setModScope(ModScope::scopeGame()); return ret; }; diff --git a/lib/bonuses/BonusParams.h b/lib/bonuses/BonusParams.h index b0b2d3ef2..57e79c43a 100644 --- a/lib/bonuses/BonusParams.h +++ b/lib/bonuses/BonusParams.h @@ -12,7 +12,7 @@ #include "Bonus.h" #include "../GameConstants.h" -#include "../JsonNode.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/bonuses/BonusSelector.cpp b/lib/bonuses/BonusSelector.cpp index 1d57e8e13..b0c30602e 100644 --- a/lib/bonuses/BonusSelector.cpp +++ b/lib/bonuses/BonusSelector.cpp @@ -15,39 +15,39 @@ VCMI_LIB_NAMESPACE_BEGIN namespace Selector { - DLL_LINKAGE CSelectFieldEqual & type() + DLL_LINKAGE const CSelectFieldEqual & type() { - static CSelectFieldEqual stype(&Bonus::type); + static const CSelectFieldEqual stype(&Bonus::type); return stype; } - DLL_LINKAGE CSelectFieldEqual & subtype() + DLL_LINKAGE const CSelectFieldEqual & subtype() { - static CSelectFieldEqual ssubtype(&Bonus::subtype); + static const CSelectFieldEqual ssubtype(&Bonus::subtype); return ssubtype; } - DLL_LINKAGE CSelectFieldEqual & info() + DLL_LINKAGE const CSelectFieldEqual & info() { - static CSelectFieldEqual sinfo(&Bonus::additionalInfo); + static const CSelectFieldEqual sinfo(&Bonus::additionalInfo); return sinfo; } - DLL_LINKAGE CSelectFieldEqual & sourceType() + DLL_LINKAGE const CSelectFieldEqual & sourceType() { - static CSelectFieldEqual ssourceType(&Bonus::source); + static const CSelectFieldEqual ssourceType(&Bonus::source); return ssourceType; } - DLL_LINKAGE CSelectFieldEqual & targetSourceType() + DLL_LINKAGE const CSelectFieldEqual & targetSourceType() { - static CSelectFieldEqual ssourceType(&Bonus::targetSourceType); + static const CSelectFieldEqual ssourceType(&Bonus::targetSourceType); return ssourceType; } - DLL_LINKAGE CSelectFieldEqual & effectRange() + DLL_LINKAGE const CSelectFieldEqual & effectRange() { - static CSelectFieldEqual seffectRange(&Bonus::effectRange); + static const CSelectFieldEqual seffectRange(&Bonus::effectRange); return seffectRange; } @@ -82,6 +82,13 @@ namespace Selector return CSelectFieldEqual(&Bonus::valType)(valType); } + CSelector DLL_LINKAGE typeSubtypeValueType(BonusType Type, BonusSubtypeID Subtype, BonusValueType valType) + { + return type()(Type) + .And(subtype()(Subtype)) + .And(valueType(valType)); + } + DLL_LINKAGE CSelector all([](const Bonus * b){return true;}); DLL_LINKAGE CSelector none([](const Bonus * b){return false;}); } diff --git a/lib/bonuses/BonusSelector.h b/lib/bonuses/BonusSelector.h index 672fcfdc2..f8434b4c7 100644 --- a/lib/bonuses/BonusSelector.h +++ b/lib/bonuses/BonusSelector.h @@ -22,7 +22,7 @@ public: template CSelector(const T &t, //SFINAE trick -> include this c-tor in overload resolution only if parameter is class //(includes functors, lambdas) or function. Without that VC is going mad about ambiguities. - typename std::enable_if < boost::mpl::or_ < std::is_class, std::is_function> ::value>::type *dummy = nullptr) + typename std::enable_if_t < std::is_class_v || std::is_function_v > *dummy = nullptr) : TBase(t) {} @@ -125,12 +125,12 @@ public: namespace Selector { - extern DLL_LINKAGE CSelectFieldEqual & type(); - extern DLL_LINKAGE CSelectFieldEqual & subtype(); - extern DLL_LINKAGE CSelectFieldEqual & info(); - extern DLL_LINKAGE CSelectFieldEqual & sourceType(); - extern DLL_LINKAGE CSelectFieldEqual & targetSourceType(); - extern DLL_LINKAGE CSelectFieldEqual & effectRange(); + extern DLL_LINKAGE const CSelectFieldEqual & type(); + extern DLL_LINKAGE const CSelectFieldEqual & subtype(); + extern DLL_LINKAGE const CSelectFieldEqual & info(); + extern DLL_LINKAGE const CSelectFieldEqual & sourceType(); + extern DLL_LINKAGE const CSelectFieldEqual & targetSourceType(); + extern DLL_LINKAGE const CSelectFieldEqual & effectRange(); extern DLL_LINKAGE CWillLastTurns turns; extern DLL_LINKAGE CWillLastDays days; @@ -139,6 +139,7 @@ namespace Selector CSelector DLL_LINKAGE source(BonusSource source, BonusSourceID sourceID); CSelector DLL_LINKAGE sourceTypeSel(BonusSource source); CSelector DLL_LINKAGE valueType(BonusValueType valType); + CSelector DLL_LINKAGE typeSubtypeValueType(BonusType Type, BonusSubtypeID Subtype, BonusValueType valType); /** * Selects all bonuses diff --git a/lib/bonuses/CBonusSystemNode.cpp b/lib/bonuses/CBonusSystemNode.cpp index 4de398409..3cedf4408 100644 --- a/lib/bonuses/CBonusSystemNode.cpp +++ b/lib/bonuses/CBonusSystemNode.cpp @@ -20,18 +20,25 @@ VCMI_LIB_NAMESPACE_BEGIN std::atomic CBonusSystemNode::treeChanged(1); constexpr bool CBonusSystemNode::cachingEnabled = true; -#define FOREACH_PARENT(pname) TNodes lparents; getParents(lparents); for(CBonusSystemNode *pname : lparents) -#define FOREACH_RED_CHILD(pname) TNodes lchildren; getRedChildren(lchildren); for(CBonusSystemNode *pname : lchildren) +std::shared_ptr CBonusSystemNode::getLocalBonus(const CSelector & selector) +{ + auto ret = bonuses.getFirst(selector); + if(ret) + return ret; + return nullptr; +} -std::shared_ptr CBonusSystemNode::getBonusLocalFirst(const CSelector & selector) +std::shared_ptr CBonusSystemNode::getFirstBonus(const CSelector & selector) const { auto ret = bonuses.getFirst(selector); if(ret) return ret; - FOREACH_PARENT(pname) + TCNodes lparents; + getParents(lparents); + for(const CBonusSystemNode *pname : lparents) { - ret = pname->getBonusLocalFirst(selector); + ret = pname->getFirstBonus(selector); if (ret) return ret; } @@ -39,28 +46,15 @@ std::shared_ptr CBonusSystemNode::getBonusLocalFirst(const CSelector & se return nullptr; } -std::shared_ptr CBonusSystemNode::getBonusLocalFirst(const CSelector & selector) const -{ - return (const_cast(this))->getBonusLocalFirst(selector); -} - void CBonusSystemNode::getParents(TCNodes & out) const /*retrieves list of parent nodes (nodes to inherit bonuses from) */ { - for(const auto * elem : parents) + for(const auto * elem : parentsToInherit) out.insert(elem); } -void CBonusSystemNode::getParents(TNodes &out) -{ - for (auto * elem : parents) - { - out.insert(elem); - } -} - void CBonusSystemNode::getAllParents(TCNodes & out) const //retrieves list of parent nodes (nodes to inherit bonuses from) { - for(auto * parent : parents) + for(auto * parent : parentsToInherit) { out.insert(parent); parent->getAllParents(out); @@ -227,39 +221,6 @@ CBonusSystemNode::CBonusSystemNode(ENodeTypes NodeType): { } -CBonusSystemNode::CBonusSystemNode(CBonusSystemNode && other) noexcept: - bonuses(std::move(other.bonuses)), - exportedBonuses(std::move(other.exportedBonuses)), - nodeType(other.nodeType), - cachedLast(0), - isHypotheticNode(other.isHypotheticNode) -{ - std::swap(parents, other.parents); - std::swap(children, other.children); - - //fixing bonus tree without recalculation - - if(!isHypothetic()) - { - for(CBonusSystemNode * n : parents) - { - n->children -= &other; - n->children.push_back(this); - } - } - - for(CBonusSystemNode * n : children) - { - n->parents -= &other; - n->parents.push_back(this); - } - - //cache ignored - - //cachedBonuses - //cachedRequests -} - CBonusSystemNode::~CBonusSystemNode() { detachFromAll(); @@ -273,14 +234,14 @@ CBonusSystemNode::~CBonusSystemNode() void CBonusSystemNode::attachTo(CBonusSystemNode & parent) { - assert(!vstd::contains(parents, &parent)); - parents.push_back(&parent); + assert(!vstd::contains(parentsToPropagate, &parent)); + parentsToPropagate.push_back(&parent); + + attachToSource(parent); if(!isHypothetic()) { - if(parent.actsAsBonusSourceOnly()) - parent.newRedDescendant(*this); - else + if(!parent.actsAsBonusSourceOnly()) newRedDescendant(parent); parent.newChildAttached(*this); @@ -289,21 +250,35 @@ void CBonusSystemNode::attachTo(CBonusSystemNode & parent) CBonusSystemNode::treeHasChanged(); } -void CBonusSystemNode::detachFrom(CBonusSystemNode & parent) +void CBonusSystemNode::attachToSource(const CBonusSystemNode & parent) { - assert(vstd::contains(parents, &parent)); + assert(!vstd::contains(parentsToInherit, &parent)); + parentsToInherit.push_back(&parent); if(!isHypothetic()) { if(parent.actsAsBonusSourceOnly()) - parent.removedRedDescendant(*this); - else + parent.newRedDescendant(*this); + } + + CBonusSystemNode::treeHasChanged(); +} + +void CBonusSystemNode::detachFrom(CBonusSystemNode & parent) +{ + assert(vstd::contains(parentsToPropagate, &parent)); + + if(!isHypothetic()) + { + if(!parent.actsAsBonusSourceOnly()) removedRedDescendant(parent); } - if (vstd::contains(parents, &parent)) + detachFromSource(parent); + + if (vstd::contains(parentsToPropagate, &parent)) { - parents -= &parent; + parentsToPropagate -= &parent; } else { @@ -318,6 +293,30 @@ void CBonusSystemNode::detachFrom(CBonusSystemNode & parent) CBonusSystemNode::treeHasChanged(); } + +void CBonusSystemNode::detachFromSource(const CBonusSystemNode & parent) +{ + assert(vstd::contains(parentsToInherit, &parent)); + + if(!isHypothetic()) + { + if(parent.actsAsBonusSourceOnly()) + parent.removedRedDescendant(*this); + } + + if (vstd::contains(parentsToInherit, &parent)) + { + parentsToInherit -= &parent; + } + else + { + logBonus->error("Error on Detach. Node %s (nodeType=%d) has not parent %s (nodeType=%d)" + , nodeShortInfo(), nodeType, parent.nodeShortInfo(), parent.nodeType); + } + + CBonusSystemNode::treeHasChanged(); +} + void CBonusSystemNode::removeBonusesRecursive(const CSelector & s) { removeBonuses(s); @@ -356,7 +355,7 @@ void CBonusSystemNode::addNewBonus(const std::shared_ptr& b) void CBonusSystemNode::accumulateBonus(const std::shared_ptr& b) { - auto bonus = exportedBonuses.getFirst(Selector::typeSubtype(b->type, b->subtype)); //only local bonuses are interesting //TODO: what about value type? + auto bonus = exportedBonuses.getFirst(Selector::typeSubtypeValueType(b->type, b->subtype, b->valType)); //only local bonuses are interesting if(bonus) bonus->val += b->val; else @@ -405,8 +404,10 @@ void CBonusSystemNode::propagateBonus(const std::shared_ptr & b, const CB logBonus->trace("#$# %s #propagated to# %s", propagated->Description(), nodeName()); } - FOREACH_RED_CHILD(child) - child->propagateBonus(b, source); + TNodes lchildren; + getRedChildren(lchildren); + for(CBonusSystemNode *pname : lchildren) + pname->propagateBonus(b, source); } void CBonusSystemNode::unpropagateBonus(const std::shared_ptr & b) @@ -417,8 +418,10 @@ void CBonusSystemNode::unpropagateBonus(const std::shared_ptr & b) logBonus->trace("#$# %s #is no longer propagated to# %s", b->Description(), nodeName()); } - FOREACH_RED_CHILD(child) - child->unpropagateBonus(b); + TNodes lchildren; + getRedChildren(lchildren); + for(CBonusSystemNode *pname : lchildren) + pname->unpropagateBonus(b); } void CBonusSystemNode::newChildAttached(CBonusSystemNode & child) @@ -440,13 +443,16 @@ void CBonusSystemNode::childDetached(CBonusSystemNode & child) void CBonusSystemNode::detachFromAll() { - while(!parents.empty()) - detachFrom(*parents.front()); + while(!parentsToPropagate.empty()) + detachFrom(*parentsToPropagate.front()); + + while(!parentsToInherit.empty()) + detachFromSource(*parentsToInherit.front()); } bool CBonusSystemNode::isIndependentNode() const { - return parents.empty() && children.empty(); + return parentsToInherit.empty() && parentsToPropagate.empty() && children.empty(); } std::string CBonusSystemNode::nodeName() const @@ -464,12 +470,13 @@ std::string CBonusSystemNode::nodeShortInfo() const void CBonusSystemNode::deserializationFix() { exportBonuses(); - } -void CBonusSystemNode::getRedParents(TNodes & out) +void CBonusSystemNode::getRedParents(TCNodes & out) const { - FOREACH_PARENT(pname) + TCNodes lparents; + getParents(lparents); + for(const CBonusSystemNode *pname : lparents) { if(pname->actsAsBonusSourceOnly()) { @@ -479,7 +486,7 @@ void CBonusSystemNode::getRedParents(TNodes & out) if(!actsAsBonusSourceOnly()) { - for(CBonusSystemNode *child : children) + for(const CBonusSystemNode *child : children) { out.insert(child); } @@ -488,7 +495,7 @@ void CBonusSystemNode::getRedParents(TNodes & out) void CBonusSystemNode::getRedChildren(TNodes &out) { - FOREACH_PARENT(pname) + for(CBonusSystemNode *pname : parentsToPropagate) { if(!pname->actsAsBonusSourceOnly()) { @@ -505,17 +512,17 @@ void CBonusSystemNode::getRedChildren(TNodes &out) } } -void CBonusSystemNode::newRedDescendant(CBonusSystemNode & descendant) +void CBonusSystemNode::newRedDescendant(CBonusSystemNode & descendant) const { for(const auto & b : exportedBonuses) { if(b->propagator) descendant.propagateBonus(b, *this); } - TNodes redParents; + TCNodes redParents; getRedAncestors(redParents); //get all red parents recursively - for(auto * parent : redParents) + for(const auto * parent : redParents) { for(const auto & b : parent->exportedBonuses) { @@ -525,13 +532,13 @@ void CBonusSystemNode::newRedDescendant(CBonusSystemNode & descendant) } } -void CBonusSystemNode::removedRedDescendant(CBonusSystemNode & descendant) +void CBonusSystemNode::removedRedDescendant(CBonusSystemNode & descendant) const { for(const auto & b : exportedBonuses) if(b->propagator) descendant.unpropagateBonus(b); - TNodes redParents; + TCNodes redParents; getRedAncestors(redParents); //get all red parents recursively for(auto * parent : redParents) @@ -542,14 +549,14 @@ void CBonusSystemNode::removedRedDescendant(CBonusSystemNode & descendant) } } -void CBonusSystemNode::getRedAncestors(TNodes &out) +void CBonusSystemNode::getRedAncestors(TCNodes &out) const { getRedParents(out); - TNodes redParents; + TCNodes redParents; getRedParents(redParents); - for(CBonusSystemNode * parent : redParents) + for(const CBonusSystemNode * parent : redParents) parent->getRedAncestors(out); } @@ -574,9 +581,9 @@ CBonusSystemNode::ENodeTypes CBonusSystemNode::getNodeType() const return nodeType; } -const TNodesVector& CBonusSystemNode::getParentNodes() const +const TCNodesVector& CBonusSystemNode::getParentNodes() const { - return parents; + return parentsToInherit; } void CBonusSystemNode::setNodeType(CBonusSystemNode::ENodeTypes type) @@ -646,4 +653,4 @@ int64_t CBonusSystemNode::getTreeVersion() const return treeChanged; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/CBonusSystemNode.h b/lib/bonuses/CBonusSystemNode.h index b5d4f8f71..9938a1978 100644 --- a/lib/bonuses/CBonusSystemNode.h +++ b/lib/bonuses/CBonusSystemNode.h @@ -19,6 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN using TNodes = std::set; using TCNodes = std::set; using TNodesVector = std::vector; +using TCNodesVector = std::vector; class DLL_LINKAGE CBonusSystemNode : public virtual IBonusBearer, public boost::noncopyable { @@ -33,7 +34,8 @@ private: BonusList bonuses; //wielded bonuses (local or up-propagated here) BonusList exportedBonuses; //bonuses coming from this node (wielded or propagated away) - TNodesVector parents; //parents -> we inherit bonuses from them, we may attach our bonuses to them + TCNodesVector parentsToInherit; // we inherit bonuses from them + TNodesVector parentsToPropagate; // we may attach our bonuses to them TNodesVector children; ENodeTypes nodeType; @@ -54,8 +56,8 @@ private: TConstBonusListPtr getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr) const; std::shared_ptr getUpdatedBonus(const std::shared_ptr & b, const TUpdaterPtr & updater) const; - void getRedParents(TNodes &out); //retrieves list of red parent nodes (nodes bonuses propagate from) - void getRedAncestors(TNodes &out); + void getRedParents(TCNodes &out) const; //retrieves list of red parent nodes (nodes bonuses propagate from) + void getRedAncestors(TCNodes &out) const; void getRedChildren(TNodes &out); void getAllParents(TCNodes & out) const; @@ -66,8 +68,8 @@ private: void unpropagateBonus(const std::shared_ptr & b); bool actsAsBonusSourceOnly() const; - void newRedDescendant(CBonusSystemNode & descendant); //propagation needed - void removedRedDescendant(CBonusSystemNode & descendant); //de-propagation needed + void newRedDescendant(CBonusSystemNode & descendant) const; //propagation needed + void removedRedDescendant(CBonusSystemNode & descendant) const; //de-propagation needed std::string nodeShortInfo() const; @@ -80,21 +82,23 @@ protected: public: explicit CBonusSystemNode(bool isHypotetic = false); explicit CBonusSystemNode(ENodeTypes NodeType); - CBonusSystemNode(CBonusSystemNode && other) noexcept; virtual ~CBonusSystemNode(); void limitBonuses(const BonusList &allBonuses, BonusList &out) const; //out will bo populed with bonuses that are not limited here TBonusListPtr limitBonuses(const BonusList &allBonuses) const; //same as above, returns out by val for convienence TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr, const std::string &cachingStr = "") const override; void getParents(TCNodes &out) const; //retrieves list of parent nodes (nodes to inherit bonuses from), - std::shared_ptr getBonusLocalFirst(const CSelector & selector) const; - //non-const interface - void getParents(TNodes &out); //retrieves list of parent nodes (nodes to inherit bonuses from) - std::shared_ptr getBonusLocalFirst(const CSelector & selector); + /// Returns first bonus matching selector + std::shared_ptr getFirstBonus(const CSelector & selector) const; + + /// Provides write access to first bonus from this node that matches selector + std::shared_ptr getLocalBonus(const CSelector & selector); void attachTo(CBonusSystemNode & parent); + void attachToSource(const CBonusSystemNode & parent); void detachFrom(CBonusSystemNode & parent); + void detachFromSource(const CBonusSystemNode & parent); void detachFromAll(); virtual void addNewBonus(const std::shared_ptr& b); void accumulateBonus(const std::shared_ptr& b); //add value of bonus with same type/subtype or create new @@ -115,7 +119,7 @@ public: const BonusList & getExportedBonusList() const; CBonusSystemNode::ENodeTypes getNodeType() const; void setNodeType(CBonusSystemNode::ENodeTypes type); - const TNodesVector & getParentNodes() const; + const TCNodesVector & getParentNodes() const; static void treeHasChanged(); @@ -126,16 +130,14 @@ public: return PlayerColor::NEUTRAL; } - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { -// h & bonuses; h & nodeType; h & exportedBonuses; BONUS_TREE_DESERIALIZATION_FIX - //h & parents & children; } friend class CBonusProxy; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.cpp b/lib/bonuses/IBonusBearer.cpp index 63ab0b631..a0bfc7087 100644 --- a/lib/bonuses/IBonusBearer.cpp +++ b/lib/bonuses/IBonusBearer.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" -#include "CBonusSystemNode.h" +#include "IBonusBearer.h" #include "BonusList.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index a2461b1e7..e17bf3965 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -24,6 +24,7 @@ #include "../TerrainHandler.h" #include "../constants/StringConstants.h" #include "../battle/BattleInfo.h" +#include "../json/JsonUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -91,7 +92,7 @@ std::string ILimiter::toString() const JsonNode ILimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = toString(); return root; } @@ -114,7 +115,7 @@ CCreatureTypeLimiter::CCreatureTypeLimiter(const CCreature & creature_, bool Inc void CCreatureTypeLimiter::setCreature(const CreatureID & id) { - creature = VLC->creh->objects[id]; + creature = id.toCreature(); } std::string CCreatureTypeLimiter::toString() const @@ -126,11 +127,11 @@ std::string CCreatureTypeLimiter::toString() const JsonNode CCreatureTypeLimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "CREATURE_TYPE_LIMITER"; - root["parameters"].Vector().push_back(JsonUtils::stringNode(creature->getJsonKey())); - root["parameters"].Vector().push_back(JsonUtils::boolNode(includeUpgrades)); + root["parameters"].Vector().emplace_back(creature->getJsonKey()); + root["parameters"].Vector().emplace_back(includeUpgrades); return root; } @@ -198,16 +199,16 @@ std::string HasAnotherBonusLimiter::toString() const JsonNode HasAnotherBonusLimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; std::string typeName = vstd::findKey(bonusNameMap, type); auto sourceTypeName = vstd::findKey(bonusSourceMap, source); root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER"; - root["parameters"].Vector().push_back(JsonUtils::stringNode(typeName)); + root["parameters"].Vector().emplace_back(typeName); if(isSubtypeRelevant) - root["parameters"].Vector().push_back(JsonUtils::stringNode(subtype.toString())); + root["parameters"].Vector().emplace_back(subtype.toString()); if(isSourceRelevant) - root["parameters"].Vector().push_back(JsonUtils::stringNode(sourceTypeName)); + root["parameters"].Vector().emplace_back(sourceTypeName); return root; } @@ -232,11 +233,11 @@ UnitOnHexLimiter::UnitOnHexLimiter(const std::set & applicableHexes): JsonNode UnitOnHexLimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "UNIT_ON_HEXES"; for(const auto & hex : applicableHexes) - root["parameters"].Vector().push_back(JsonUtils::intNode(hex)); + root["parameters"].Vector().emplace_back(hex); return root; } @@ -277,11 +278,11 @@ std::string CreatureTerrainLimiter::toString() const JsonNode CreatureTerrainLimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "CREATURE_TERRAIN_LIMITER"; auto terrainName = VLC->terrainTypeHandler->getById(terrainType)->getJsonKey(); - root["parameters"].Vector().push_back(JsonUtils::stringNode(terrainName)); + root["parameters"].Vector().emplace_back(terrainName); return root; } @@ -317,16 +318,16 @@ ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context) std::string FactionLimiter::toString() const { boost::format fmt("FactionLimiter(faction=%s)"); - fmt % VLC->factions()->getByIndex(faction)->getJsonKey(); + fmt % VLC->factions()->getById(faction)->getJsonKey(); return fmt.str(); } JsonNode FactionLimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "FACTION_LIMITER"; - root["parameters"].Vector().push_back(JsonUtils::stringNode(VLC->factions()->getByIndex(faction)->getJsonKey())); + root["parameters"].Vector().emplace_back(VLC->factions()->getById(faction)->getJsonKey()); return root; } @@ -353,11 +354,11 @@ std::string CreatureLevelLimiter::toString() const JsonNode CreatureLevelLimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "CREATURE_LEVEL_LIMITER"; - root["parameters"].Vector().push_back(JsonUtils::intNode(minLevel)); - root["parameters"].Vector().push_back(JsonUtils::intNode(maxLevel)); + root["parameters"].Vector().emplace_back(minLevel); + root["parameters"].Vector().emplace_back(maxLevel); return root; } @@ -391,10 +392,10 @@ std::string CreatureAlignmentLimiter::toString() const JsonNode CreatureAlignmentLimiter::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "CREATURE_ALIGNMENT_LIMITER"; - root["parameters"].Vector().push_back(JsonUtils::stringNode(GameConstants::ALIGNMENT_NAMES[vstd::to_underlying(alignment)])); + root["parameters"].Vector().emplace_back(GameConstants::ALIGNMENT_NAMES[vstd::to_underlying(alignment)]); return root; } @@ -449,8 +450,8 @@ void AggregateLimiter::add(const TLimiterPtr & limiter) JsonNode AggregateLimiter::toJsonNode() const { - JsonNode result(JsonNode::JsonType::DATA_VECTOR); - result.Vector().push_back(JsonUtils::stringNode(getAggregator())); + JsonNode result; + result.Vector().emplace_back(getAggregator()); for(const auto & l : limiters) result.Vector().push_back(l->toJsonNode()); return result; diff --git a/lib/bonuses/Limiters.h b/lib/bonuses/Limiters.h index 2e0b60126..6487f6cd9 100644 --- a/lib/bonuses/Limiters.h +++ b/lib/bonuses/Limiters.h @@ -38,7 +38,7 @@ public: virtual std::string toString() const; virtual JsonNode toJsonNode() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { } }; @@ -53,7 +53,7 @@ public: void add(const TLimiterPtr & limiter); JsonNode toJsonNode() const override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & limiters; @@ -104,7 +104,7 @@ public: std::string toString() const override; JsonNode toJsonNode() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & creature; @@ -132,7 +132,7 @@ public: std::string toString() const override; JsonNode toJsonNode() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & type; @@ -156,7 +156,7 @@ public: std::string toString() const override; JsonNode toJsonNode() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & terrainType; @@ -175,7 +175,7 @@ public: std::string toString() const override; JsonNode toJsonNode() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & minLevel; @@ -193,7 +193,7 @@ public: std::string toString() const override; JsonNode toJsonNode() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & faction; @@ -210,7 +210,7 @@ public: std::string toString() const override; JsonNode toJsonNode() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & alignment; @@ -225,7 +225,7 @@ public: EDecision limit(const BonusLimitationContext &context) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & owner; @@ -235,13 +235,14 @@ public: class DLL_LINKAGE RankRangeLimiter : public ILimiter //applies to creatures with min <= Rank <= max { public: - ui8 minRank, maxRank; + ui8 minRank; + ui8 maxRank; RankRangeLimiter(); RankRangeLimiter(ui8 Min, ui8 Max = 255); EDecision limit(const BonusLimitationContext &context) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & minRank; @@ -258,7 +259,7 @@ public: EDecision limit(const BonusLimitationContext &context) const override; JsonNode toJsonNode() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & applicableHexes; diff --git a/lib/bonuses/Propagators.h b/lib/bonuses/Propagators.h index 833e6b73d..e9affb097 100644 --- a/lib/bonuses/Propagators.h +++ b/lib/bonuses/Propagators.h @@ -23,7 +23,7 @@ public: virtual bool shouldBeAttached(CBonusSystemNode *dest); virtual CBonusSystemNode::ENodeTypes getPropagatorType() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) {} }; @@ -36,7 +36,7 @@ public: bool shouldBeAttached(CBonusSystemNode *dest) override; CBonusSystemNode::ENodeTypes getPropagatorType() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & nodeType; } diff --git a/lib/bonuses/Updaters.cpp b/lib/bonuses/Updaters.cpp index 0c21733b8..5a41aa047 100644 --- a/lib/bonuses/Updaters.cpp +++ b/lib/bonuses/Updaters.cpp @@ -13,6 +13,7 @@ #include "Updaters.h" #include "Limiters.h" +#include "../json/JsonNode.h" #include "../mapObjects/CGHeroInstance.h" #include "../CStack.h" @@ -38,7 +39,7 @@ std::string IUpdater::toString() const JsonNode IUpdater::toJsonNode() const { - return JsonNode(JsonNode::JsonType::DATA_NULL); + return JsonNode(); } GrowsWithLevelUpdater::GrowsWithLevelUpdater(int valPer20, int stepSize) : valPer20(valPer20), stepSize(stepSize) @@ -54,7 +55,7 @@ std::shared_ptr GrowsWithLevelUpdater::createUpdatedBonus(const std::shar //rounding follows format for HMM3 creature specialty bonus int newVal = (valPer20 * steps + 19) / 20; //return copy of bonus with updated val - std::shared_ptr newBonus = std::make_shared(*b); + auto newBonus = std::make_shared(*b); newBonus->val = newVal; return newBonus; } @@ -68,12 +69,12 @@ std::string GrowsWithLevelUpdater::toString() const JsonNode GrowsWithLevelUpdater::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "GROWS_WITH_LEVEL"; - root["parameters"].Vector().push_back(JsonUtils::intNode(valPer20)); + root["parameters"].Vector().emplace_back(valPer20); if(stepSize > 1) - root["parameters"].Vector().push_back(JsonUtils::intNode(stepSize)); + root["parameters"].Vector().emplace_back(stepSize); return root; } @@ -83,7 +84,7 @@ std::shared_ptr TimesHeroLevelUpdater::createUpdatedBonus(const std::shar if(context.getNodeType() == CBonusSystemNode::HERO) { int level = dynamic_cast(context).level; - std::shared_ptr newBonus = std::make_shared(*b); + auto newBonus = std::make_shared(*b); newBonus->val *= level; return newBonus; } @@ -97,7 +98,7 @@ std::string TimesHeroLevelUpdater::toString() const JsonNode TimesHeroLevelUpdater::toJsonNode() const { - return JsonUtils::stringNode("TIMES_HERO_LEVEL"); + return JsonNode("TIMES_HERO_LEVEL"); } ArmyMovementUpdater::ArmyMovementUpdater(): @@ -140,13 +141,13 @@ std::string ArmyMovementUpdater::toString() const JsonNode ArmyMovementUpdater::toJsonNode() const { - JsonNode root(JsonNode::JsonType::DATA_STRUCT); + JsonNode root; root["type"].String() = "ARMY_MOVEMENT"; - root["parameters"].Vector().push_back(JsonUtils::intNode(base)); - root["parameters"].Vector().push_back(JsonUtils::intNode(divider)); - root["parameters"].Vector().push_back(JsonUtils::intNode(multiplier)); - root["parameters"].Vector().push_back(JsonUtils::intNode(max)); + root["parameters"].Vector().emplace_back(base); + root["parameters"].Vector().emplace_back(divider); + root["parameters"].Vector().emplace_back(multiplier); + root["parameters"].Vector().emplace_back(max); return root; } @@ -155,7 +156,7 @@ std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::sha if(context.getNodeType() == CBonusSystemNode::STACK_INSTANCE) { int level = dynamic_cast(context).getLevel(); - std::shared_ptr newBonus = std::make_shared(*b); + auto newBonus = std::make_shared(*b); newBonus->val *= level; return newBonus; } @@ -167,7 +168,7 @@ std::shared_ptr TimesStackLevelUpdater::createUpdatedBonus(const std::sha if(stack.base == nullptr) { int level = stack.unitType()->getLevel(); - std::shared_ptr newBonus = std::make_shared(*b); + auto newBonus = std::make_shared(*b); newBonus->val *= level; return newBonus; } @@ -182,7 +183,7 @@ std::string TimesStackLevelUpdater::toString() const JsonNode TimesStackLevelUpdater::toJsonNode() const { - return JsonUtils::stringNode("TIMES_STACK_LEVEL"); + return JsonNode("TIMES_STACK_LEVEL"); } std::string OwnerUpdater::toString() const @@ -192,7 +193,7 @@ std::string OwnerUpdater::toString() const JsonNode OwnerUpdater::toJsonNode() const { - return JsonUtils::stringNode("BONUS_OWNER_UPDATER"); + return JsonNode("BONUS_OWNER_UPDATER"); } std::shared_ptr OwnerUpdater::createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const @@ -208,4 +209,4 @@ std::shared_ptr OwnerUpdater::createUpdatedBonus(const std::shared_ptr void serialize(Handler & h, const int version) + template void serialize(Handler & h) { } }; @@ -40,7 +40,7 @@ public: GrowsWithLevelUpdater() = default; GrowsWithLevelUpdater(int valPer20, int stepSize = 1); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & valPer20; @@ -48,34 +48,34 @@ public: } std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; - virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + std::string toString() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE TimesHeroLevelUpdater : public IUpdater { public: - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); } std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; - virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + std::string toString() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE TimesStackLevelUpdater : public IUpdater { public: - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); } std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; - virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + std::string toString() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE ArmyMovementUpdater : public IUpdater @@ -87,7 +87,7 @@ public: si32 max; ArmyMovementUpdater(); ArmyMovementUpdater(int base, int divider, int multiplier, int max); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & base; @@ -97,21 +97,21 @@ public: } std::shared_ptr createUpdatedBonus(const std::shared_ptr & b, const CBonusSystemNode & context) const override; - virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + std::string toString() const override; + JsonNode toJsonNode() const override; }; class DLL_LINKAGE OwnerUpdater : public IUpdater { public: - template void serialize(Handler& h, const int version) + template void serialize(Handler& h) { h & static_cast(*this); } std::shared_ptr createUpdatedBonus(const std::shared_ptr& b, const CBonusSystemNode& context) const override; - virtual std::string toString() const override; - virtual JsonNode toJsonNode() const override; + std::string toString() const override; + JsonNode toJsonNode() const override; }; VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index ad59559df..5aed7fe91 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -46,7 +46,7 @@ void CampaignHandler::readCampaign(Campaign * ret, const std::vector & inpu } else // text format (json) { - JsonNode jsonCampaign((const char*)input.data(), input.size()); + JsonNode jsonCampaign(reinterpret_cast(input.data()), input.size()); readHeaderFromJson(*ret, jsonCampaign, filename, modName, encoding); for(auto & scenario : jsonCampaign["scenarios"].Vector()) @@ -66,7 +66,7 @@ std::unique_ptr CampaignHandler::getHeader( const std::string & name) auto ret = std::make_unique(); auto fileStream = CResourceHandler::get(modName)->load(resourceID); - std::vector cmpgn = getFile(std::move(fileStream), true)[0]; + std::vector cmpgn = getFile(std::move(fileStream), name, true)[0]; readCampaign(ret.get(), cmpgn, resourceID.getName(), modName, encoding); @@ -84,7 +84,7 @@ std::shared_ptr CampaignHandler::getCampaign( const std::string & auto fileStream = CResourceHandler::get(modName)->load(resourceID); - std::vector> files = getFile(std::move(fileStream), false); + std::vector> files = getFile(std::move(fileStream), name, false); readCampaign(ret.get(), files[0], resourceID.getName(), modName, encoding); @@ -124,7 +124,7 @@ static std::string convertMapName(std::string input) return input; } -std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier) +std::string CampaignHandler::readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier) { TextIdentifier stringID( "campaign", convertMapName(filename), identifier); @@ -133,7 +133,7 @@ std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::st if (input.empty()) return ""; - VLC->generaltexth->registerString(modName, stringID, input); + target.getTexts().registerString(modName, stringID, input); return stringID.get(); } @@ -383,8 +383,8 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader ret.version = static_cast(reader.readUInt32()); ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] ret.loadLegacyData(campId); - ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name")); - ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description")); + ret.name.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "name")); + ret.description.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "description")); if (ret.version > CampaignVersion::RoE) ret.difficultyChoosenByPlayer = reader.readInt8(); else @@ -396,7 +396,7 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader ret.encoding = encoding; } -CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CampaignHeader & header) +CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, CampaignHeader & header) { auto prologEpilogReader = [&](const std::string & identifier) -> CampaignScenarioPrologEpilog { @@ -410,7 +410,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader ret.prologVideo = CampaignHandler::prologVideoName(index); ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath(); - ret.prologText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier)); + ret.prologText.appendTextID(readLocalizedString(header, reader, header.filename, header.modName, header.encoding, identifier)); } return ret; }; @@ -428,7 +428,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader } ret.regionColor = reader.readUInt8(); ret.difficulty = reader.readUInt8(); - ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); + ret.regionText.appendTextID(readLocalizedString(header, reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); @@ -578,19 +578,32 @@ CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & rea return ret; } -std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, bool headerOnly) +std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, const std::string & filename, bool headerOnly) { CCompressedStream stream(std::move(file), true); std::vector< std::vector > ret; - do + + try { - std::vector block(stream.getSize()); - stream.read(block.data(), block.size()); - ret.push_back(block); - ret.back().shrink_to_fit(); + do + { + std::vector block(stream.getSize()); + stream.read(block.data(), block.size()); + ret.push_back(block); + ret.back().shrink_to_fit(); + } + while (!headerOnly && stream.getNextBlock()); + } + catch (const DecompressionException & e) + { + // Some campaigns in French version from gog.com have trailing garbage bytes + // For example, slayer.h3c consist from 5 parts: header + 4 maps + // However file also contains ~100 "extra" bytes after those 5 parts are decompressed that do not represent gzip stream + // leading to exception "Incorrect header check" + // Since H3 handles these files correctly, simply log this as warning and proceed + logGlobal->warn("Failed to read file %s. Encountered error during decompression: %s", filename, e.what()); } - while (!headerOnly && stream.getNextBlock()); return ret; } diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index d6e2d0579..cd7332627 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CampaignHandler { - static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); + static std::string readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); static void readCampaign(Campaign * target, const std::vector & stream, std::string filename, std::string modName, std::string encoding); @@ -27,11 +27,11 @@ class DLL_LINKAGE CampaignHandler //parsers for original H3C campaigns static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding); - static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CampaignHeader & header); + static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, CampaignHeader & header); static CampaignTravel readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version); /// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m) /// headerOnly - only header will be decompressed, returned vector wont have any maps - static std::vector> getFile(std::unique_ptr file, bool headerOnly); + static std::vector> getFile(std::unique_ptr file, const std::string & filename, bool headerOnly); static VideoPath prologVideoName(ui8 index); static AudioPath prologMusicName(ui8 index); diff --git a/lib/campaign/CampaignScenarioPrologEpilog.h b/lib/campaign/CampaignScenarioPrologEpilog.h index 64611be70..8bf9a1aa5 100644 --- a/lib/campaign/CampaignScenarioPrologEpilog.h +++ b/lib/campaign/CampaignScenarioPrologEpilog.h @@ -22,7 +22,7 @@ struct DLL_LINKAGE CampaignScenarioPrologEpilog AudioPath prologVoice; MetaString prologText; - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & hasPrologEpilog; h & prologVideo; diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index 082ddf7ee..b3e4fe3a2 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "CampaignState.h" -#include "../JsonNode.h" +#include "../Point.h" #include "../filesystem/ResourcePath.h" #include "../VCMI_Lib.h" #include "../CGeneralTextHandler.h" @@ -73,13 +73,13 @@ ImagePath CampaignRegions::getBackgroundName() const Point CampaignRegions::getPosition(CampaignScenarioID which) const { - auto const & region = regions[static_cast(which)]; + auto const & region = regions[which.getNum()]; return Point(region.xpos, region.ypos); } ImagePath CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const { - auto const & region = regions[static_cast(which)]; + auto const & region = regions[which.getNum()]; static const std::string colors[2][8] = { @@ -169,6 +169,11 @@ const CampaignRegions & CampaignHeader::getRegions() const return campaignRegions; } +TextContainerRegistrable & CampaignHeader::getTexts() +{ + return textContainer; +} + bool CampaignState::isConquered(CampaignScenarioID whichScenario) const { return vstd::contains(mapsConquered, whichScenario); @@ -219,7 +224,7 @@ std::set CampaignState::getReservedHeroes() const const CGHeroInstance * CampaignState::strongestHero(CampaignScenarioID scenarioId, const PlayerColor & owner) const { - std::function isOwned = [owner](const JsonNode & node) + std::function isOwned = [&](const JsonNode & node) { auto * h = CampaignState::crossoverDeserialize(node, nullptr); bool result = h->tempOwner == owner; @@ -267,24 +272,23 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe return a->getHeroStrength() > b->getHeroStrength(); }); - logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast(*currentMap), getFilename(), getNameTranslated()); + logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated()); mapsConquered.push_back(*currentMap); auto reservedHeroes = getReservedHeroes(); for (auto * hero : heroes) { - HeroTypeID heroType(hero->subID); JsonNode node = CampaignState::crossoverSerialize(hero); - if (reservedHeroes.count(heroType)) + if (reservedHeroes.count(hero->getHeroType())) { - logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->subID, hero->getNameTranslated()); - globalHeroPool[heroType] = node; + logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->getHeroType(), hero->getNameTranslated()); + globalHeroPool[hero->getHeroType()] = node; } else { - logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->getHeroType(), hero->getNameTranslated()); scenarioHeroPool[*currentMap].push_back(node); } } @@ -312,7 +316,7 @@ std::optional CampaignState::getBonusID(CampaignScenarioID which) const return chosenCampaignBonuses.at(which); } -std::unique_ptr CampaignState::getMap(CampaignScenarioID scenarioId) const +std::unique_ptr CampaignState::getMap(CampaignScenarioID scenarioId, IGameCallback * cb) { // FIXME: there is certainly better way to handle maps inside campaigns if(scenarioId == CampaignScenarioID::NONE) @@ -321,9 +325,12 @@ std::unique_ptr CampaignState::getMap(CampaignScenarioID scenarioId) const CMapService mapService; std::string scenarioName = getFilename().substr(0, getFilename().find('.')); boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(static_cast(scenarioId)); + scenarioName += ':' + std::to_string(scenarioId.getNum()); const auto & mapContent = mapPieces.find(scenarioId)->second; - return mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding()); + auto result = mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding(), cb); + + mapTranslations[scenarioId] = result->texts; + return result; } std::unique_ptr CampaignState::getMapHeader(CampaignScenarioID scenarioId) const @@ -334,7 +341,7 @@ std::unique_ptr CampaignState::getMapHeader(CampaignScenarioID scena CMapService mapService; std::string scenarioName = getFilename().substr(0, getFilename().find('.')); boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(static_cast(scenarioId)); + scenarioName += ':' + std::to_string(scenarioId.getNum()); const auto & mapContent = mapPieces.find(scenarioId)->second; return mapService.loadMapHeader(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding()); } @@ -351,7 +358,7 @@ std::shared_ptr CampaignState::getMapInfo(CampaignScenarioID scenarioI return mapInfo; } -JsonNode CampaignState::crossoverSerialize(CGHeroInstance * hero) +JsonNode CampaignState::crossoverSerialize(CGHeroInstance * hero) const { JsonNode node; JsonSerializer handler(nullptr, node); @@ -359,10 +366,10 @@ JsonNode CampaignState::crossoverSerialize(CGHeroInstance * hero) return node; } -CGHeroInstance * CampaignState::crossoverDeserialize(const JsonNode & node, CMap * map) +CGHeroInstance * CampaignState::crossoverDeserialize(const JsonNode & node, CMap * map) const { JsonDeserializer handler(nullptr, const_cast(node)); - auto * hero = new CGHeroInstance(); + auto * hero = new CGHeroInstance(map->cb); hero->ID = Obj::HERO; hero->serializeJsonOptions(handler); if (map) diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index cbe133467..ddc1b4451 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -9,9 +9,10 @@ */ #pragma once -#include "../lib/GameConstants.h" -#include "../lib/MetaString.h" -#include "../lib/filesystem/ResourcePath.h" +#include "../GameConstants.h" +#include "../MetaString.h" +#include "../filesystem/ResourcePath.h" +#include "../CGeneralTextHandler.h" #include "CampaignConstants.h" #include "CampaignScenarioPrologEpilog.h" @@ -26,6 +27,7 @@ class CMapHeader; class CMapInfo; class JsonNode; class Point; +class IGameCallback; class DLL_LINKAGE CampaignRegions { @@ -35,9 +37,10 @@ class DLL_LINKAGE CampaignRegions struct DLL_LINKAGE RegionDescription { std::string infix; - int xpos, ypos; + int xpos; + int ypos; - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & infix; h & xpos; @@ -58,7 +61,7 @@ public: ImagePath getSelectedName(CampaignScenarioID which, int color) const; ImagePath getConqueredName(CampaignScenarioID which, int color) const; - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & campPrefix; h & colorSuffixLength; @@ -87,6 +90,8 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable void loadLegacyData(ui8 campId); + TextContainerRegistrable textContainer; + public: bool playerSelectedDifficulty() const; bool formatVCMI() const; @@ -99,8 +104,9 @@ public: AudioPath getMusic() const; const CampaignRegions & getRegions() const; + TextContainerRegistrable & getTexts(); - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & version; h & campaignRegions; @@ -112,6 +118,8 @@ public: h & modName; h & music; h & encoding; + if (h.version >= Handler::Version::RELEASE_143) + h & textContainer; } }; @@ -126,7 +134,7 @@ struct DLL_LINKAGE CampaignBonus bool isBonusForHero() const; - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & type; h & info1; @@ -145,7 +153,7 @@ struct DLL_LINKAGE CampaignTravel bool spells = false; bool artifacts = false; - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & experience; h & primarySkills; @@ -163,7 +171,7 @@ struct DLL_LINKAGE CampaignTravel CampaignStartOptions startOptions = CampaignStartOptions::NONE; //1 - start bonus, 2 - traveling hero, 3 - hero options PlayerColor playerColor = PlayerColor::NEUTRAL; //only for startOptions == 1 - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & whatHeroKeeps; h & monstersKeptByHero; @@ -191,7 +199,7 @@ struct DLL_LINKAGE CampaignScenario void loadPreconditionRegions(ui32 regions); bool isNotVoid() const; - template void serialize(Handler &h, const int formatVersion) + template void serialize(Handler &h) { h & mapName; h & scenarioName; @@ -217,7 +225,7 @@ public: std::set allScenarios() const; int scenariosCount() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & scenarios; @@ -236,6 +244,9 @@ class DLL_LINKAGE CampaignState : public Campaign /// List of all maps completed by player, in order of their completion std::vector mapsConquered; + /// List of previously loaded campaign maps, to prevent translation of transferred hero names getting lost after their original map has been completed + std::map mapTranslations; + std::map > mapPieces; //binary h3ms, scenario number -> map data std::map chosenCampaignBonuses; std::optional currentMap; @@ -270,7 +281,7 @@ public: /// Returns true if all available scenarios have been completed and campaign is finished bool isCampaignFinished() const; - std::unique_ptr getMap(CampaignScenarioID scenarioId) const; + std::unique_ptr getMap(CampaignScenarioID scenarioId, IGameCallback * cb); std::unique_ptr getMapHeader(CampaignScenarioID scenarioId) const; std::shared_ptr getMapInfo(CampaignScenarioID scenarioId) const; @@ -291,12 +302,12 @@ public: /// May return empty JsonNode if such hero was not found const JsonNode & getHeroByType(HeroTypeID heroID) const; - static JsonNode crossoverSerialize(CGHeroInstance * hero); - static CGHeroInstance * crossoverDeserialize(const JsonNode & node, CMap * map); + JsonNode crossoverSerialize(CGHeroInstance * hero) const; + CGHeroInstance * crossoverDeserialize(const JsonNode & node, CMap * map) const; std::string campaignSet; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & scenarioHeroPool; @@ -306,6 +317,8 @@ public: h & currentMap; h & chosenCampaignBonuses; h & campaignSet; + if (h.version >= Handler::Version::CAMPAIGN_MAP_TRANSLATIONS) + h & mapTranslations; } }; diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 95d56a267..d1d2b66c6 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -28,13 +29,17 @@ #include "modding/IdentifierStorage.h" #include "modding/ModScope.h" #include "VCMI_Lib.h" +#include "CHeroHandler.h" #include "CArtHandler.h"//todo: remove #include "CCreatureHandler.h"//todo: remove #include "spells/CSpellHandler.h" //todo: remove #include "CSkillHandler.h"//todo: remove +#include "mapObjectConstructors/AObjectTypeHandler.h" #include "constants/StringConstants.h" #include "CGeneralTextHandler.h" #include "TerrainHandler.h" //TODO: remove +#include "RiverHandler.h" +#include "RoadHandler.h" #include "BattleFieldHandler.h" #include "ObstacleHandler.h" #include "CTownHandler.h" @@ -48,6 +53,9 @@ const QueryID QueryID::NONE(-1); const QueryID QueryID::CLIENT(-2); const HeroTypeID HeroTypeID::NONE(-1); const HeroTypeID HeroTypeID::RANDOM(-2); +const HeroTypeID HeroTypeID::GEM(27); +const HeroTypeID HeroTypeID::SOLMYR(45); + const ObjectInstanceID ObjectInstanceID::NONE(-1); const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER(-2); @@ -117,17 +125,27 @@ namespace GameConstants #endif } +int32_t IdentifierBase::resolveIdentifier(const std::string & entityType, const std::string identifier) +{ + if (identifier.empty()) + return -1; + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType, identifier); + + if (rawId) + return rawId.value(); + throw IdentifierResolutionException(identifier); +} + si32 HeroClassID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); - if(rawId) - return rawId.value(); - else - return -1; + return resolveIdentifier("heroClass", identifier); } std::string HeroClassID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->heroClasses()->getByIndex(index)->getJsonKey(); } @@ -136,6 +154,16 @@ std::string HeroClassID::entityType() return "heroClass"; } +const CHeroClass * HeroClassID::toHeroClass() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const HeroClass * HeroClassID::toEntity(const Services * services) const +{ + return services->heroClasses()->getByIndex(num); +} + si32 ObjectInstanceID::decode(const std::string & identifier) { return std::stoi(identifier); @@ -156,31 +184,68 @@ std::string CampaignScenarioID::encode(const si32 index) return std::to_string(index); } -std::string Obj::encode(int32_t index) +std::string MapObjectID::encode(int32_t index) { - return VLC->objtypeh->getObjectHandlerName(index); + if (index == -1) + return ""; + return VLC->objtypeh->getJsonKey(MapObjectID(index)); } -si32 Obj::decode(const std::string & identifier) +si32 MapObjectID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); - if(rawId) - return rawId.value(); - else - return -1; + return resolveIdentifier("object", identifier); +} + +std::string MapObjectSubID::encode(MapObjectID primaryID, int32_t index) +{ + if (index == -1) + return ""; + + if(primaryID == Obj::PRISON || primaryID == Obj::HERO) + return HeroTypeID::encode(index); + + if (primaryID == Obj::SPELL_SCROLL) + return SpellID::encode(index); + + return VLC->objtypeh->getHandlerFor(primaryID, index)->getJsonKey(); +} + +si32 MapObjectSubID::decode(MapObjectID primaryID, const std::string & identifier) +{ + if(primaryID == Obj::PRISON || primaryID == Obj::HERO) + return HeroTypeID::decode(identifier); + + if (primaryID == Obj::SPELL_SCROLL) + return SpellID::decode(identifier); + + return resolveIdentifier(VLC->objtypeh->getJsonKey(primaryID), identifier); +} + +std::string BoatId::encode(int32_t index) +{ + if (index == -1) + return ""; + return VLC->objtypeh->getHandlerFor(MapObjectID::BOAT, index)->getJsonKey(); +} + +si32 BoatId::decode(const std::string & identifier) +{ + return resolveIdentifier("core:boat", identifier); } si32 HeroTypeID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); - if(rawId) - return rawId.value(); - else - return -1; + if (identifier == "random") + return -2; + return resolveIdentifier("hero", identifier); } std::string HeroTypeID::encode(const si32 index) { + if (index == -1) + return ""; + if (index == -2) + return "random"; return VLC->heroTypes()->getByIndex(index)->getJsonKey(); } @@ -191,25 +256,23 @@ std::string HeroTypeID::entityType() const CArtifact * ArtifactIDBase::toArtifact() const { - return dynamic_cast(toArtifact(VLC->artifacts())); + return dynamic_cast(toEntity(VLC)); } -const Artifact * ArtifactIDBase::toArtifact(const ArtifactService * service) const +const Artifact * ArtifactIDBase::toEntity(const Services * services) const { - return service->getByIndex(num); + return services->artifacts()->getByIndex(num); } si32 ArtifactID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); - if(rawId) - return rawId.value(); - else - return -1; + return resolveIdentifier("artifact", identifier); } std::string ArtifactID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->artifacts()->getByIndex(index)->getJsonKey(); } @@ -220,39 +283,50 @@ std::string ArtifactID::entityType() si32 SecondarySkill::decode(const std::string& identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "secondarySkill", identifier); - if(rawId) - return rawId.value(); - else - return -1; + return resolveIdentifier("secondarySkill", identifier); } std::string SecondarySkill::encode(const si32 index) { + if (index == -1) + return ""; return VLC->skills()->getById(SecondarySkill(index))->getJsonKey(); } +const CSkill * SecondarySkill::toSkill() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const Skill * SecondarySkill::toEntity(const Services * services) const +{ + return services->skills()->getByIndex(num); +} + const CCreature * CreatureIDBase::toCreature() const { - return VLC->creh->objects.at(num); + return dynamic_cast(toEntity(VLC)); } -const Creature * CreatureIDBase::toCreature(const CreatureService * creatures) const +const Creature * CreatureIDBase::toEntity(const Services * services) const +{ + return toEntity(services->creatures()); +} + +const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) const { return creatures->getByIndex(num); } si32 CreatureID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); - if(rawId) - return rawId.value(); - else - return -1; + return resolveIdentifier("creature", identifier); } std::string CreatureID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); } @@ -263,45 +337,49 @@ std::string CreatureID::entityType() const CSpell * SpellIDBase::toSpell() const { - if(num < 0 || num >= VLC->spellh->objects.size()) - { - logGlobal->error("Unable to get spell of invalid ID %d", static_cast(num)); - return nullptr; - } - return VLC->spellh->objects[num]; + return dynamic_cast(toEntity(VLC)); } -const spells::Spell * SpellIDBase::toSpell(const spells::Service * service) const +const spells::Spell * SpellIDBase::toEntity(const Services * services) const +{ + return toEntity(services->spells()); +} + +const spells::Spell * SpellIDBase::toEntity(const spells::Service * service) const { return service->getByIndex(num); } +const CHero * HeroTypeID::toHeroType() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const HeroType * HeroTypeID::toEntity(const Services * services) const +{ + return services->heroTypes()->getByIndex(num); +} + si32 SpellID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - if(rawId) - return rawId.value(); - else - return -1; + return resolveIdentifier("spell", identifier); } std::string SpellID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->spells()->getByIndex(index)->getJsonKey(); } si32 BattleField::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - if(rawId) - return rawId.value(); - else - return -1; + return resolveIdentifier("battlefield", identifier); } std::string BattleField::encode(const si32 index) { - return VLC->spells()->getByIndex(index)->getJsonKey(); + return VLC->battlefields()->getByIndex(index)->getJsonKey(); } std::string SpellID::entityType() @@ -350,7 +428,7 @@ std::string PlayerColor::entityType() si32 PrimarySkill::decode(const std::string& identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string PrimarySkill::encode(const si32 index) @@ -365,15 +443,13 @@ std::string PrimarySkill::entityType() si32 FactionID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else - return FactionID::DEFAULT; + return resolveIdentifier(entityType(), identifier); } std::string FactionID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->factions()->getByIndex(index)->getJsonKey(); } @@ -382,17 +458,30 @@ std::string FactionID::entityType() return "faction"; } +const CFaction * FactionID::toFaction() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const Faction * FactionID::toEntity(const Services * service) const +{ + return service->factions()->getByIndex(num); +} + si32 TerrainId::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else - return static_cast(TerrainId::NONE); + if (identifier == "native") + return TerrainId::NATIVE_TERRAIN; + + return resolveIdentifier(entityType(), identifier); } std::string TerrainId::encode(const si32 index) { + if (index == TerrainId::NONE) + return ""; + if (index == TerrainId::NATIVE_TERRAIN) + return "native"; return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); } @@ -401,6 +490,61 @@ std::string TerrainId::entityType() return "terrain"; } +si32 RoadId::decode(const std::string & identifier) +{ + if (identifier.empty()) + return RoadId::NO_ROAD.getNum(); + + return resolveIdentifier(entityType(), identifier); +} + +std::string RoadId::encode(const si32 index) +{ + if (index == RoadId::NO_ROAD.getNum()) + return ""; + return VLC->roadTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string RoadId::entityType() +{ + return "road"; +} + +si32 RiverId::decode(const std::string & identifier) +{ + if (identifier.empty()) + return RiverId::NO_RIVER.getNum(); + + return resolveIdentifier(entityType(), identifier); +} + +std::string RiverId::encode(const si32 index) +{ + if (index == RiverId::NO_RIVER.getNum()) + return ""; + return VLC->riverTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string RiverId::entityType() +{ + return "river"; +} + +const TerrainType * TerrainId::toEntity(const Services * service) const +{ + return VLC->terrainTypeHandler->getByIndex(num); +} + +const RoadType * RoadId::toEntity(const Services * service) const +{ + return VLC->roadTypeHandler->getByIndex(num); +} + +const RiverType * RiverId::toEntity(const Services * service) const +{ + return VLC->riverTypeHandler->getByIndex(num); +} + const BattleField BattleField::NONE; const BattleFieldInfo * BattleField::getInfo() const @@ -415,7 +559,7 @@ const ObstacleInfo * Obstacle::getInfo() const si32 SpellSchool::decode(const std::string & identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string SpellSchool::encode(const si32 index) @@ -433,7 +577,7 @@ std::string SpellSchool::entityType() si32 GameResID::decode(const std::string & identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string GameResID::encode(const si32 index) @@ -458,9 +602,34 @@ std::string GameResID::entityType() return "resource"; } +const std::array & GameResID::ALL_RESOURCES() +{ + static const std::array allResources = { + GameResID(WOOD), + GameResID(MERCURY), + GameResID(ORE), + GameResID(SULFUR), + GameResID(CRYSTAL), + GameResID(GEMS), + GameResID(GOLD) + }; + + return allResources; +} + std::string SecondarySkill::entityType() { return "secondarySkill"; } +std::string BuildingID::encode(int32_t index) +{ + return std::to_string(index); +} + +si32 BuildingID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index d074b0f8a..387994014 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -14,10 +14,22 @@ VCMI_LIB_NAMESPACE_BEGIN +class Services; class Artifact; class ArtifactService; class Creature; class CreatureService; +class HeroType; +class CHero; +class CHeroClass; +class HeroClass; +class HeroTypeService; +class CFaction; +class Faction; +class Skill; +class RoadType; +class RiverType; +class TerrainType; namespace spells { @@ -34,160 +46,65 @@ class CSkill; class CGameInfoCallback; class CNonConstInfoCallback; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons -template -class Identifier : public IdentifierBase +class ArtifactInstanceID : public StaticIdentifier { - using BaseClass = IdentifierBase; public: - constexpr Identifier() - {} - - explicit constexpr Identifier(int32_t value): - IdentifierBase(value) - {} - - constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } - constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } - constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } - constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } - constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } - constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } - - constexpr FinalClass & operator++() - { - ++BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass & operator--() - { - --BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass operator--(int) - { - FinalClass ret(num); - --BaseClass::num; - return ret; - } - - constexpr FinalClass operator++(int) - { - FinalClass ret(num); - ++BaseClass::num; - return ret; - } + using StaticIdentifier::StaticIdentifier; }; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -template -class IdentifierWithEnum : public BaseClass -{ - using EnumType = typename BaseClass::Type; - - static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); -public: - constexpr EnumType toEnum() const - { - return static_cast(BaseClass::num); - } - - constexpr IdentifierWithEnum(const EnumType & enumValue) - { - BaseClass::num = static_cast(enumValue); - } - - constexpr IdentifierWithEnum(int32_t _num = -1) - { - BaseClass::num = _num; - } - - constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } - constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } - constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } - constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } - constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } - constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } - - constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } - constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } - constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } - constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } - constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } - constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } - - constexpr FinalClass & operator++() - { - ++BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass operator++(int) - { - FinalClass ret(BaseClass::num); - ++BaseClass::num; - return ret; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class ArtifactInstanceID : public Identifier +class QueryID : public StaticIdentifier { public: - using Identifier::Identifier; -}; - -class QueryID : public Identifier -{ -public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const QueryID NONE; DLL_LINKAGE static const QueryID CLIENT; }; -class BattleID : public Identifier +class BattleID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const BattleID NONE; }; -class DLL_LINKAGE ObjectInstanceID : public Identifier +class DLL_LINKAGE ObjectInstanceID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static const ObjectInstanceID NONE; static si32 decode(const std::string & identifier); static std::string encode(const si32 index); }; -class HeroClassID : public Identifier +class HeroClassID : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; ///json serialization helpers DLL_LINKAGE static si32 decode(const std::string & identifier); DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); + + const CHeroClass * toHeroClass() const; + const HeroClass * toEntity(const Services * services) const; }; -class HeroTypeID : public Identifier +class DLL_LINKAGE HeroTypeID : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); - DLL_LINKAGE static const HeroTypeID NONE; - DLL_LINKAGE static const HeroTypeID RANDOM; + const CHero * toHeroType() const; + const HeroType * toEntity(const Services * services) const; + + static const HeroTypeID NONE; + static const HeroTypeID RANDOM; + static const HeroTypeID GEM; // aka Gem, Sorceress in campaign + static const HeroTypeID SOLMYR; // aka Young Yog in campaigns bool isValid() const { @@ -195,10 +112,10 @@ public: } }; -class SlotID : public Identifier +class SlotID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; /// +class DLL_LINKAGE PlayerColor : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; enum EPlayerColor { @@ -237,18 +154,18 @@ public: static std::string entityType(); }; -class TeamID : public Identifier +class TeamID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const TeamID NO_TEAM; }; -class TeleportChannelID : public Identifier +class TeleportChannelID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; }; class SecondarySkillBase : public IdentifierBase @@ -290,19 +207,22 @@ public: static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); }; -class SecondarySkill : public IdentifierWithEnum +class DLL_LINKAGE SecondarySkill : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; static std::string entityType(); static si32 decode(const std::string& identifier); static std::string encode(const si32 index); + + const CSkill * toSkill() const; + const Skill * toEntity(const Services * services) const; }; -class DLL_LINKAGE PrimarySkill : public Identifier +class DLL_LINKAGE PrimarySkill : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static const PrimarySkill NONE; static const PrimarySkill ATTACK; @@ -320,10 +240,10 @@ public: static std::string entityType(); }; -class DLL_LINKAGE FactionID : public Identifier +class DLL_LINKAGE FactionID : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; static const FactionID NONE; static const FactionID DEFAULT; @@ -342,6 +262,8 @@ public: static si32 decode(const std::string& identifier); static std::string encode(const si32 index); + const CFaction * toFaction() const; + const Faction * toEntity(const Services * service) const; static std::string entityType(); bool isValid() const @@ -397,17 +319,27 @@ public: } }; -class BuildingID : public IdentifierWithEnum +class DLL_LINKAGE BuildingID : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); + static BuildingID HALL_LEVEL(unsigned int level) + { + assert(level < 4); + return BuildingID(Type::VILLAGE_HALL + level); + } + static BuildingID FORT_LEVEL(unsigned int level) + { + assert(level < 3); + return BuildingID(Type::FORT + level); + } + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); }; -class ObjBase : public IdentifierBase +class MapObjectBaseID : public IdentifierBase { public: enum Type @@ -552,36 +484,98 @@ public: }; }; -class DLL_LINKAGE Obj : public IdentifierWithEnum +class DLL_LINKAGE MapObjectID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; static std::string encode(int32_t index); static si32 decode(const std::string & identifier); + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } }; -class DLL_LINKAGE RoadId : public Identifier +class DLL_LINKAGE MapObjectSubID : public Identifier { public: - using Identifier::Identifier; + constexpr MapObjectSubID(const IdentifierBase & value): + Identifier(value.getNum()) + {} + constexpr MapObjectSubID(int32_t value = -1): + Identifier(value) + {} + + MapObjectSubID & operator =(int32_t value) + { + this->num = value; + return *this; + } + + MapObjectSubID & operator =(const IdentifierBase & value) + { + this->num = value.getNum(); + return *this; + } + + static si32 decode(MapObjectID primaryID, const std::string & identifier); + static std::string encode(MapObjectID primaryID, si32 index); + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } + + template + void serializeIdentifier(Handler &h, const MapObjectID & primaryID) + { + std::string secondaryStringID; + + if (h.saving) + secondaryStringID = encode(primaryID, num); + + h & secondaryStringID; + + if (!h.saving) + num = decode(primaryID, secondaryStringID); + } +}; + +class DLL_LINKAGE RoadId : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); static const RoadId NO_ROAD; static const RoadId DIRT_ROAD; static const RoadId GRAVEL_ROAD; static const RoadId COBBLESTONE_ROAD; + + const RoadType * toEntity(const Services * service) const; }; -class DLL_LINKAGE RiverId : public Identifier +class DLL_LINKAGE RiverId : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); static const RiverId NO_RIVER; static const RiverId WATER_RIVER; static const RiverId ICY_RIVER; static const RiverId MUD_RIVER; static const RiverId LAVA_RIVER; + + const RiverType * toEntity(const Services * service) const; }; class DLL_LINKAGE EPathfindingLayerBase : public IdentifierBase @@ -593,10 +587,10 @@ public: }; }; -class EPathfindingLayer : public IdentifierWithEnum +class EPathfindingLayer : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; }; class ArtifactPositionBase : public IdentifierBase @@ -607,29 +601,41 @@ public: TRANSITION_POS = -3, FIRST_AVAILABLE = -2, PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack + + // Hero HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 RIGHT_RING, LEFT_RING, FEET, //8 MISC1, MISC2, MISC3, MISC4, //12 MACH1, MACH2, MACH3, MACH4, //16 SPELLBOOK, MISC5, //18 - AFTER_LAST, - //cres + BACKPACK_START = 19, + + // Creatures CREATURE_SLOT = 0, - COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST, + + // Commander + COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, - BACKPACK_START = 19 + // Altar + ALTAR = BACKPACK_START }; - static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots"); + static_assert(MISC5 < BACKPACK_START, "incorrect number of artifact slots"); DLL_LINKAGE static si32 decode(const std::string & identifier); DLL_LINKAGE static std::string encode(const si32 index); }; -class ArtifactPosition : public IdentifierWithEnum +class ArtifactPosition : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } }; class ArtifactIDBase : public IdentifierBase @@ -647,19 +653,20 @@ public: FIRST_AID_TENT = 6, VIAL_OF_DRAGON_BLOOD = 127, ARMAGEDDONS_BLADE = 128, + ANGELIC_ALLIANCE = 129, TITANS_THUNDER = 135, ART_SELECTION = 144, ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 }; DLL_LINKAGE const CArtifact * toArtifact() const; - DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; + DLL_LINKAGE const Artifact * toEntity(const Services * service) const; }; -class ArtifactID : public IdentifierWithEnum +class ArtifactID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; ///json serialization helpers DLL_LINKAGE static si32 decode(const std::string & identifier); @@ -675,6 +682,7 @@ public: NONE = -1, ARCHER = 2, // for debug / fallback IMP = 42, // for Deity of Fire + FAMILIAR = 43, // for Deity of Fire SKELETON = 56, // for Skeleton Transformer BONE_DRAGON = 68, // for Skeleton Transformer TROGLODYTES = 70, // for Abandoned Mine @@ -685,6 +693,7 @@ public: FIRE_ELEMENTAL = 114, // for tests PSYCHIC_ELEMENTAL = 120, // for hardcoded ability MAGIC_ELEMENTAL = 121, // for hardcoded ability + AZURE_DRAGON = 132, CATAPULT = 145, BALLISTA = 146, FIRST_AID_TENT = 147, @@ -693,13 +702,14 @@ public: }; DLL_LINKAGE const CCreature * toCreature() const; - DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; + DLL_LINKAGE const Creature * toEntity(const Services * services) const; + DLL_LINKAGE const Creature * toEntity(const CreatureService * creatures) const; }; -class DLL_LINKAGE CreatureID : public IdentifierWithEnum +class DLL_LINKAGE CreatureID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; ///json serialization helpers static si32 decode(const std::string & identifier); @@ -729,7 +739,7 @@ public: DIMENSION_DOOR = 8, TOWN_PORTAL = 9, - // Combar spells + // Combat spells QUICKSAND = 10, LAND_MINE = 11, FORCE_FIELD = 12, @@ -811,13 +821,14 @@ public: }; const CSpell * toSpell() const; //deprecated - const spells::Spell * toSpell(const spells::Service * service) const; + const spells::Spell * toEntity(const Services * service) const; + const spells::Spell * toEntity(const spells::Service * service) const; }; -class DLL_LINKAGE SpellID : public IdentifierWithEnum +class DLL_LINKAGE SpellID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; ///json serialization helpers static si32 decode(const std::string & identifier); @@ -826,10 +837,10 @@ public: }; class BattleFieldInfo; -class DLL_LINKAGE BattleField : public Identifier +class DLL_LINKAGE BattleField : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; static const BattleField NONE; const BattleFieldInfo * getInfo() const; @@ -838,10 +849,13 @@ public: static std::string encode(const si32 index); }; -class DLL_LINKAGE BoatId : public Identifier +class DLL_LINKAGE BoatId : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static const BoatId NONE; static const BoatId NECROPOLIS; @@ -872,28 +886,29 @@ public: }; }; -class TerrainId : public IdentifierWithEnum +class DLL_LINKAGE TerrainId : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); + const TerrainType * toEntity(const Services * service) const; }; class ObstacleInfo; -class Obstacle : public Identifier +class Obstacle : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; DLL_LINKAGE const ObstacleInfo * getInfo() const; }; -class DLL_LINKAGE SpellSchool : public Identifier +class DLL_LINKAGE SpellSchool : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static const SpellSchool ANY; static const SpellSchool AIR; @@ -926,17 +941,19 @@ public: }; }; -class DLL_LINKAGE GameResID : public IdentifierWithEnum +class DLL_LINKAGE GameResID : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; static si32 decode(const std::string & identifier); static std::string encode(const si32 index); static std::string entityType(); + + static const std::array & ALL_RESOURCES(); }; -class BuildingTypeUniqueID : public Identifier +class DLL_LINKAGE BuildingTypeUniqueID : public Identifier { public: BuildingTypeUniqueID(FactionID faction, BuildingID building ); @@ -948,12 +965,25 @@ public: FactionID getFaction() const; using Identifier::Identifier; + + template + void serialize(Handler & h) + { + FactionID faction = getFaction(); + BuildingID building = getBuilding(); + + h & faction; + h & building; + + if (!h.saving) + *this = BuildingTypeUniqueID(faction, building); + } }; -class DLL_LINKAGE CampaignScenarioID : public Identifier +class DLL_LINKAGE CampaignScenarioID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static si32 decode(const std::string & identifier); static std::string encode(int32_t index); @@ -963,6 +993,7 @@ public: // Deprecated // TODO: remove +using Obj = MapObjectID; using ETownType = FactionID; using EGameResID = GameResID; using River = RiverId; diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index e358ed202..9c7b93f0e 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -64,10 +64,10 @@ enum class EMarketMode : int8_t enum class EAiTactic : int8_t { NONE = -1, - RANDOM, - WARRIOR, - BUILDER, - EXPLORER + RANDOM = 0, + WARRIOR = 1, + BUILDER = 2, + EXPLORER = 3 }; enum class EBuildingState : int8_t @@ -246,4 +246,10 @@ enum class ETileVisibility : int8_t // Fog of war change REVEALED }; +enum class EArmyFormation : int8_t +{ + LOOSE, + TIGHT +}; + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index 5ad0a8dd3..858900658 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -9,6 +9,19 @@ */ #pragma once +VCMI_LIB_NAMESPACE_BEGIN + +class IdentifierResolutionException : public std::runtime_error +{ +public: + const std::string identifierName; + + IdentifierResolutionException(const std::string & identifierName ) + : std::runtime_error("Failed to resolve identifier " + identifierName) + , identifierName(identifierName) + {} +}; + class IdentifierBase { protected: @@ -21,6 +34,11 @@ protected: {} ~IdentifierBase() = default; + + /// Attempts to resolve identifier using provided entity type + /// Returns resolved numeric identifier + /// Throws IdentifierResolutionException on failure + static int32_t resolveIdentifier(const std::string & entityType, const std::string identifier); public: int32_t num; @@ -34,6 +52,11 @@ public: num = value; } + constexpr bool hasValue() const + { + return num >= 0; + } + struct hash { size_t operator()(const IdentifierBase & id) const @@ -42,11 +65,6 @@ public: } }; - template void serialize(Handler &h, const int version) - { - h & num; - } - constexpr void advance(int change) { num += change; @@ -62,3 +80,180 @@ public: return os << dt.num; } }; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons +template +class Identifier : public IdentifierBase +{ + using BaseClass = IdentifierBase; +public: + constexpr Identifier() + {} + + explicit constexpr Identifier(int32_t value): + IdentifierBase(value) + {} + + constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass & operator--() + { + --BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator--(int) + { + FinalClass ret(num); + --BaseClass::num; + return ret; + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class IdentifierWithEnum : public BaseClass +{ + using EnumType = typename BaseClass::Type; + + static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); +public: + constexpr EnumType toEnum() const + { + return static_cast(BaseClass::num); + } + + constexpr IdentifierWithEnum(const EnumType & enumValue) + { + BaseClass::num = static_cast(enumValue); + } + + constexpr IdentifierWithEnum(int32_t _num = -1) + { + BaseClass::num = _num; + } + + constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } + constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } + constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } + constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } + constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } + constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } + + constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(BaseClass::num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class EntityIdentifier : public Identifier +{ +public: + using Identifier::Identifier; + + template + void serialize(Handler &h) + { + auto * finalClass = static_cast(this); + std::string value; + + if (h.saving) + value = FinalClass::encode(finalClass->num); + + h & value; + + if (!h.saving) + finalClass->num = FinalClass::decode(value); + } +}; + +template +class EntityIdentifierWithEnum : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + template + void serialize(Handler &h) + { + auto * finalClass = static_cast(this); + std::string value; + + if (h.saving) + value = FinalClass::encode(finalClass->num); + + h & value; + + if (!h.saving) + finalClass->num = FinalClass::decode(value); + } +}; + +template +class StaticIdentifier : public Identifier +{ +public: + using Identifier::Identifier; + + template + void serialize(Handler &h) + { + auto * finalClass = static_cast(this); + h & finalClass->num; + } +}; + +template +class StaticIdentifierWithEnum : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + template + void serialize(Handler &h) + { + auto * finalClass = static_cast(this); + h & finalClass->num; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index 9d61935ff..fc6def5ab 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -50,6 +50,8 @@ namespace GameConstants constexpr int CREATURES_COUNT = 197; constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement + constexpr int64_t PLAYER_RESOURCES_CAP = 1000 * 1000 * 1000; + constexpr int ALTAR_ARTIFACTS_SLOTS = 22; } VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/VariantIdentifier.h b/lib/constants/VariantIdentifier.h index 6b0394f65..a49be1cde 100644 --- a/lib/constants/VariantIdentifier.h +++ b/lib/constants/VariantIdentifier.h @@ -13,13 +13,14 @@ VCMI_LIB_NAMESPACE_BEGIN -/// This class represents field that may contain value of multiple different identifer types +/// This class represents field that may contain value of multiple different identifier types template -class DLL_LINKAGE VariantIdentifier +class VariantIdentifier { - std::variant value; -public: + using Type = std::variant; + Type value; +public: VariantIdentifier() {} @@ -58,7 +59,7 @@ public: return IdentifierType(); } - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & value; } diff --git a/lib/events/ApplyDamage.cpp b/lib/events/ApplyDamage.cpp index 2056f0c2d..82e499cd9 100644 --- a/lib/events/ApplyDamage.cpp +++ b/lib/events/ApplyDamage.cpp @@ -21,7 +21,7 @@ namespace events SubscriptionRegistry * ApplyDamage::getRegistry() { - static std::unique_ptr> Instance = std::make_unique>(); + static auto Instance = std::make_unique>(); return Instance.get(); } diff --git a/lib/events/GameResumed.cpp b/lib/events/GameResumed.cpp index eb7105c7c..41384f552 100644 --- a/lib/events/GameResumed.cpp +++ b/lib/events/GameResumed.cpp @@ -20,7 +20,7 @@ namespace events SubscriptionRegistry * GameResumed::getRegistry() { - static std::unique_ptr> Instance = std::make_unique>(); + static auto Instance = std::make_unique>(); return Instance.get(); } diff --git a/lib/events/ObjectVisitEnded.cpp b/lib/events/ObjectVisitEnded.cpp index 3bde4c19d..16290488a 100644 --- a/lib/events/ObjectVisitEnded.cpp +++ b/lib/events/ObjectVisitEnded.cpp @@ -21,7 +21,7 @@ namespace events SubscriptionRegistry * ObjectVisitEnded::getRegistry() { - static std::unique_ptr Instance = std::make_unique(); + static auto Instance = std::make_unique(); return Instance.get(); } diff --git a/lib/events/ObjectVisitStarted.cpp b/lib/events/ObjectVisitStarted.cpp index c97c5f7df..e8e68171b 100644 --- a/lib/events/ObjectVisitStarted.cpp +++ b/lib/events/ObjectVisitStarted.cpp @@ -21,7 +21,7 @@ namespace events SubscriptionRegistry * ObjectVisitStarted::getRegistry() { - static std::unique_ptr Instance = std::make_unique(); + static auto Instance = std::make_unique(); return Instance.get(); } diff --git a/lib/events/PlayerGotTurn.cpp b/lib/events/PlayerGotTurn.cpp index ccbffecc5..8e69289fc 100644 --- a/lib/events/PlayerGotTurn.cpp +++ b/lib/events/PlayerGotTurn.cpp @@ -20,7 +20,7 @@ namespace events SubscriptionRegistry * PlayerGotTurn::getRegistry() { - static std::unique_ptr> Instance = std::make_unique>(); + static auto Instance = std::make_unique>(); return Instance.get(); } diff --git a/lib/events/TurnStarted.cpp b/lib/events/TurnStarted.cpp index b3888d699..e71b0f608 100644 --- a/lib/events/TurnStarted.cpp +++ b/lib/events/TurnStarted.cpp @@ -20,7 +20,7 @@ namespace events SubscriptionRegistry * TurnStarted::getRegistry() { - static std::unique_ptr> Instance = std::make_unique>(); + static auto Instance = std::make_unique>(); return Instance.get(); } diff --git a/lib/filesystem/AdapterLoaders.cpp b/lib/filesystem/AdapterLoaders.cpp index 8bdf8abab..7ad58b155 100644 --- a/lib/filesystem/AdapterLoaders.cpp +++ b/lib/filesystem/AdapterLoaders.cpp @@ -10,8 +10,8 @@ #include "StdInc.h" #include "AdapterLoaders.h" -#include "../JsonNode.h" #include "Filesystem.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/filesystem/CCompressedStream.cpp b/lib/filesystem/CCompressedStream.cpp index 72f1a697a..1b9306a2d 100644 --- a/lib/filesystem/CCompressedStream.cpp +++ b/lib/filesystem/CCompressedStream.cpp @@ -162,9 +162,9 @@ si64 CCompressedStream::readMore(ui8 *data, si64 size) break; default: if (inflateState->msg == nullptr) - throw std::runtime_error("Decompression error. Return code was " + std::to_string(ret)); + throw DecompressionException("Error code " + std::to_string(ret)); else - throw std::runtime_error(std::string("Decompression error: ") + inflateState->msg); + throw DecompressionException(inflateState->msg); } } while (!endLoop && inflateState->avail_out != 0 ); diff --git a/lib/filesystem/CCompressedStream.h b/lib/filesystem/CCompressedStream.h index 5c5930e67..2fad04ebb 100644 --- a/lib/filesystem/CCompressedStream.h +++ b/lib/filesystem/CCompressedStream.h @@ -15,6 +15,12 @@ struct z_stream_s; VCMI_LIB_NAMESPACE_BEGIN +class DecompressionException : public std::runtime_error +{ +public: + using runtime_error::runtime_error; +}; + /// Abstract class that provides buffer for one-directional input streams (e.g. compressed data) /// Used for zip archives support and in .lod deflate compression class CBufferedStream : public CInputStream diff --git a/lib/filesystem/CZipLoader.cpp b/lib/filesystem/CZipLoader.cpp index e6592f7bd..60e28972e 100644 --- a/lib/filesystem/CZipLoader.cpp +++ b/lib/filesystem/CZipLoader.cpp @@ -150,22 +150,11 @@ static bool extractCurrent(unzFile file, std::ostream & where) return false; } -std::vector ZipArchive::listFiles(const boost::filesystem::path & filename) +std::vector ZipArchive::listFiles() { std::vector ret; - CDefaultIOApi zipAPI; - auto zipStructure = zipAPI.getApiStructure(); - - unzFile file = unzOpen2_64(filename.c_str(), &zipStructure); - - if (file == nullptr) - { - logGlobal->error("Failed to open file '%s'! Unable to list files!", filename.string()); - return {}; - } - - int result = unzGoToFirstFile(file); + int result = unzGoToFirstFile(archive); if (result == UNZ_OK) { @@ -174,73 +163,66 @@ std::vector ZipArchive::listFiles(const boost::filesystem::path & f unz_file_info64 info; std::vector zipFilename; - unzGetCurrentFileInfo64 (file, &info, nullptr, 0, nullptr, 0, nullptr, 0); + unzGetCurrentFileInfo64 (archive, &info, nullptr, 0, nullptr, 0, nullptr, 0); zipFilename.resize(info.size_filename); // Get name of current file. Contrary to docs "info" parameter can't be null - unzGetCurrentFileInfo64(file, &info, zipFilename.data(), static_cast(zipFilename.size()), nullptr, 0, nullptr, 0); + unzGetCurrentFileInfo64(archive, &info, zipFilename.data(), static_cast(zipFilename.size()), nullptr, 0, nullptr, 0); ret.emplace_back(zipFilename.data(), zipFilename.size()); - result = unzGoToNextFile(file); + result = unzGoToNextFile(archive); } while (result == UNZ_OK); - - if (result != UNZ_OK && result != UNZ_END_OF_LIST_OF_FILE) - { - logGlobal->error("Failed to list file from '%s'! Error code %d", filename.string(), result); - } } - else - { - logGlobal->error("Failed to list files from '%s'! Error code %d", filename.string(), result); - } - - unzClose(file); - return ret; } -bool ZipArchive::extract(const boost::filesystem::path & from, const boost::filesystem::path & where) -{ - // Note: may not be fast enough for large archives (should NOT happen with mods) - // because locating each file by name may be slow. Unlikely slower than decompression though - return extract(from, where, listFiles(from)); -} - -bool ZipArchive::extract(const boost::filesystem::path & from, const boost::filesystem::path & where, const std::vector & what) +ZipArchive::ZipArchive(const boost::filesystem::path & from) { CDefaultIOApi zipAPI; auto zipStructure = zipAPI.getApiStructure(); - unzFile archive = unzOpen2_64(from.c_str(), &zipStructure); + archive = unzOpen2_64(from.c_str(), &zipStructure); - auto onExit = vstd::makeScopeGuard([&]() - { - unzClose(archive); - }); + if (archive == nullptr) + throw std::runtime_error("Failed to open file" + from.string() + "'%s'! Unable to list files!"); +} +ZipArchive::~ZipArchive() +{ + unzClose(archive); +} + +bool ZipArchive::extract(const boost::filesystem::path & where, const std::vector & what) +{ for (const std::string & file : what) - { - if (unzLocateFile(archive, file.c_str(), 1) != UNZ_OK) + if (!extract(where, file)) return false; - const boost::filesystem::path fullName = where / file; - const boost::filesystem::path fullPath = fullName.parent_path(); + return true; +} - boost::filesystem::create_directories(fullPath); - // directory. No file to extract - // TODO: better way to detect directory? Probably check return value of unzOpenCurrentFile? - if (boost::algorithm::ends_with(file, "/")) - continue; +bool ZipArchive::extract(const boost::filesystem::path & where, const std::string & file) +{ + if (unzLocateFile(archive, file.c_str(), 1) != UNZ_OK) + return false; - std::fstream destFile(fullName.c_str(), std::ios::out | std::ios::binary); - if (!destFile.good()) - return false; + const boost::filesystem::path fullName = where / file; + const boost::filesystem::path fullPath = fullName.parent_path(); - if (!extractCurrent(archive, destFile)) - return false; - } + boost::filesystem::create_directories(fullPath); + // directory. No file to extract + // TODO: better way to detect directory? Probably check return value of unzOpenCurrentFile? + if (boost::algorithm::ends_with(file, "/")) + return true; + + std::fstream destFile(fullName.c_str(), std::ios::out | std::ios::binary); + if (!destFile.good()) + return false; + + if (!extractCurrent(archive, destFile)) + return false; return true; } diff --git a/lib/filesystem/CZipLoader.h b/lib/filesystem/CZipLoader.h index 139e49188..be5428926 100644 --- a/lib/filesystem/CZipLoader.h +++ b/lib/filesystem/CZipLoader.h @@ -61,16 +61,17 @@ public: std::unordered_set getFilteredFiles(std::function filter) const override; }; -namespace ZipArchive +class DLL_LINKAGE ZipArchive : boost::noncopyable { - /// List all files present in archive - std::vector DLL_LINKAGE listFiles(const boost::filesystem::path & filename); + unzFile archive; - /// extracts all files from archive "from" into destination directory "where". Directory must exist - bool DLL_LINKAGE extract(const boost::filesystem::path & from, const boost::filesystem::path & where); +public: + ZipArchive(const boost::filesystem::path & from); + ~ZipArchive(); - ///same as above, but extracts only files mentioned in "what" list - bool DLL_LINKAGE extract(const boost::filesystem::path & from, const boost::filesystem::path & where, const std::vector & what); -} + std::vector listFiles(); + bool extract(const boost::filesystem::path & where, const std::vector & what); + bool extract(const boost::filesystem::path & where, const std::string & what); +}; VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index 84378f56b..7b4821c5c 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -16,10 +16,10 @@ #include "CZipLoader.h" //For filesystem initialization -#include "../JsonNode.h" #include "../GameConstants.h" #include "../VCMIDirs.h" #include "../CStopWatch.h" +#include "../json/JsonNode.h" #include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -117,7 +117,7 @@ void CFilesystemGenerator::loadJsonMap(const std::string &mountPoint, const Json if (filename) { auto configData = CResourceHandler::get("initial")->load(JsonPath::builtin(URI))->readAll(); - const JsonNode configInitial(reinterpret_cast(configData.first.get()), configData.second); + const JsonNode configInitial(reinterpret_cast(configData.first.get()), configData.second); filesystem->addLoader(new CMappedFileLoader(mountPoint, configInitial), false); } } @@ -212,7 +212,7 @@ void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives { auto fsConfigData = get("initial")->load(JsonPath::builtin(fsConfigURI))->readAll(); - const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second); + const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second); addFilesystem("data", ModScope::scopeBuiltin(), createFileSystem("", fsConfig["filesystem"], extractArchives)); } diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index cba1fbb43..15efb5cb7 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -11,7 +11,6 @@ #include "ResourcePath.h" #include "FileInfo.h" -#include "../JsonNode.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" @@ -119,6 +118,7 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension) {".MJPG", EResType::VIDEO}, {".MPG", EResType::VIDEO}, {".AVI", EResType::VIDEO}, + {".WEBM", EResType::VIDEO}, {".ZIP", EResType::ARCHIVE_ZIP}, {".LOD", EResType::ARCHIVE_LOD}, {".PAC", EResType::ARCHIVE_LOD}, diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index baad69bbe..250e76bfc 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -28,7 +28,7 @@ class JsonSerializeFormat; * Font: .fnt * Image: .bmp, .jpg, .pcx, .png, .tga * Sound: .wav .82m - * Video: .smk, .bik .mjpg .mpg + * Video: .smk, .bik .mjpg .mpg .webm * Music: .mp3, .ogg * Archive: .lod, .snd, .vid .pac .zip * Palette: .pal @@ -102,7 +102,7 @@ public: void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & type; h & name; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index a73a2391a..9806cd060 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -31,18 +31,22 @@ #include "../campaign/CampaignState.h" #include "../constants/StringConstants.h" #include "../filesystem/ResourcePath.h" +#include "../json/JsonBonus.h" +#include "../json/JsonUtils.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/CQuest.h" +#include "../mapObjects/MiscObjects.h" #include "../mapping/CMap.h" #include "../mapping/CMapEditManager.h" #include "../mapping/CMapService.h" #include "../modding/IdentifierStorage.h" #include "../pathfinder/CPathfinder.h" #include "../pathfinder/PathfinderOptions.h" -#include "../registerTypes/RegisterTypes.h" +#include "../registerTypes/RegisterTypesClientPacks.h" #include "../rmg/CMapGenerator.h" #include "../serializer/CMemorySerializer.h" #include "../serializer/CTypeList.h" @@ -57,7 +61,7 @@ template class CApplyOnGS; class CBaseForGSApply { public: - virtual void applyOnGS(CGameState *gs, void *pack) const =0; + virtual void applyOnGS(CGameState *gs, CPack * pack) const =0; virtual ~CBaseForGSApply() = default; template static CBaseForGSApply *getApplier(const U * t=nullptr) { @@ -68,7 +72,7 @@ public: template class CApplyOnGS : public CBaseForGSApply { public: - void applyOnGS(CGameState *gs, void *pack) const override + void applyOnGS(CGameState *gs, CPack * pack) const override { T *ptr = static_cast(pack); @@ -77,34 +81,6 @@ public: } }; -static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & pos, const PlayerColor & owner) -{ - CGObjectInstance * nobj; - switch(id) - { - case Obj::HERO: - { - auto handler = VLC->objtypeh->getHandlerFor(id, VLC->heroh->objects[subid]->heroClass->getIndex()); - nobj = handler->create(handler->getTemplates().front()); - break; - } - case Obj::TOWN: - nobj = new CGTownInstance(); - break; - default: //rest of objects - nobj = new CGObjectInstance(); - break; - } - nobj->ID = id; - nobj->subID = subid; - nobj->pos = pos; - nobj->tempOwner = owner; - if (id != Obj::HERO) - nobj->appearance = VLC->objtypeh->getHandlerFor(id, subid)->getTemplates().front(); - - return nobj; -} - HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); @@ -125,7 +101,7 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); for(const HeroTypeID & hid : getUnusedAllowedHeroes()) { - if(VLC->heroh->objects[hid.getNum()]->heroClass->faction == ps.castle) + if(hid.toHeroType()->heroClass->faction == ps.castle) factionHeroes.push_back(hid); else otherHeroes.push_back(hid); @@ -149,225 +125,7 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) return *notAllowedHeroesButStillBetterThanCrash.begin(); logGlobal->error("No free heroes at all!"); - assert(0); //current code can't handle this situation - return HeroTypeID::NONE; // no available heroes at all -} - -std::pair CGameState::pickObject (CGObjectInstance *obj) -{ - switch(obj->ID) - { - case Obj::RANDOM_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC)); - case Obj::RANDOM_TREASURE_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE)); - case Obj::RANDOM_MINOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MINOR)); - case Obj::RANDOM_MAJOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MAJOR)); - case Obj::RANDOM_RELIC_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_RELIC)); - case Obj::RANDOM_HERO: - return std::make_pair(Obj::HERO, pickNextHeroType(obj->tempOwner)); - case Obj::RANDOM_MONSTER: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator())); - case Obj::RANDOM_MONSTER_L1: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 1)); - case Obj::RANDOM_MONSTER_L2: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 2)); - case Obj::RANDOM_MONSTER_L3: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 3)); - case Obj::RANDOM_MONSTER_L4: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 4)); - case Obj::RANDOM_RESOURCE: - return std::make_pair(Obj::RESOURCE,getRandomGenerator().nextInt(6)); //now it's OH3 style, use %8 for mithril - case Obj::RANDOM_TOWN: - { - PlayerColor align = (dynamic_cast(obj))->alignmentToPlayer; - si32 f; // can be negative (for random) - if(!align.isValidPlayer()) //same as owner / random - { - if(!obj->tempOwner.isValidPlayer()) - f = -1; //random - else - f = scenarioOps->getIthPlayersSettings(obj->tempOwner).castle; - } - else - { - f = scenarioOps->getIthPlayersSettings(align).castle; - } - if(f<0) - { - do - { - f = getRandomGenerator().nextInt((int)VLC->townh->size() - 1); - } - while ((*VLC->townh)[f]->town == nullptr); // find playable faction - } - return std::make_pair(Obj::TOWN,f); - } - case Obj::RANDOM_MONSTER_L5: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 5)); - case Obj::RANDOM_MONSTER_L6: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 6)); - case Obj::RANDOM_MONSTER_L7: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 7)); - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - { - auto * dwl = dynamic_cast(obj); - int faction; - - //if castle alignment available - if(auto * info = dynamic_cast(dwl->info)) - { - faction = getRandomGenerator().nextInt((int)VLC->townh->size() - 1); - if(info->asCastle && !info->instanceId.empty()) - { - auto iter = map->instanceNames.find(info->instanceId); - - if(iter == map->instanceNames.end()) - logGlobal->error("Map object not found: %s", info->instanceId); - else - { - auto elem = iter->second; - if(elem->ID==Obj::RANDOM_TOWN) - { - randomizeObject(elem.get()); //we have to randomize the castle first - faction = elem->subID; - } - else if(elem->ID==Obj::TOWN) - faction = elem->subID; - else - logGlobal->error("Map object must be town: %s", info->instanceId); - } - } - else if(info->asCastle) - { - - for(auto & elem : map->objects) - { - if(!elem) - continue; - - if(elem->ID==Obj::RANDOM_TOWN - && dynamic_cast(elem.get())->identifier == info->identifier) - { - randomizeObject(elem); //we have to randomize the castle first - faction = elem->subID; - break; - } - else if(elem->ID==Obj::TOWN - && dynamic_cast(elem.get())->identifier == info->identifier) - { - faction = elem->subID; - break; - } - } - } - else - { - std::set temp; - - for(int i = 0; i < info->allowedFactions.size(); i++) - if(info->allowedFactions[i]) - temp.insert(i); - - if(temp.empty()) - logGlobal->error("Random faction selection failed. Nothing is allowed. Fall back to random."); - else - faction = *RandomGeneratorUtil::nextItem(temp, getRandomGenerator()); - } - } - else // castle alignment fixed - faction = obj->subID; - - int level; - - //if level set to range - if(auto * info = dynamic_cast(dwl->info)) - { - level = getRandomGenerator().nextInt(info->minLevel, info->maxLevel) - 1; - } - else // fixed level - { - level = obj->subID; - } - - delete dwl->info; - dwl->info = nullptr; - - std::pair result(Obj::NO_OBJ, -1); - CreatureID cid; - if((*VLC->townh)[faction]->town) - cid = (*VLC->townh)[faction]->town->creatures[level][0]; - else - { - //neutral faction - std::vector possibleCreatures; - std::copy_if(VLC->creh->objects.begin(), VLC->creh->objects.end(), std::back_inserter(possibleCreatures), [faction](const CCreature * c) - { - return c->getFaction().getNum() == faction; - }); - assert(!possibleCreatures.empty()); - cid = (*RandomGeneratorUtil::nextItem(possibleCreatures, getRandomGenerator()))->getId(); - } - - //NOTE: this will pick last dwelling with this creature (Mantis #900) - //check for block map equality is better but more complex solution - auto testID = [&](const Obj & primaryID) -> void - { - auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID); - for (si32 entry : dwellingIDs) - { - const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); - - if (handler->producesCreature(VLC->creh->objects[cid])) - result = std::make_pair(primaryID, entry); - } - }; - - testID(Obj::CREATURE_GENERATOR1); - if (result.first == Obj::NO_OBJ) - testID(Obj::CREATURE_GENERATOR4); - - if (result.first == Obj::NO_OBJ) - { - logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level)); - result = std::make_pair(Obj::CREATURE_GENERATOR1, *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), getRandomGenerator())); - } - - return result; - } - } - return std::make_pair(Obj::NO_OBJ,-1); -} - -void CGameState::randomizeObject(CGObjectInstance *cur) -{ - std::pair ran = pickObject(cur); - if(ran.first == Obj::NO_OBJ || ran.second<0) //this is not a random object, or we couldn't find anything - { - if(cur->ID==Obj::TOWN || cur->ID==Obj::MONSTER) - cur->setType(cur->ID, cur->subID); // update def, if necessary - } - else if(ran.first==Obj::HERO)//special code for hero - { - auto * h = dynamic_cast(cur); - cur->setType(ran.first, ran.second); - map->heroesOnMap.emplace_back(h); - } - else if(ran.first==Obj::TOWN)//special code for town - { - auto * t = dynamic_cast(cur); - cur->setType(ran.first, ran.second); - map->towns.emplace_back(t); - } - else - { - cur->setType(ran.first, ran.second); - } + throw std::runtime_error("Can not allocate hero. All heroes are already used."); } int CGameState::getDate(Date mode) const @@ -402,24 +160,29 @@ CGameState::CGameState() gs = this; heroesPool = std::make_unique(); applier = std::make_shared>(); - registerTypesClientPacks1(*applier); - registerTypesClientPacks2(*applier); + registerTypesClientPacks(*applier); globalEffects.setNodeType(CBonusSystemNode::GLOBAL_EFFECTS); } CGameState::~CGameState() { + // explicitly delete all ongoing battles first - BattleInfo destructor requires valid CGameState + currentBattles.clear(); map.dellNull(); + scenarioOps.dellNull(); + initialOpts.dellNull(); } -void CGameState::preInit(Services * services) +void CGameState::preInit(Services * newServices, IGameCallback * newCallback) { - this->services = services; + services = newServices; + callback = newCallback; } void CGameState::init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator & progressTracking, bool allowSavingRandomMap) { - preInitAuto(); + assert(services); + assert(callback); logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed); getRandomGenerator().setSeed(si->seedToBeUsed); scenarioOps = CMemorySerializer::deepCopy(*si).release(); @@ -428,17 +191,16 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog switch(scenarioOps->mode) { - case StartInfo::NEW_GAME: + case EStartMode::NEW_GAME: initNewGame(mapService, allowSavingRandomMap, progressTracking); break; - case StartInfo::CAMPAIGN: + case EStartMode::CAMPAIGN: initCampaign(); break; default: logGlobal->error("Wrong mode: %d", static_cast(scenarioOps->mode)); return; } - VLC->arth->initAllowedArtifactsList(map->allowedArtifact); logGlobal->info("Map loaded!"); checkMapChecksum(); @@ -460,20 +222,16 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog initHeroes(); initStartingBonus(); initTowns(); + initTownNames(); placeHeroesInTowns(); initMapObjects(); buildBonusSystemTree(); initVisitingAndGarrisonedHeroes(); initFogOfWar(); - // Explicitly initialize static variables - for(auto & elem : players) - { - CGKeys::playerKeyMap[elem.first] = std::set(); - } for(auto & elem : teams) { - CGObelisk::visited[elem.first] = 0; + map->obelisksVisited[elem.first] = 0; } logGlobal->debug("\tChecking objectives"); @@ -533,21 +291,13 @@ void CGameState::updateEntity(Metatype metatype, int32_t index, const JsonNode & void CGameState::updateOnLoad(StartInfo * si) { - preInitAuto(); + assert(services); + assert(callback); scenarioOps->playerInfos = si->playerInfos; for(auto & i : si->playerInfos) gs->players[i.first].human = i.second.isControlledByHuman(); } -void CGameState::preInitAuto() -{ - if(services == nullptr) - { - logGlobal->error("Game state preinit missing"); - preInit(VLC); - } -} - void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking) { if(scenarioOps->createRandomMap()) @@ -556,7 +306,7 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan CStopWatch sw; // Gen map - CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, scenarioOps->seedToBeUsed); + CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, callback, scenarioOps->seedToBeUsed); progressTracking.include(mapGenerator); std::unique_ptr randomMap = mapGenerator.generate(); @@ -619,7 +369,7 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan { logGlobal->info("Open map file: %s", scenarioOps->mapname); const ResourcePath mapURI(scenarioOps->mapname, EResType::MAP); - map = mapService->loadMap(mapURI).release(); + map = mapService->loadMap(mapURI, callback).release(); } } @@ -762,20 +512,21 @@ void CGameState::initRandomFactionsForPlayers() void CGameState::randomizeMapObjects() { logGlobal->debug("\tRandomizing objects"); - for(CGObjectInstance *obj : map->objects) + for(CGObjectInstance *object : map->objects) { - if(!obj) continue; + if(!object) + continue; - randomizeObject(obj); + object->pickRandomObject(getRandomGenerator()); //handle Favouring Winds - mark tiles under it - if(obj->ID == Obj::FAVORABLE_WINDS) + if(object->ID == Obj::FAVORABLE_WINDS) { - for (int i = 0; i < obj->getWidth() ; i++) + for (int i = 0; i < object->getWidth() ; i++) { - for (int j = 0; j < obj->getHeight() ; j++) + for (int j = 0; j < object->getHeight() ; j++) { - int3 pos = obj->pos - int3(i,j,0); + int3 pos = object->pos - int3(i,j,0); if(map->isInTheMap(pos)) map->getTile(pos).extTileFlags |= 128; } } @@ -808,7 +559,15 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy } } - CGObjectInstance * hero = createObject(Obj::HERO, heroTypeId.getNum(), townPos, playerColor); + auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, heroTypeId.toHeroType()->heroClass->getIndex()); + CGObjectInstance * obj = handler->create(callback, handler->getTemplates().front()); + CGHeroInstance * hero = dynamic_cast(obj); + + hero->ID = Obj::HERO; + hero->setHeroType(heroTypeId); + hero->tempOwner = playerColor; + + hero->pos = townPos; hero->pos += hero->getVisitableOffset(); map->getEditManager()->insertObject(hero); } @@ -864,7 +623,7 @@ void CGameState::initHeroes() hero->initHero(getRandomGenerator()); getPlayerState(hero->getOwner())->heroes.push_back(hero); - map->allHeroes[hero->type->getIndex()] = hero; + map->allHeroes[hero->getHeroType().getNum()] = hero; } // generate boats for all heroes on water @@ -875,11 +634,9 @@ void CGameState::initHeroes() if (tile.terType->isWater()) { auto handler = VLC->objtypeh->getHandlerFor(Obj::BOAT, hero->getBoatType().getNum()); - CGBoat * boat = dynamic_cast(handler->create()); + auto boat = dynamic_cast(handler->create(callback, nullptr)); handler->configureObject(boat, gs->getRandomGenerator()); - boat->ID = Obj::BOAT; - boat->subID = hero->getBoatType().getNum(); boat->pos = hero->pos; boat->appearance = handler->getTemplates().front(); boat->id = ObjectInstanceID(static_cast(gs->map->objects.size())); @@ -894,24 +651,28 @@ void CGameState::initHeroes() for(auto obj : map->objects) //prisons { if(obj && obj->ID == Obj::PRISON) - map->allHeroes[obj->subID] = dynamic_cast(obj.get()); + { + auto * hero = dynamic_cast(obj.get()); + hero->initHero(getRandomGenerator()); + map->allHeroes[hero->getHeroType().getNum()] = hero; + } } std::set heroesToCreate = getUnusedAllowedHeroes(); //ids of heroes to be created and put into the pool for(auto ph : map->predefinedHeroes) { - if(!vstd::contains(heroesToCreate, HeroTypeID(ph->subID))) + if(!vstd::contains(heroesToCreate, ph->getHeroType())) continue; ph->initHero(getRandomGenerator()); heroesPool->addHeroToPool(ph); heroesToCreate.erase(ph->type->getId()); - map->allHeroes[ph->subID] = ph; + map->allHeroes[ph->getHeroType().getNum()] = ph; } for(const HeroTypeID & htype : heroesToCreate) //all not used allowed heroes go with default state into the pool { - auto * vhi = new CGHeroInstance(); + auto * vhi = new CGHeroInstance(callback); vhi->initHero(getRandomGenerator(), htype); int typeID = htype.getNum(); @@ -933,7 +694,7 @@ void CGameState::initFogOfWar() int layers = map->levels(); for(auto & elem : teams) { - auto fow = elem.second.fogOfWarMap; + auto & fow = elem.second.fogOfWarMap; fow->resize(boost::extents[layers][map->width][map->height]); std::fill(fow->data(), fow->data() + fow->num_elements(), 0); @@ -953,7 +714,7 @@ void CGameState::initFogOfWar() void CGameState::initStartingBonus() { - if (scenarioOps->mode == StartInfo::CAMPAIGN) + if (scenarioOps->mode == EStartMode::CAMPAIGN) return; // These are the single scenario bonuses; predefined // campaign bonuses are spread out over other init* functions. @@ -992,7 +753,7 @@ void CGameState::initStartingBonus() logGlobal->error("Cannot give starting artifact - no heroes!"); break; } - const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toArtifact(VLC->artifacts()); + const Artifact * toGive = pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC); CGHeroInstance *hero = elem.second.heroes[0]; if(!giveHeroArtifact(hero, toGive->getId())) @@ -1003,6 +764,50 @@ void CGameState::initStartingBonus() } } +void CGameState::initTownNames() +{ + std::map> availableNames; + for(const auto & faction : VLC->townh->getDefaultAllowed()) + { + std::vector potentialNames; + if(faction.toFaction()->town->getRandomNamesCount() > 0) + { + for(int i = 0; i < faction.toFaction()->town->getRandomNamesCount(); ++i) + potentialNames.push_back(i); + + availableNames[faction] = potentialNames; + } + } + + for(auto & vti : map->towns) + { + assert(vti->town); + + if(!vti->getNameTextID().empty()) + continue; + + FactionID faction = vti->getFaction(); + + if(availableNames.empty()) + { + logGlobal->warn("Failed to find available name for a random town!"); + vti->setNameTextId("core.genrltxt.508"); // Unnamed + continue; + } + + // If town has no available names (for example - all were picked) - pick names from some other faction that still has names available + if(!availableNames.count(faction)) + faction = RandomGeneratorUtil::nextItem(availableNames, getRandomGenerator())->first; + + auto nameIt = RandomGeneratorUtil::nextItem(availableNames[faction], getRandomGenerator()); + vti->setNameTextId(faction.toFaction()->town->getRandomNameTextID(*nameIt)); + + availableNames[faction].erase(nameIt); + if(availableNames[faction].empty()) + availableNames.erase(faction); + } +} + void CGameState::initTowns() { logGlobal->debug("\tTowns"); @@ -1010,26 +815,19 @@ void CGameState::initTowns() if (campaign) campaign->initTowns(); - CGTownInstance::universitySkills.clear(); - for ( int i=0; i<4; i++) - CGTownInstance::universitySkills.push_back(14+i);//skills for university + map->townUniversitySkills.clear(); + map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::FIRE_MAGIC)); + map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::AIR_MAGIC)); + map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::WATER_MAGIC)); + map->townUniversitySkills.push_back(SecondarySkill(SecondarySkill::EARTH_MAGIC)); - for (auto & elem : map->towns) + for (auto & vti : map->towns) { - CGTownInstance * vti =(elem); - if(!vti->town) - { - vti->town = (*VLC->townh)[vti->subID]->town; - } - if(vti->getNameTranslated().empty()) - { - size_t nameID = getRandomGenerator().nextInt(vti->getTown()->getRandomNamesCount() - 1); - vti->setNameTextId(vti->getTown()->getRandomNameTextID(nameID)); - } + assert(vti->town); - static const BuildingID basicDwellings[] = { BuildingID::DWELL_FIRST, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 }; - static const BuildingID upgradedDwellings[] = { BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP }; - static const BuildingID hordes[] = { BuildingID::HORDE_PLACEHOLDER1, BuildingID::HORDE_PLACEHOLDER2, BuildingID::HORDE_PLACEHOLDER3, BuildingID::HORDE_PLACEHOLDER4, BuildingID::HORDE_PLACEHOLDER5, BuildingID::HORDE_PLACEHOLDER6, BuildingID::HORDE_PLACEHOLDER7 }; + constexpr std::array basicDwellings = { BuildingID::DWELL_FIRST, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 }; + constexpr std::array upgradedDwellings = { BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP }; + constexpr std::array hordes = { BuildingID::HORDE_PLACEHOLDER1, BuildingID::HORDE_PLACEHOLDER2, BuildingID::HORDE_PLACEHOLDER3, BuildingID::HORDE_PLACEHOLDER4, BuildingID::HORDE_PLACEHOLDER5, BuildingID::HORDE_PLACEHOLDER6, BuildingID::HORDE_PLACEHOLDER7 }; //init buildings if(vstd::contains(vti->builtBuildings, BuildingID::DEFAULT)) //give standard set of buildings @@ -1155,17 +953,15 @@ void CGameState::initMapObjects() for(CGObjectInstance *obj : map->objects) { if(obj) - { - logGlobal->trace("Calling Init for object %d, %s, %s", obj->id.getNum(), obj->typeName, obj->subTypeName); obj->initObj(getRandomGenerator()); - } } + logGlobal->debug("\tObject initialization done"); for(CGObjectInstance *obj : map->objects) { if(!obj) continue; - switch (obj->ID) + switch(obj->ID.toEnum()) { case Obj::QUEST_GUARD: case Obj::SEER_HUT: @@ -1176,7 +972,7 @@ void CGameState::initMapObjects() } } } - CGSubterraneanGate::postInit(); //pairing subterranean gates + CGSubterraneanGate::postInit(callback); //pairing subterranean gates map->calculateGuardingGreaturePositions(); //calculate once again when all the guards are placed and initialized } @@ -1370,7 +1166,7 @@ PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor void CGameState::apply(CPack *pack) { - ui16 typ = typeList.getTypeID(pack); + ui16 typ = CTypeList::getInstance().getTypeID(pack); applier->getApplier(typ)->applyOnGS(this, pack); } @@ -1448,7 +1244,7 @@ int3 CGameState::guardingCreaturePosition (int3 pos) const void CGameState::updateRumor() { - static std::vector rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND}; + static const std::vector rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND}; std::vector sRumorTypes = { RumorState::RUMOR_OBELISKS, RumorState::RUMOR_ARTIFACTS, RumorState::RUMOR_ARMY, RumorState::RUMOR_INCOME}; if(map->grailPos.valid()) // Grail should always be on map, but I had related crash I didn't manage to reproduce @@ -1617,7 +1413,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio case EventCondition::HAVE_ARTIFACT: //check if any hero has winning artifact { for(const auto & elem : p->heroes) - if(elem->hasArt(ArtifactID(condition.objectType))) + if(elem->hasArt(condition.objectType.as())) return true; return false; } @@ -1627,13 +1423,11 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio int total = 0; //creature counter for(auto object : map->objects) { - const CArmedInstance *ai = nullptr; - if(object - && object->tempOwner == player //object controlled by player - && (ai = dynamic_cast(object.get()))) //contains army + const auto * ai = dynamic_cast(object.get()); + if(ai && ai->getOwner() == player) { for(const auto & elem : ai->Slots()) //iterate through army - if(elem.second->type->getIndex() == condition.objectType) //it's searched creature + if(elem.second->getId() == condition.objectType.as()) //it's searched creature total += elem.second->count; } } @@ -1641,20 +1435,20 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::HAVE_RESOURCES: { - return p->resources[condition.objectType] >= condition.value; + return p->resources[condition.objectType.as()] >= condition.value; } case EventCondition::HAVE_BUILDING: { - if (condition.object) // specific town + if (condition.objectID != ObjectInstanceID::NONE) // specific town { - const auto * t = dynamic_cast(condition.object); - return (t->tempOwner == player && t->hasBuilt(BuildingID(condition.objectType))); + const auto * t = getTown(condition.objectID); + return (t->tempOwner == player && t->hasBuilt(condition.objectType.as())); } else // any town { for (const CGTownInstance * t : p->towns) { - if (t->hasBuilt(BuildingID(condition.objectType))) + if (t->hasBuilt(condition.objectType.as())) return true; } return false; @@ -1662,18 +1456,15 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::DESTROY: { - if (condition.object) // mode A - destroy specific object of this type + if (condition.objectID != ObjectInstanceID::NONE) // mode A - destroy specific object of this type { - if(const auto * hero = dynamic_cast(condition.object)) - return boost::range::find(gs->map->heroesOnMap, hero) == gs->map->heroesOnMap.end(); - else - return getObj(condition.object->id) == nullptr; + return p->destroyedObjects.count(condition.objectID); } else { for(const auto & elem : map->objects) // mode B - destroy all objects of this type { - if(elem && elem->ID.getNum() == condition.objectType) + if(elem && elem->ID == condition.objectType.as()) return false; } return true; @@ -1683,18 +1474,22 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio { // list of players that need to control object to fulfull condition // NOTE: cgameinfocallback specified explicitly in order to get const version - const auto & team = CGameInfoCallback::getPlayerTeam(player)->players; + const auto * team = CGameInfoCallback::getPlayerTeam(player); - if (condition.object) // mode A - flag one specific object, like town + if (condition.objectID != ObjectInstanceID::NONE) // mode A - flag one specific object, like town { - return team.count(condition.object->tempOwner) != 0; + const auto * object = getObjInstance(condition.objectID); + + if (!object) + return false; + return team->players.count(object->getOwner()) != 0; } else { for(const auto & elem : map->objects) // mode B - flag all objects of this type { //check not flagged objs - if ( elem && elem->ID.getNum() == condition.objectType && team.count(elem->tempOwner) == 0 ) + if ( elem && elem->ID == condition.objectType.as() && team->players.count(elem->getOwner()) == 0 ) return false; } return true; @@ -1702,9 +1497,9 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::TRANSPORT: { - const auto * t = dynamic_cast(condition.object); - return (t->visitingHero && t->visitingHero->hasArt(ArtifactID(condition.objectType))) || - (t->garrisonHero && t->garrisonHero->hasArt(ArtifactID(condition.objectType))); + const auto * t = getTown(condition.objectID); + return (t->visitingHero && t->visitingHero->hasArt(condition.objectType.as())) || + (t->garrisonHero && t->garrisonHero->hasArt(condition.objectType.as())); } case EventCondition::DAYS_PASSED: { @@ -1725,24 +1520,6 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio { return condition.value; // just convert to bool } - case EventCondition::HAVE_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } - case EventCondition::HAVE_BUILDING_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } - case EventCondition::DESTROY_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } default: logGlobal->error("Invalid event condition type: %d", (int)condition.condition); return false; @@ -1976,10 +1753,10 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) } if(level >= 3) //obelisks found { - auto getObeliskVisited = [](const TeamID & t) + auto getObeliskVisited = [&](const TeamID & t) { - if(CGObelisk::visited.count(t)) - return CGObelisk::visited[t]; + if(map->obelisksVisited.count(t)) + return map->obelisksVisited[t]; else return ui8(0); }; @@ -2026,13 +1803,13 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { if(playerInactive(player.second.color)) //do nothing for neutral player continue; - int bestCre = -1; //best creature's ID + CreatureID bestCre; //best creature's ID for(const auto & elem : player.second.heroes) { for(const auto & it : elem->Slots()) { - int toCmp = it.second->type->getId(); //ID of creature we should compare with the best one - if(bestCre == -1 || VLC->creh->objects[bestCre]->getAIValue() < VLC->creh->objects[toCmp]->getAIValue()) + CreatureID toCmp = it.second->type->getId(); //ID of creature we should compare with the best one + if(bestCre == CreatureID::NONE || bestCre.toEntity(VLC)->getAIValue() < toCmp.toEntity(VLC)->getAIValue()) { bestCre = toCmp; } @@ -2054,12 +1831,6 @@ void CGameState::buildBonusSystemTree() { t->deserializationFix(); } - // CStackInstance <-> CCreature, CStackInstance <-> CArmedInstance, CArtifactInstance <-> CArtifact - // are provided on initializing / deserializing - - // NOTE: calling deserializationFix() might be more correct option, but might lead to side effects - for (auto hero : map->heroesOnMap) - hero->boatDeserializationFix(); } void CGameState::deserializationFix() @@ -2097,13 +1868,12 @@ void CGameState::attachArmedObjects() bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) { - CArtifact * const artifact = VLC->arth->objects[aid]; //pointer to constant object - CArtifactInstance * ai = ArtifactUtils::createNewArtifactInstance(artifact); + CArtifactInstance * ai = ArtifactUtils::createNewArtifactInstance(aid); map->addNewArtifactInstance(ai); auto slot = ArtifactUtils::getArtAnyPosition(h, aid); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) { - ai->putAt(ArtifactLocation(h, slot)); + ai->putAt(*h, slot); return true; } else @@ -2114,10 +1884,7 @@ bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllowed) const { - std::set ret; - for(int i = 0; i < map->allowedHeroes.size(); i++) - if(map->allowedHeroes[i] || alsoIncludeNotAllowed) - ret.insert(HeroTypeID(i)); + std::set ret = map->allowedHeroes; for(const auto & playerSettingPair : scenarioOps->playerInfos) //remove uninitialized yet heroes picked for start by other players { @@ -2130,12 +1897,15 @@ std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllow if(hero->type) ret -= hero->type->getId(); else - ret -= HeroTypeID(hero->subID); + ret -= hero->getHeroType(); } for(auto obj : map->objects) //prisons - if(obj && obj->ID == Obj::PRISON) - ret -= HeroTypeID(obj->subID); + { + auto * hero = dynamic_cast(obj.get()); + if(hero && hero->ID == Obj::PRISON) + ret -= hero->getHeroType(); + } return ret; } @@ -2147,23 +1917,19 @@ bool CGameState::isUsedHero(const HeroTypeID & hid) const CGHeroInstance * CGameState::getUsedHero(const HeroTypeID & hid) const { - for(auto hero : map->heroesOnMap) //heroes instances initialization - { - if(hero->type && hero->type->getId() == hid) - { - return hero; - } - } - for(auto obj : map->objects) //prisons { - if(obj && obj->ID == Obj::PRISON ) - { - auto * hero = dynamic_cast(obj.get()); - assert(hero); - if ( hero->type && hero->type->getId() == hid ) - return hero; - } + if (!obj) + continue; + + if ( obj->ID !=Obj::PRISON && obj->ID != Obj::HERO) + continue; + + auto * hero = dynamic_cast(obj.get()); + assert(hero); + + if (hero->getHeroType() == hid) + return hero; } return nullptr; @@ -2190,15 +1956,7 @@ bool RumorState::update(int id, int extra) TeamState::TeamState() { setNodeType(TEAM); - fogOfWarMap = std::make_shared>(); -} - -TeamState::TeamState(TeamState && other) noexcept: - CBonusSystemNode(std::move(other)), - id(other.id) -{ - std::swap(players, other.players); - std::swap(fogOfWarMap, other.fogOfWarMap); + fogOfWarMap = std::make_unique>(); } CRandomGenerator & CGameState::getRandomGenerator() @@ -2206,4 +1964,78 @@ CRandomGenerator & CGameState::getRandomGenerator() return rand; } +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) +{ + std::set potentialPicks; + + // Select artifacts that satisfy provided criterias + for (auto const & artifactID : map->allowedArtifact) + { + if (!VLC->arth->legalArtifact(artifactID)) + continue; + + auto const * artifact = artifactID.toArtifact(); + + assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized + + if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE) + continue; + + if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR) + continue; + + if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR) + continue; + + if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC) + continue; + + if (!accepts(artifact->getId())) + continue; + + potentialPicks.insert(artifact->getId()); + } + + return pickRandomArtifact(rand, potentialPicks); +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) +{ + // No allowed artifacts at all - give Grail - this can't be banned (hopefully) + // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior + if (potentialPicks.empty()) + { + logGlobal->warn("Failed to find artifact that matches requested parameters!"); + return ArtifactID::GRAIL; + } + + // Find how many times least used artifacts were picked by randomizer + int leastUsedTimes = std::numeric_limits::max(); + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] < leastUsedTimes) + leastUsedTimes = allocatedArtifacts[artifact]; + + // Pick all artifacts that were used least number of times + std::set preferredPicks; + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] == leastUsedTimes) + preferredPicks.insert(artifact); + + assert(!preferredPicks.empty()); + + ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand); + allocatedArtifacts[artID] += 1; // record +1 more usage + return artID; +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) +{ + return pickRandomArtifact(rand, 0xff, std::move(accepts)); +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags) +{ + return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 94db3c60e..ce2a4b102 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -13,6 +13,7 @@ #include "IGameCallback.h" #include "LoadProgress.h" #include "ConstTransitivePtr.h" +#include "../CRandomGenerator.h" namespace boost { @@ -59,7 +60,7 @@ struct DLL_LINKAGE RumorState RumorState(){type = TYPE_NONE;}; bool update(int id, int extra); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & type; h & last; @@ -83,6 +84,9 @@ class DLL_LINKAGE CGameState : public CNonConstInfoCallback friend class CGameStateCampaign; public: + /// Stores number of times each artifact was placed on map via randomization + std::map allocatedArtifacts; + /// List of currently ongoing battles std::vector> currentBattles; /// ID that can be allocated to next battle @@ -94,15 +98,18 @@ public: /// list of players currently making turn. Usually - just one, except for simturns std::set actingPlayers; + IGameCallback * callback; + CGameState(); virtual ~CGameState(); - void preInit(Services * services); + void preInit(Services * services, IGameCallback * callback); void init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator &, bool allowSavingRandomMap = true); void updateOnLoad(StartInfo * si); - ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) + ConstTransitivePtr scenarioOps; + ConstTransitivePtr initialOpts; //copy of settings received from pregame (not randomized) ui32 day; //total number of days in game ConstTransitivePtr map; std::map players; @@ -115,6 +122,8 @@ public: void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; bool giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid); + /// picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly + HeroTypeID pickNextHeroType(const PlayerColor & owner); void apply(CPack *pack); BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand); @@ -128,6 +137,12 @@ public: std::vector guardingCreatures (int3 pos) const; void updateRumor(); + /// Gets a artifact ID randomly and removes the selected artifact from this handler. + ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set filtered); + /// Returns battle in which selected player is engaged, or nullptr if none. /// Can NOT be used with neutral player, use battle by ID instead const BattleInfo * getBattle(const PlayerColor & player) const; @@ -160,7 +175,7 @@ public: /// Any server-side code outside of GH must use CRandomGenerator::getDefault CRandomGenerator & getRandomGenerator(); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & scenarioOps; h & initialOpts; @@ -174,20 +189,19 @@ public: h & rand; h & rumor; h & campaign; + h & allocatedArtifacts; BONUS_TREE_DESERIALIZATION_FIX } private: // ----- initialization ----- - void preInitAuto(); void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); void checkMapChecksum(); void initGlobalBonuses(); void initGrailPosition(); void initRandomFactionsForPlayers(); void randomizeMapObjects(); - void randomizeObject(CGObjectInstance *cur); void initPlayerStates(); void placeStartingHeroes(); void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); @@ -198,6 +212,7 @@ private: void initFogOfWar(); void initStartingBonus(); void initTowns(); + void initTownNames(); void initMapObjects(); void initVisitingAndGarrisonedHeroes(); void initCampaign(); @@ -214,9 +229,7 @@ private: CGHeroInstance * getUsedHero(const HeroTypeID & hid) const; bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; - std::pair pickObject(CGObjectInstance *obj); //chooses type of object to be randomized, returns HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly - HeroTypeID pickNextHeroType(const PlayerColor & owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; // ---- data ----- diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 462204cb7..46417dbe7 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -16,7 +16,8 @@ #include "../campaign/CampaignState.h" #include "../mapping/CMapEditManager.h" #include "../mapObjects/CGHeroInstance.h" -#include "../registerTypes/RegisterTypes.h" +#include "../mapObjects/CGTownInstance.h" +#include "../networkPacks/ArtifactLocation.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../StartInfo.h" @@ -38,7 +39,7 @@ CampaignHeroReplacement::CampaignHeroReplacement(CGHeroInstance * hero, const Ob CGameStateCampaign::CGameStateCampaign(CGameState * owner): gameState(owner) { - assert(gameState->scenarioOps->mode == StartInfo::CAMPAIGN); + assert(gameState->scenarioOps->mode == EStartMode::CAMPAIGN); assert(gameState->scenarioOps->campState != nullptr); } @@ -54,35 +55,27 @@ std::optional CGameStateCampaign::getHeroesSourceScenario() auto bonus = currentBonus(); if(bonus && bonus->type == CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO) - return static_cast(bonus->info2);; + return static_cast(bonus->info2); return campaignState->lastScenario(); } -void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector & campaignHeroReplacements, const CampaignTravel & travelOptions) +void CGameStateCampaign::trimCrossoverHeroesParameters(const CampaignTravel & travelOptions) { - // create heroes list for convenience iterating - std::vector crossoverHeroes; - crossoverHeroes.reserve(campaignHeroReplacements.size()); - for(auto & campaignHeroReplacement : campaignHeroReplacements) - { - crossoverHeroes.push_back(campaignHeroReplacement.hero); - } - // TODO this logic (what should be kept) should be part of CScenarioTravel and be exposed via some clean set of methods if(!travelOptions.whatHeroKeeps.experience) { //trimming experience - for(CGHeroInstance * cgh : crossoverHeroes) + for(auto & hero : campaignHeroReplacements) { - cgh->initExp(gameState->getRandomGenerator()); + hero.hero->initExp(gameState->getRandomGenerator()); } } if(!travelOptions.whatHeroKeeps.primarySkills) { //trimming prim skills - for(CGHeroInstance * cgh : crossoverHeroes) + for(auto & hero : campaignHeroReplacements) { for(auto g = PrimarySkill::BEGIN; g < PrimarySkill::END; ++g) { @@ -90,7 +83,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorgetBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g]; + hero.hero->getLocalBonus(sel)->val = hero.hero->type->heroClass->primarySkillInitial[g.getNum()]; } } } @@ -98,32 +91,32 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorsecSkills = cgh->type->secSkillsInit; - cgh->recreateSecondarySkillsBonuses(); + hero.hero->secSkills = hero.hero->type->secSkillsInit; + hero.hero->recreateSecondarySkillsBonuses(); } } if(!travelOptions.whatHeroKeeps.spells) { - for(CGHeroInstance * cgh : crossoverHeroes) + for(auto & hero : campaignHeroReplacements) { - cgh->removeSpellbook(); + hero.hero->removeSpellbook(); } } if(!travelOptions.whatHeroKeeps.artifacts) { //trimming artifacts - for(CGHeroInstance * hero : crossoverHeroes) + for(auto & hero : campaignHeroReplacements) { const auto & checkAndRemoveArtifact = [&](const ArtifactPosition & artifactPosition) { if(artifactPosition == ArtifactPosition::SPELLBOOK) return; // do not handle spellbook this way - const ArtSlotInfo *info = hero->getSlot(artifactPosition); + const ArtSlotInfo *info = hero.hero->getSlot(artifactPosition); if(!info) return; @@ -134,24 +127,27 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorartType->getId()); - ArtifactLocation al(hero, artifactPosition); - if(!takeable && !al.getSlot()->locked) //don't try removing locked artifacts -> it crashes #1719 - al.removeArtifact(); + if (takeable) + hero.transferrableArtifacts.push_back(artifactPosition); + + ArtifactLocation al(hero.hero->id, artifactPosition); + if(!takeable && !hero.hero->getSlot(al.slot)->locked) //don't try removing locked artifacts -> it crashes #1719 + hero.hero->getArt(al.slot)->removeFrom(*hero.hero, al.slot); }; // process on copy - removal of artifact will invalidate container - auto artifactsWorn = hero->artifactsWorn; + auto artifactsWorn = hero.hero->artifactsWorn; for(const auto & art : artifactsWorn) checkAndRemoveArtifact(art.first); // process in reverse - removal of artifact will shift all artifacts after this one - for(int slotNumber = hero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) + for(int slotNumber = hero.hero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) checkAndRemoveArtifact(ArtifactPosition::BACKPACK_START + slotNumber); } } //trimming creatures - for(CGHeroInstance * cgh : crossoverHeroes) + for(auto & hero : campaignHeroReplacements) { auto shouldSlotBeErased = [&](const std::pair & j) -> bool { @@ -159,16 +155,16 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorstacks; //copy of the map, so we can iterate iover it and remove stacks + auto stacksCopy = hero.hero->stacks; //copy of the map, so we can iterate iover it and remove stacks for(auto &slotPair : stacksCopy) if(shouldSlotBeErased(slotPair)) - cgh->eraseStack(slotPair.first); + hero.hero->eraseStack(slotPair.first); } // Removing short-term bonuses - for(CGHeroInstance * cgh : crossoverHeroes) + for(auto & hero : campaignHeroReplacements) { - cgh->removeBonusesRecursive(CSelector(Bonus::OneDay) + hero.hero->removeBonusesRecursive(CSelector(Bonus::OneDay) .Or(CSelector(Bonus::OneWeek)) .Or(CSelector(Bonus::NTurns)) .Or(CSelector(Bonus::NDays)) @@ -189,8 +185,8 @@ void CGameStateCampaign::placeCampaignHeroes() auto it = gameState->scenarioOps->playerInfos.find(playerColor); if(it != gameState->scenarioOps->playerInfos.end()) { - auto heroTypeId = campaignBonus->info2; - if(heroTypeId == 0xffff) // random bonus hero + HeroTypeID heroTypeId = HeroTypeID(campaignBonus->info2); + if(heroTypeId.getNum() == 0xffff) // random bonus hero { heroTypeId = gameState->pickUnusedHeroTypeRandomly(playerColor); } @@ -200,20 +196,29 @@ void CGameStateCampaign::placeCampaignHeroes() } logGlobal->debug("\tGenerate list of hero placeholders"); - auto campaignHeroReplacements = generateCampaignHeroesToReplace(); + generateCampaignHeroesToReplace(); logGlobal->debug("\tPrepare crossover heroes"); - trimCrossoverHeroesParameters(campaignHeroReplacements, campaignState->scenario(*campaignState->currentScenario()).travelOptions); + trimCrossoverHeroesParameters(campaignState->scenario(*campaignState->currentScenario()).travelOptions); // remove same heroes on the map which will be added through crossover heroes // INFO: we will remove heroes because later it may be possible that the API doesn't allow having heroes // with the same hero type id std::vector removedHeroes; - std::set heroesToRemove = campaignState->getReservedHeroes(); + std::set reservedHeroes = campaignState->getReservedHeroes(); + std::set heroesToRemove; - for(auto & campaignHeroReplacement : campaignHeroReplacements) - heroesToRemove.insert(HeroTypeID(campaignHeroReplacement.hero->subID)); + for (auto const & heroID : reservedHeroes ) + { + // Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear + if (!campaignState->getHeroByType(heroID).isNull()) + heroesToRemove.insert(heroID); + } + + for(auto & replacement : campaignHeroReplacements) + if (replacement.heroPlaceholderId.hasValue()) + heroesToRemove.insert(replacement.hero->getHeroType()); for(auto & heroID : heroesToRemove) { @@ -228,7 +233,7 @@ void CGameStateCampaign::placeCampaignHeroes() } logGlobal->debug("\tReplace placeholders with heroes"); - replaceHeroesPlaceholders(campaignHeroReplacements); + replaceHeroesPlaceholders(); // now add removed heroes again with unused type ID for(auto * hero : removedHeroes) @@ -256,7 +261,7 @@ void CGameStateCampaign::placeCampaignHeroes() assert(0); // should not happen } - hero->subID = heroTypeId; + hero->setHeroType(heroTypeId); gameState->map->getEditManager()->insertObject(hero); } } @@ -300,7 +305,7 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) CArtifactInstance * scroll = ArtifactUtils::createScroll(SpellID(curBonus->info2)); const auto slot = ArtifactUtils::getArtAnyPosition(hero, scroll->getTypeId()); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) - scroll->putAt(ArtifactLocation(hero, slot)); + scroll->putAt(*hero, slot); else logGlobal->error("Cannot give starting scroll - no free slots!"); break; @@ -328,10 +333,13 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) } } -void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector & campaignHeroReplacements) +void CGameStateCampaign::replaceHeroesPlaceholders() { for(const auto & campaignHeroReplacement : campaignHeroReplacements) { + if (!campaignHeroReplacement.heroPlaceholderId.hasValue()) + continue; + auto * heroPlaceholder = dynamic_cast(gameState->getObjInstance(campaignHeroReplacement.heroPlaceholderId)); CGHeroInstance *heroToPlace = campaignHeroReplacement.hero; @@ -339,7 +347,7 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vectortempOwner.isValidPlayer()) heroToPlace->tempOwner = heroPlaceholder->tempOwner; heroToPlace->pos = heroPlaceholder->pos; - heroToPlace->type = VLC->heroh->objects[heroToPlace->subID]; + heroToPlace->type = heroToPlace->getHeroType().toHeroType(); heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, heroToPlace->type->heroClass->getIndex())->getTemplates().front(); gameState->map->removeBlockVisTiles(heroPlaceholder, true); @@ -355,14 +363,65 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector CGameStateCampaign::generateCampaignHeroesToReplace() +void CGameStateCampaign::transferMissingArtifacts(const CampaignTravel & travelOptions) +{ + CGHeroInstance * receiver = nullptr; + + for(auto obj : gameState->map->objects) + { + if (!obj) + continue; + + if (obj->ID != Obj::HERO) + continue; + + auto * hero = dynamic_cast(obj.get()); + + if (gameState->getPlayerState(hero->getOwner())->isHuman()) + { + receiver = hero; + break; + } + } + assert(receiver); + + for(const auto & campaignHeroReplacement : campaignHeroReplacements) + { + if (campaignHeroReplacement.heroPlaceholderId.hasValue()) + continue; + + auto * donorHero = campaignHeroReplacement.hero; + + for (auto const & artLocation : campaignHeroReplacement.transferrableArtifacts) + { + auto * artifact = donorHero->getArt(artLocation); + artifact->removeFrom(*donorHero, artLocation); + + if (receiver) + { + const auto slot = ArtifactUtils::getArtAnyPosition(receiver, artifact->getTypeId()); + if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) + artifact->putAt(*receiver, slot); + else + logGlobal->error("Cannot transfer artifact - no free slots!"); + } + else + logGlobal->error("Cannot transfer artifact - no receiver hero!"); + } + + delete donorHero; + } +} + +void CGameStateCampaign::generateCampaignHeroesToReplace() { auto campaignState = gameState->scenarioOps->campState; - std::vector campaignHeroReplacements; std::vector placeholdersByPower; std::vector placeholdersByType; + campaignHeroReplacements.clear(); + // find all placeholders on map for(auto obj : gameState->map->objects) { @@ -375,7 +434,7 @@ std::vector CGameStateCampaign::generateCampaignHeroesT auto * heroPlaceholder = dynamic_cast(obj.get()); // only 1 field must be set - assert(heroPlaceholder->powerRank != heroPlaceholder->heroType); + assert(heroPlaceholder->powerRank.has_value() != heroPlaceholder->heroType.has_value()); if(heroPlaceholder->powerRank) placeholdersByPower.push_back(heroPlaceholder); @@ -394,16 +453,16 @@ std::vector CGameStateCampaign::generateCampaignHeroesT continue; } - CGHeroInstance * hero = CampaignState::crossoverDeserialize(node, gameState->map); + CGHeroInstance * hero = campaignState->crossoverDeserialize(node, gameState->map); - logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->getHeroType(), hero->getNameTranslated()); campaignHeroReplacements.emplace_back(hero, placeholder->id); } auto lastScenario = getHeroesSourceScenario(); - if (!placeholdersByPower.empty() && lastScenario) + if (lastScenario) { // sort hero placeholders descending power boost::range::sort(placeholdersByPower, [](const CGHeroPlaceholder * a, const CGHeroPlaceholder * b) @@ -419,15 +478,21 @@ std::vector CGameStateCampaign::generateCampaignHeroesT if (nodeListIter == nodeList.end()) break; - CGHeroInstance * hero = CampaignState::crossoverDeserialize(*nodeListIter, gameState->map); + CGHeroInstance * hero = campaignState->crossoverDeserialize(*nodeListIter, gameState->map); nodeListIter++; - logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->getHeroType(), hero->getNameTranslated()); campaignHeroReplacements.emplace_back(hero, placeholder->id); } + + // Add remaining heroes without placeholders - to transfer their artifacts to placed heroes + for (;nodeListIter != nodeList.end(); ++nodeListIter) + { + CGHeroInstance * hero = campaignState->crossoverDeserialize(*nodeListIter, gameState->map); + campaignHeroReplacements.emplace_back(hero, ObjectInstanceID::NONE); + } } - return campaignHeroReplacements; } void CGameStateCampaign::initHeroes() @@ -468,7 +533,7 @@ void CGameStateCampaign::initHeroes() { for (auto & heroe : heroes) { - if (heroe->subID == chosenBonus->info1) + if (heroe->getHeroType().getNum() == chosenBonus->info1) { giveCampaignBonusToHero(heroe); break; @@ -476,6 +541,16 @@ void CGameStateCampaign::initHeroes() } } } + + auto campaignState = gameState->scenarioOps->campState; + auto * yog = gameState->getUsedHero(HeroTypeID::SOLMYR); + if (yog && boost::starts_with(campaignState->getFilename(), "DATA/YOG") && campaignState->currentScenario()->getNum() == 2) + { + assert(yog->isCampaignYog()); + gameState->giveHeroArtifact(yog, ArtifactID::ANGELIC_ALLIANCE); + } + + transferMissingArtifacts(campaignState->scenario(*campaignState->currentScenario()).travelOptions); } void CGameStateCampaign::initStartingResources() @@ -498,7 +573,7 @@ void CGameStateCampaign::initStartingResources() std::vector people = getHumanPlayerInfo(); //players we will give resource bonus for(const PlayerSettings *ps : people) { - std::vector res; //resources we will give + std::vector res; //resources we will give switch (chosenBonus->info1) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: @@ -557,7 +632,7 @@ void CGameStateCampaign::initTowns() if(gameState->scenarioOps->campState->formatVCMI()) newBuilding = BuildingID(chosenBonus->info1); else - newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->subID, town->builtBuildings); + newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->getFaction(), town->builtBuildings); // Build granted building & all prerequisites - e.g. Mages Guild Lvl 3 should also give Mages Guild Lvl 1 & 2 while(true) @@ -589,9 +664,9 @@ bool CGameStateCampaign::playerHasStartingHero(PlayerColor playerColor) const return false; } -std::unique_ptr CGameStateCampaign::getCurrentMap() const +std::unique_ptr CGameStateCampaign::getCurrentMap() { - return gameState->scenarioOps->campState->getMap(CampaignScenarioID::NONE); + return gameState->scenarioOps->campState->getMap(CampaignScenarioID::NONE, gameState->callback); } VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameStateCampaign.h b/lib/gameState/CGameStateCampaign.h index 376ec0ca3..2614d0621 100644 --- a/lib/gameState/CGameStateCampaign.h +++ b/lib/gameState/CGameStateCampaign.h @@ -25,24 +25,30 @@ struct CampaignHeroReplacement CampaignHeroReplacement(CGHeroInstance * hero, const ObjectInstanceID & heroPlaceholderId); CGHeroInstance * hero; ObjectInstanceID heroPlaceholderId; + std::vector transferrableArtifacts; }; class CGameStateCampaign { CGameState * gameState; + /// Contains list of heroes that may be available in this scenario + /// temporary helper for game initialization, not serialized + std::vector campaignHeroReplacements; + /// Returns ID of scenario from which hero placeholders should be selected std::optional getHeroesSourceScenario() const; /// returns heroes and placeholders in where heroes will be put - std::vector generateCampaignHeroesToReplace(); + void generateCampaignHeroesToReplace(); std::optional currentBonus() const; /// Trims hero parameters that should not transfer between scenarios according to travelOptions flags - void trimCrossoverHeroesParameters(std::vector & campaignHeroReplacements, const CampaignTravel & travelOptions); + void trimCrossoverHeroesParameters(const CampaignTravel & travelOptions); - void replaceHeroesPlaceholders(const std::vector & campaignHeroReplacements); + void replaceHeroesPlaceholders(); + void transferMissingArtifacts(const CampaignTravel & travelOptions); void giveCampaignBonusToHero(CGHeroInstance * hero); @@ -56,9 +62,9 @@ public: void initTowns(); bool playerHasStartingHero(PlayerColor player) const; - std::unique_ptr getCurrentMap() const; + std::unique_ptr getCurrentMap(); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & gameState; } diff --git a/lib/gameState/EVictoryLossCheckResult.h b/lib/gameState/EVictoryLossCheckResult.h index 1091b4a3a..c6f056222 100644 --- a/lib/gameState/EVictoryLossCheckResult.h +++ b/lib/gameState/EVictoryLossCheckResult.h @@ -58,7 +58,7 @@ public: MetaString messageToSelf; MetaString messageToOthers; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & intValue; h & messageToSelf; diff --git a/lib/gameState/QuestInfo.h b/lib/gameState/QuestInfo.h index b89e467da..37e35de23 100644 --- a/lib/gameState/QuestInfo.h +++ b/lib/gameState/QuestInfo.h @@ -44,7 +44,7 @@ struct DLL_LINKAGE QuestInfo //universal interface for human and AI return (quest == qi.quest && obj == qi.obj); } - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & quest; h & obj; diff --git a/lib/gameState/SThievesGuildInfo.h b/lib/gameState/SThievesGuildInfo.h index b712d543e..6e4c06aeb 100644 --- a/lib/gameState/SThievesGuildInfo.h +++ b/lib/gameState/SThievesGuildInfo.h @@ -23,9 +23,9 @@ struct DLL_LINKAGE SThievesGuildInfo std::map colorToBestHero; //maps player's color to his best heros' std::map personality; // color to personality // ai tactic - std::map bestCreature; // color to ID // id or -1 if not known + std::map bestCreature; // color to ID // id or -1 if not known -// template void serialize(Handler &h, const int version) +// template void serialize(Handler &h) // { // h & playerColors; // h & numOfTowns; diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index 4dabd8ab3..bafaf28b0 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -25,7 +25,7 @@ std::map TavernHeroesPool::unusedHeroesFromPool() c { std::map pool = heroesPool; for(const auto & slot : currentTavern) - pool.erase(HeroTypeID(slot.hero->subID)); + pool.erase(slot.hero->getHeroType()); return pool; } @@ -34,13 +34,13 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const { for (auto const & slot : currentTavern) { - if (HeroTypeID(slot.hero->subID) == hero) + if (slot.hero->getHeroType() == hero) return slot.role; } return TavernSlotRole::NONE; } -void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role) +void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints) { vstd::erase_if(currentTavern, [&](const TavernSlot & entry){ return entry.player == player && entry.slot == slot; @@ -54,6 +54,12 @@ void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, if (h && army) h->setToArmy(army); + if (h && replenishPoints) + { + h->setMovementPoints(h->movementPointsLimit(true)); + h->mana = h->manaLimit(); + } + TavernSlot newSlot; newSlot.hero = h; newSlot.player = player; @@ -126,13 +132,13 @@ void TavernHeroesPool::onNewDay() continue; hero.second->setMovementPoints(hero.second->movementPointsLimit(true)); - hero.second->mana = hero.second->manaLimit(); + hero.second->mana = hero.second->getManaNewTurn(); } } void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero) { - heroesPool[HeroTypeID(hero->subID)] = hero; + heroesPool[hero->getHeroType()] = hero; } void TavernHeroesPool::setAvailability(HeroTypeID hero, std::set mask) diff --git a/lib/gameState/TavernHeroesPool.h b/lib/gameState/TavernHeroesPool.h index fb5dc136f..ceab10c15 100644 --- a/lib/gameState/TavernHeroesPool.h +++ b/lib/gameState/TavernHeroesPool.h @@ -30,7 +30,7 @@ class DLL_LINKAGE TavernHeroesPool TavernSlotRole role; PlayerColor player; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & hero; h & slot; @@ -74,9 +74,9 @@ public: void setAvailability(HeroTypeID hero, std::set mask); /// Makes hero available in tavern of specified player - void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role); + void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & heroesPool; h & perPlayerAvailability; diff --git a/lib/int3.h b/lib/int3.h index e8a68e8ef..1307255d0 100644 --- a/lib/int3.h +++ b/lib/int3.h @@ -15,7 +15,9 @@ VCMI_LIB_NAMESPACE_BEGIN class int3 { public: - si32 x, y, z; + si32 x; + si32 y; + si32 z; //c-tor: x, y, z initialized to 0 constexpr int3() : x(0), y(0), z(0) {} // I think that x, y, z should be left uninitialized. @@ -82,19 +84,13 @@ public: constexpr bool operator<(const int3 & i) const { - if (z < i.z) - return true; - if (z > i.z) - return false; - if (y < i.y) - return true; - if (y > i.y) - return false; - if (x < i.x) - return true; - if (x > i.x) - return false; - return false; + if (z != i.z) + return z < i.z; + + if (y != i.y) + return y < i.y; + + return x < i.x; } enum EDistanceFormula @@ -168,7 +164,7 @@ public: } template - void serialize(Handler &h, const int version) + void serialize(Handler &h) { h & x; h & y; @@ -180,12 +176,25 @@ public: return { { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) } }; } + + // Solution by ChatGPT + + // Assume values up to +- 1000 + friend std::size_t hash_value(const int3& v) { + // Since the range is [-1000, 1000], offsetting by 1000 maps it to [0, 2000] + std::size_t hx = v.x + 1000; + std::size_t hy = v.y + 1000; + std::size_t hz = v.z + 1000; + + // Combine the hash values, multiplying them by prime numbers + return ((hx * 4000037u) ^ (hy * 2003u)) + hz; + } }; template int3 findClosestTile (Container & container, int3 dest) { - static_assert(std::is_same::value, + static_assert(std::is_same_v, "findClosestTile requires container."); int3 result(-1, -1, -1); @@ -204,14 +213,9 @@ int3 findClosestTile (Container & container, int3 dest) VCMI_LIB_NAMESPACE_END - template<> struct std::hash { - size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const - { - size_t ret = std::hash()(pos.x); - VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.y); - VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.z); - return ret; + std::size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const noexcept { + return hash_value(pos); } }; diff --git a/lib/JsonNode.cpp b/lib/json/JsonBonus.cpp similarity index 55% rename from lib/JsonNode.cpp rename to lib/json/JsonBonus.cpp index 869e632ce..b56de5786 100644 --- a/lib/JsonNode.cpp +++ b/lib/json/JsonBonus.cpp @@ -1,5 +1,5 @@ /* - * JsonNode.cpp, part of VCMI engine + * JsonUtils.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,414 +9,51 @@ */ #include "StdInc.h" -#include "JsonNode.h" +#include "JsonBonus.h" -#include "ScopeGuard.h" +#include "JsonValidator.h" -#include "bonuses/BonusParams.h" -#include "bonuses/Bonus.h" -#include "bonuses/Limiters.h" -#include "bonuses/Propagators.h" -#include "bonuses/Updaters.h" -#include "filesystem/Filesystem.h" -#include "modding/IdentifierStorage.h" -#include "VCMI_Lib.h" //for identifier resolution -#include "CGeneralTextHandler.h" -#include "JsonDetail.h" -#include "constants/StringConstants.h" -#include "battle/BattleHex.h" +#include "../CGeneralTextHandler.h" +#include "../VCMI_Lib.h" +#include "../bonuses/BonusParams.h" +#include "../bonuses/Limiters.h" +#include "../bonuses/Propagators.h" +#include "../bonuses/Updaters.h" +#include "../constants/StringConstants.h" +#include "../modding/IdentifierStorage.h" -namespace +VCMI_LIB_USING_NAMESPACE + +template +const T parseByMap(const std::map & map, const JsonNode * val, const std::string & err) { -// to avoid duplicating const and non-const code -template -Node & resolvePointer(Node & in, const std::string & pointer) -{ - if(pointer.empty()) - return in; - assert(pointer[0] == '/'); - - size_t splitPos = pointer.find('/', 1); - - std::string entry = pointer.substr(1, splitPos - 1); - std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); - - if(in.getType() == VCMI_LIB_WRAP_NAMESPACE(JsonNode)::JsonType::DATA_VECTOR) + if (!val->isNull()) { - if(entry.find_first_not_of("0123456789") != std::string::npos) // non-numbers in string - throw std::runtime_error("Invalid Json pointer"); - - if(entry.size() > 1 && entry[0] == '0') // leading zeros are not allowed - throw std::runtime_error("Invalid Json pointer"); - - auto index = boost::lexical_cast(entry); - - if (in.Vector().size() > index) - return in.Vector()[index].resolvePointer(remainer); - } - return in[entry].resolvePointer(remainer); -} -} - -VCMI_LIB_NAMESPACE_BEGIN - -using namespace JsonDetail; - -class LibClasses; -class CModHandler; - -static const JsonNode nullNode; - -JsonNode::JsonNode(JsonType Type) -{ - setType(Type); -} - -JsonNode::JsonNode(const char *data, size_t datasize) -{ - JsonParser parser(data, datasize); - *this = parser.parse(""); -} - -JsonNode::JsonNode(const JsonPath & fileURI) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(const std::string & idx, const JsonPath & fileURI) -{ - auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); - isValidSyntax = parser.isValid(); -} - -bool JsonNode::operator == (const JsonNode &other) const -{ - return data == other.data; -} - -bool JsonNode::operator != (const JsonNode &other) const -{ - return !(*this == other); -} - -JsonNode::JsonType JsonNode::getType() const -{ - return static_cast(data.index()); -} - -void JsonNode::setMeta(const std::string & metadata, bool recursive) -{ - meta = metadata; - if (recursive) - { - switch (getType()) + const std::string & type = val->String(); + auto it = map.find(type); + if (it == map.end()) { - break; case JsonType::DATA_VECTOR: - { - for(auto & node : Vector()) - { - node.setMeta(metadata); - } - } - break; case JsonType::DATA_STRUCT: - { - for(auto & node : Struct()) - { - node.second.setMeta(metadata); - } - } + logMod->error("Error: invalid %s%s.", err, type); + return {}; + } + else + { + return it->second; } } + else + return {}; } -void JsonNode::setType(JsonType Type) +template +const T parseByMapN(const std::map & map, const JsonNode * val, const std::string & err) { - if (getType() == Type) - return; - - //float<->int conversion - if(getType() == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) - { - si64 converted = static_cast(std::get(data)); - data = JsonData(converted); - return; - } - else if(getType() == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) - { - double converted = static_cast(std::get(data)); - data = JsonData(converted); - return; - } - - //Set new node type - switch(Type) - { - break; case JsonType::DATA_NULL: data = JsonData(); - break; case JsonType::DATA_BOOL: data = JsonData(false); - break; case JsonType::DATA_FLOAT: data = JsonData(static_cast(0.0)); - break; case JsonType::DATA_STRING: data = JsonData(std::string()); - break; case JsonType::DATA_VECTOR: data = JsonData(JsonVector()); - break; case JsonType::DATA_STRUCT: data = JsonData(JsonMap()); - break; case JsonType::DATA_INTEGER: data = JsonData(static_cast(0)); - } + if(val->isNumber()) + return static_cast(val->Integer()); + else + return parseByMap(map, val, err); } -bool JsonNode::isNull() const -{ - return getType() == JsonType::DATA_NULL; -} - -bool JsonNode::isNumber() const -{ - return getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT; -} - -bool JsonNode::isString() const -{ - return getType() == JsonType::DATA_STRING; -} - -bool JsonNode::isVector() const -{ - return getType() == JsonType::DATA_VECTOR; -} - -bool JsonNode::isStruct() const -{ - return getType() == JsonType::DATA_STRUCT; -} - -bool JsonNode::containsBaseData() const -{ - switch(getType()) - { - case JsonType::DATA_NULL: - return false; - case JsonType::DATA_STRUCT: - for(const auto & elem : Struct()) - { - if(elem.second.containsBaseData()) - return true; - } - return false; - default: - //other types (including vector) cannot be extended via merge - return true; - } -} - -bool JsonNode::isCompact() const -{ - switch(getType()) - { - case JsonType::DATA_VECTOR: - for(const JsonNode & elem : Vector()) - { - if(!elem.isCompact()) - return false; - } - return true; - case JsonType::DATA_STRUCT: - { - auto propertyCount = Struct().size(); - if(propertyCount == 0) - return true; - else if(propertyCount == 1) - return Struct().begin()->second.isCompact(); - } - return false; - default: - return true; - } -} - -bool JsonNode::TryBoolFromString(bool & success) const -{ - success = true; - if(getType() == JsonNode::JsonType::DATA_BOOL) - return Bool(); - - success = getType() == JsonNode::JsonType::DATA_STRING; - if(success) - { - auto boolParamStr = String(); - boost::algorithm::trim(boolParamStr); - boost::algorithm::to_lower(boolParamStr); - success = boolParamStr == "true"; - - if(success) - return true; - - success = boolParamStr == "false"; - } - return false; -} - -void JsonNode::clear() -{ - setType(JsonType::DATA_NULL); -} - -bool & JsonNode::Bool() -{ - setType(JsonType::DATA_BOOL); - return std::get(data); -} - -double & JsonNode::Float() -{ - setType(JsonType::DATA_FLOAT); - return std::get(data); -} - -si64 & JsonNode::Integer() -{ - setType(JsonType::DATA_INTEGER); - return std::get(data); -} - -std::string & JsonNode::String() -{ - setType(JsonType::DATA_STRING); - return std::get(data); -} - -JsonVector & JsonNode::Vector() -{ - setType(JsonType::DATA_VECTOR); - return std::get(data); -} - -JsonMap & JsonNode::Struct() -{ - setType(JsonType::DATA_STRUCT); - return std::get(data); -} - -const bool boolDefault = false; -bool JsonNode::Bool() const -{ - if (getType() == JsonType::DATA_NULL) - return boolDefault; - assert(getType() == JsonType::DATA_BOOL); - return std::get(data); -} - -const double floatDefault = 0; -double JsonNode::Float() const -{ - if(getType() == JsonType::DATA_NULL) - return floatDefault; - - if(getType() == JsonType::DATA_INTEGER) - return static_cast(std::get(data)); - - assert(getType() == JsonType::DATA_FLOAT); - return std::get(data); -} - -const si64 integetDefault = 0; -si64 JsonNode::Integer() const -{ - if(getType() == JsonType::DATA_NULL) - return integetDefault; - - if(getType() == JsonType::DATA_FLOAT) - return static_cast(std::get(data)); - - assert(getType() == JsonType::DATA_INTEGER); - return std::get(data); -} - -const std::string stringDefault = std::string(); -const std::string & JsonNode::String() const -{ - if (getType() == JsonType::DATA_NULL) - return stringDefault; - assert(getType() == JsonType::DATA_STRING); - return std::get(data); -} - -const JsonVector vectorDefault = JsonVector(); -const JsonVector & JsonNode::Vector() const -{ - if (getType() == JsonType::DATA_NULL) - return vectorDefault; - assert(getType() == JsonType::DATA_VECTOR); - return std::get(data); -} - -const JsonMap mapDefault = JsonMap(); -const JsonMap & JsonNode::Struct() const -{ - if (getType() == JsonType::DATA_NULL) - return mapDefault; - assert(getType() == JsonType::DATA_STRUCT); - return std::get(data); -} - -JsonNode & JsonNode::operator[](const std::string & child) -{ - return Struct()[child]; -} - -const JsonNode & JsonNode::operator[](const std::string & child) const -{ - auto it = Struct().find(child); - if (it != Struct().end()) - return it->second; - return nullNode; -} - -JsonNode & JsonNode::operator[](size_t child) -{ - if (child >= Vector().size() ) - Vector().resize(child + 1); - return Vector()[child]; -} - -const JsonNode & JsonNode::operator[](size_t child) const -{ - if (child < Vector().size() ) - return Vector()[child]; - - return nullNode; -} - -const JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) const -{ - return ::resolvePointer(*this, jsonPointer); -} - -JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) -{ - return ::resolvePointer(*this, jsonPointer); -} - -std::string JsonNode::toJson(bool compact) const -{ - std::ostringstream out; - JsonWriter writer(out, compact); - writer.writeNode(*this); - return out.str(); -} - -///JsonUtils - static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const JsonNode & node) { if (node.isNull()) @@ -427,14 +64,14 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso if (node.isNumber()) // Compatibility code for 1.3 or older { - logMod->warn("Bonus subtype must be string!"); + logMod->warn("Bonus subtype must be string! (%s)", node.getModScope()); subtype = BonusCustomSubtype(node.Integer()); return; } if (!node.isString()) { - logMod->warn("Bonus subtype must be string!"); + logMod->warn("Bonus subtype must be string! (%s)", node.getModScope()); subtype = BonusSubtypeID(); return; } @@ -527,6 +164,7 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso case BonusType::NEGATE_ALL_NATURAL_IMMUNITIES: case BonusType::CREATURE_DAMAGE: case BonusType::FLYING: + case BonusType::FIRST_STRIKE: case BonusType::GENERAL_DAMAGE_REDUCTION: case BonusType::PERCENTAGE_DAMAGE_BOOST: case BonusType::SOUL_STEAL: @@ -656,6 +294,77 @@ static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource } } +static BonusParams convertDeprecatedBonus(const JsonNode &ability) +{ + if(vstd::contains(deprecatedBonusSet, ability["type"].String())) + { + logMod->warn("There is deprecated bonus found:\n%s\nTrying to convert...", ability.toString()); + auto params = BonusParams(ability["type"].String(), + ability["subtype"].isString() ? ability["subtype"].String() : "", + ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1); + if(params.isConverted) + { + if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && bonusValueMap.find(ability["valueType"].String())->second == BonusValueType::PERCENT_TO_BASE) //assume secondary skill special + { + params.valueType = BonusValueType::PERCENT_TO_TARGET_TYPE; + params.targetType = BonusSource::SECONDARY_SKILL; + } + + logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toString()); + return params; + } + else + logMod->error("Cannot convert bonus!\n%s", ability.toString()); + } + BonusParams ret; + ret.isConverted = false; + return ret; +} + +static TUpdaterPtr parseUpdater(const JsonNode & updaterJson) +{ + switch(updaterJson.getType()) + { + case JsonNode::JsonType::DATA_STRING: + return parseByMap(bonusUpdaterMap, &updaterJson, "updater type "); + break; + case JsonNode::JsonType::DATA_STRUCT: + if(updaterJson["type"].String() == "GROWS_WITH_LEVEL") + { + auto updater = std::make_shared(); + const JsonVector param = updaterJson["parameters"].Vector(); + updater->valPer20 = static_cast(param[0].Integer()); + if(param.size() > 1) + updater->stepSize = static_cast(param[1].Integer()); + return updater; + } + else if (updaterJson["type"].String() == "ARMY_MOVEMENT") + { + auto updater = std::make_shared(); + if(updaterJson["parameters"].isVector()) + { + const auto & param = updaterJson["parameters"].Vector(); + if(param.size() < 4) + logMod->warn("Invalid ARMY_MOVEMENT parameters, using default!"); + else + { + updater->base = static_cast(param.at(0).Integer()); + updater->divider = static_cast(param.at(1).Integer()); + updater->multiplier = static_cast(param.at(2).Integer()); + updater->max = static_cast(param.at(3).Integer()); + } + return updater; + } + } + else + logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String()); + break; + } + return nullptr; +} + +VCMI_LIB_NAMESPACE_BEGIN + std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) { auto b = std::make_shared(); @@ -676,37 +385,6 @@ std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) return b; } -template -const T parseByMap(const std::map & map, const JsonNode * val, const std::string & err) -{ - static T defaultValue = T(); - if (!val->isNull()) - { - const std::string & type = val->String(); - auto it = map.find(type); - if (it == map.end()) - { - logMod->error("Error: invalid %s%s.", err, type); - return defaultValue; - } - else - { - return it->second; - } - } - else - return defaultValue; -} - -template -const T parseByMapN(const std::map & map, const JsonNode * val, const std::string & err) -{ - if(val->isNumber()) - return static_cast(val->Integer()); - else - return parseByMap(map, val, err); -} - void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) { const JsonNode & value = node["addInfo"]; @@ -808,7 +486,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) const JsonVector & parameters = limiter["parameters"].Vector(); if(limiterType == "CREATURE_TYPE_LIMITER") { - std::shared_ptr creatureLimiter = std::make_shared(); + auto creatureLimiter = std::make_shared(); VLC->identifiers()->requestIdentifier("creature", parameters[0], [=](si32 creature) { creatureLimiter->setCreature(CreatureID(creature)); @@ -836,7 +514,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) } else { - std::shared_ptr bonusLimiter = std::make_shared(); + auto bonusLimiter = std::make_shared(); bonusLimiter->type = it->second; auto findSource = [&](const JsonNode & parameter) { @@ -880,7 +558,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) } else if(limiterType == "FACTION_LIMITER" || limiterType == "CREATURE_FACTION_LIMITER") //Second name is deprecated, 1.2 compat { - std::shared_ptr factionLimiter = std::make_shared(); + auto factionLimiter = std::make_shared(); VLC->identifiers()->requestIdentifier("faction", parameters[0], [=](si32 faction) { factionLimiter->faction = FactionID(faction); @@ -900,7 +578,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) } else if(limiterType == "CREATURE_TERRAIN_LIMITER") { - std::shared_ptr terrainLimiter = std::make_shared(); + auto terrainLimiter = std::make_shared(); if(!parameters.empty()) { VLC->identifiers()->requestIdentifier("terrain", parameters[0], [=](si32 terrain) @@ -940,7 +618,7 @@ std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) if (!parseBonus(ability, b.get())) { // caller code can not handle this case and presumes that returned bonus is always valid - logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); + logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toString()); b->type = BonusType::NONE; return b; } @@ -960,75 +638,6 @@ std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, c return b; } -static BonusParams convertDeprecatedBonus(const JsonNode &ability) -{ - if(vstd::contains(deprecatedBonusSet, ability["type"].String())) - { - logMod->warn("There is deprecated bonus found:\n%s\nTrying to convert...", ability.toJson()); - auto params = BonusParams(ability["type"].String(), - ability["subtype"].isString() ? ability["subtype"].String() : "", - ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1); - if(params.isConverted) - { - if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && bonusValueMap.find(ability["valueType"].String())->second == BonusValueType::PERCENT_TO_BASE) //assume secondary skill special - { - params.valueType = BonusValueType::PERCENT_TO_TARGET_TYPE; - params.targetType = BonusSource::SECONDARY_SKILL; - } - - logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toJson()); - return params; - } - else - logMod->error("Cannot convert bonus!\n%s", ability.toJson()); - } - BonusParams ret; - ret.isConverted = false; - return ret; -} - -static TUpdaterPtr parseUpdater(const JsonNode & updaterJson) -{ - switch(updaterJson.getType()) - { - case JsonNode::JsonType::DATA_STRING: - return parseByMap(bonusUpdaterMap, &updaterJson, "updater type "); - break; - case JsonNode::JsonType::DATA_STRUCT: - if(updaterJson["type"].String() == "GROWS_WITH_LEVEL") - { - std::shared_ptr updater = std::make_shared(); - const JsonVector param = updaterJson["parameters"].Vector(); - updater->valPer20 = static_cast(param[0].Integer()); - if(param.size() > 1) - updater->stepSize = static_cast(param[1].Integer()); - return updater; - } - else if (updaterJson["type"].String() == "ARMY_MOVEMENT") - { - std::shared_ptr updater = std::make_shared(); - if(updaterJson["parameters"].isVector()) - { - const auto & param = updaterJson["parameters"].Vector(); - if(param.size() < 4) - logMod->warn("Invalid ARMY_MOVEMENT parameters, using default!"); - else - { - updater->base = static_cast(param.at(0).Integer()); - updater->divider = static_cast(param.at(1).Integer()); - updater->multiplier = static_cast(param.at(2).Integer()); - updater->max = static_cast(param.at(3).Integer()); - } - return updater; - } - } - else - logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String()); - break; - } - return nullptr; -} - bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) { const JsonNode * value = nullptr; @@ -1122,7 +731,7 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) if (!value->isNull()) { //ALL_CREATURES old propagator compatibility - if(value->String() == "ALL_CREATURES") + if(value->String() == "ALL_CREATURES") { logMod->warn("ALL_CREATURES propagator is deprecated. Use GLOBAL_EFFECT propagator with CREATURES_ONLY limiter"); b->addLimiter(std::make_shared()); @@ -1159,7 +768,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) CSelector base = Selector::none; for(const auto & andN : value->Vector()) base = base.Or(parseSelector(andN)); - + ret = ret.And(base); } @@ -1169,7 +778,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) CSelector base = Selector::none; for(const auto & andN : value->Vector()) base = base.Or(parseSelector(andN)); - + ret = ret.And(base.Not()); } @@ -1214,7 +823,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) else if(src) ret = ret.And(Selector::sourceTypeSel(*src)); - + value = &ability["targetSourceType"]; if(value->isString()) { @@ -1253,373 +862,4 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) return ret; } -//returns first Key with value equal to given one -template -Key reverseMapFirst(const Val & val, const std::map & map) -{ - for(auto it : map) - { - if(it.second == val) - { - return it.first; - } - } - assert(0); - return ""; -} - -static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName) -{ - const JsonNode & fieldProps = schema["properties"][fieldName]; - -#if defined(VCMI_IOS) - if (!fieldProps["defaultIOS"].isNull()) - return fieldProps["defaultIOS"]; -#elif defined(VCMI_ANDROID) - if (!fieldProps["defaultAndroid"].isNull()) - return fieldProps["defaultAndroid"]; -#elif !defined(VCMI_MOBILE) - if (!fieldProps["defaultDesktop"].isNull()) - return fieldProps["defaultDesktop"]; -#endif - return fieldProps["default"]; -} - -static void eraseOptionalNodes(JsonNode & node, const JsonNode & schema) -{ - assert(schema["type"].String() == "object"); - - std::set foundEntries; - - for(const auto & entry : schema["required"].Vector()) - foundEntries.insert(entry.String()); - - vstd::erase_if(node.Struct(), [&](const auto & node){ - return !vstd::contains(foundEntries, node.first); - }); -} - -static void minimizeNode(JsonNode & node, const JsonNode & schema) -{ - if (schema["type"].String() != "object") - return; - - for(const auto & entry : schema["required"].Vector()) - { - const std::string & name = entry.String(); - minimizeNode(node[name], schema["properties"][name]); - - if (vstd::contains(node.Struct(), name) && node[name] == getDefaultValue(schema, name)) - node.Struct().erase(name); - } - eraseOptionalNodes(node, schema); -} - -static void maximizeNode(JsonNode & node, const JsonNode & schema) -{ - // "required" entry can only be found in object/struct - if (schema["type"].String() != "object") - return; - - // check all required entries that have default version - for(const auto & entry : schema["required"].Vector()) - { - const std::string & name = entry.String(); - - if (node[name].isNull() && !getDefaultValue(schema, name).isNull()) - node[name] = getDefaultValue(schema, name); - - maximizeNode(node[name], schema["properties"][name]); - } - - eraseOptionalNodes(node, schema); -} - -void JsonUtils::minimize(JsonNode & node, const std::string & schemaName) -{ - minimizeNode(node, getSchema(schemaName)); -} - -void JsonUtils::maximize(JsonNode & node, const std::string & schemaName) -{ - maximizeNode(node, getSchema(schemaName)); -} - -bool JsonUtils::validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName) -{ - std::string log = Validation::check(schemaName, node); - if (!log.empty()) - { - logMod->warn("Data in %s is invalid!", dataName); - logMod->warn(log); - logMod->trace("%s json: %s", dataName, node.toJson(true)); - } - return log.empty(); -} - -const JsonNode & getSchemaByName(const std::string & name) -{ - // cached schemas to avoid loading json data multiple times - static std::map loadedSchemas; - - if (vstd::contains(loadedSchemas, name)) - return loadedSchemas[name]; - - auto filename = JsonPath::builtin("config/schemas/" + name); - - if (CResourceHandler::get()->existsResource(filename)) - { - loadedSchemas[name] = JsonNode(filename); - return loadedSchemas[name]; - } - - logMod->error("Error: missing schema with name %s!", name); - assert(0); - return nullNode; -} - -const JsonNode & JsonUtils::getSchema(const std::string & URI) -{ - size_t posColon = URI.find(':'); - size_t posHash = URI.find('#'); - std::string filename; - if(posColon == std::string::npos) - { - filename = URI.substr(0, posHash); - } - else - { - std::string protocolName = URI.substr(0, posColon); - filename = URI.substr(posColon + 1, posHash - posColon - 1) + ".json"; - if(protocolName != "vcmi") - { - logMod->error("Error: unsupported URI protocol for schema: %s", URI); - return nullNode; - } - } - - // check if json pointer if present (section after hash in string) - if(posHash == std::string::npos || posHash == URI.size() - 1) - { - auto const & result = getSchemaByName(filename); - if (result.isNull()) - logMod->error("Error: missing schema %s", URI); - return result; - } - else - { - auto const & result = getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); - if (result.isNull()) - logMod->error("Error: missing schema %s", URI); - return result; - } -} - -void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, bool copyMeta) -{ - if (dest.getType() == JsonNode::JsonType::DATA_NULL) - { - std::swap(dest, source); - return; - } - - switch (source.getType()) - { - case JsonNode::JsonType::DATA_NULL: - { - dest.clear(); - break; - } - case JsonNode::JsonType::DATA_BOOL: - case JsonNode::JsonType::DATA_FLOAT: - case JsonNode::JsonType::DATA_INTEGER: - case JsonNode::JsonType::DATA_STRING: - case JsonNode::JsonType::DATA_VECTOR: - { - std::swap(dest, source); - break; - } - case JsonNode::JsonType::DATA_STRUCT: - { - if(!ignoreOverride && vstd::contains(source.flags, "override")) - { - std::swap(dest, source); - } - else - { - if (copyMeta) - dest.meta = source.meta; - - //recursively merge all entries from struct - for(auto & node : source.Struct()) - merge(dest[node.first], node.second, ignoreOverride); - } - } - } -} - -void JsonUtils::mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride, bool copyMeta) -{ - // uses copy created in stack to safely merge two nodes - merge(dest, source, ignoreOverride, copyMeta); -} - -void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) -{ - JsonNode inheritedNode(base); - merge(inheritedNode, descendant, true, true); - std::swap(descendant, inheritedNode); -} - -JsonNode JsonUtils::intersect(const std::vector & nodes, bool pruneEmpty) -{ - if(nodes.empty()) - return nullNode; - - JsonNode result = nodes[0]; - for(int i = 1; i < nodes.size(); i++) - { - if(result.isNull()) - break; - result = JsonUtils::intersect(result, nodes[i], pruneEmpty); - } - return result; -} - -JsonNode JsonUtils::intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty) -{ - if(a.getType() == JsonNode::JsonType::DATA_STRUCT && b.getType() == JsonNode::JsonType::DATA_STRUCT) - { - // intersect individual properties - JsonNode result(JsonNode::JsonType::DATA_STRUCT); - for(const auto & property : a.Struct()) - { - if(vstd::contains(b.Struct(), property.first)) - { - JsonNode propertyIntersect = JsonUtils::intersect(property.second, b.Struct().find(property.first)->second); - if(pruneEmpty && !propertyIntersect.containsBaseData()) - continue; - result[property.first] = propertyIntersect; - } - } - return result; - } - else - { - // not a struct - same or different, no middle ground - if(a == b) - return a; - } - return nullNode; -} - -JsonNode JsonUtils::difference(const JsonNode & node, const JsonNode & base) -{ - auto addsInfo = [](JsonNode diff) -> bool - { - switch(diff.getType()) - { - case JsonNode::JsonType::DATA_NULL: - return false; - case JsonNode::JsonType::DATA_STRUCT: - return !diff.Struct().empty(); - default: - return true; - } - }; - - if(node.getType() == JsonNode::JsonType::DATA_STRUCT && base.getType() == JsonNode::JsonType::DATA_STRUCT) - { - // subtract individual properties - JsonNode result(JsonNode::JsonType::DATA_STRUCT); - for(const auto & property : node.Struct()) - { - if(vstd::contains(base.Struct(), property.first)) - { - const JsonNode propertyDifference = JsonUtils::difference(property.second, base.Struct().find(property.first)->second); - if(addsInfo(propertyDifference)) - result[property.first] = propertyDifference; - } - else - { - result[property.first] = property.second; - } - } - return result; - } - else - { - if(node == base) - return nullNode; - } - return node; -} - -JsonNode JsonUtils::assembleFromFiles(const std::vector & files) -{ - bool isValid = false; - return assembleFromFiles(files, isValid); -} - -JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bool & isValid) -{ - isValid = true; - JsonNode result; - - for(const auto & file : files) - { - bool isValidFile = false; - JsonNode section(JsonPath::builtinTODO(file), isValidFile); - merge(result, section); - isValid |= isValidFile; - } - return result; -} - -JsonNode JsonUtils::assembleFromFiles(const std::string & filename) -{ - JsonNode result; - JsonPath resID = JsonPath::builtinTODO(filename); - - for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) - { - // FIXME: some way to make this code more readable - auto stream = loader->load(resID); - std::unique_ptr textData(new ui8[stream->getSize()]); - stream->read(textData.get(), stream->getSize()); - - JsonNode section(reinterpret_cast(textData.get()), stream->getSize()); - merge(result, section); - } - return result; -} - -DLL_LINKAGE JsonNode JsonUtils::boolNode(bool value) -{ - JsonNode node; - node.Bool() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::floatNode(double value) -{ - JsonNode node; - node.Float() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::stringNode(const std::string & value) -{ - JsonNode node; - node.String() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::intNode(si64 value) -{ - JsonNode node; - node.Integer() = value; - return node; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonBonus.h b/lib/json/JsonBonus.h new file mode 100644 index 000000000..db5ed492c --- /dev/null +++ b/lib/json/JsonBonus.h @@ -0,0 +1,33 @@ +/* + * JsonBonus.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "JsonNode.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct Bonus; +class ILimiter; +class CSelector; +class CAddInfo; + +namespace JsonUtils +{ + std::shared_ptr parseBonus(const JsonVector & ability_vec); + std::shared_ptr parseBonus(const JsonNode & ability); + std::shared_ptr parseBuildingBonus(const JsonNode & ability, const FactionID & faction, const BuildingID & building, const std::string & description); + bool parseBonus(const JsonNode & ability, Bonus * placement); + std::shared_ptr parseLimiter(const JsonNode & limiter); + CSelector parseSelector(const JsonNode &ability); + void resolveAddInfo(CAddInfo & var, const JsonNode & node); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonFormatException.h b/lib/json/JsonFormatException.h new file mode 100644 index 000000000..7e9f0fdc0 --- /dev/null +++ b/lib/json/JsonFormatException.h @@ -0,0 +1,20 @@ +/* + * JsonFormatException.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE JsonFormatException : public std::runtime_error +{ +public: + using runtime_error::runtime_error; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonNode.cpp b/lib/json/JsonNode.cpp new file mode 100644 index 000000000..17fbe4406 --- /dev/null +++ b/lib/json/JsonNode.cpp @@ -0,0 +1,506 @@ +/* + * JsonNode.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "JsonNode.h" + +#include "JsonParser.h" +#include "JsonWriter.h" +#include "filesystem/Filesystem.h" + +// to avoid duplicating const and non-const code +template +Node & resolvePointer(Node & in, const std::string & pointer) +{ + if(pointer.empty()) + return in; + assert(pointer[0] == '/'); + + size_t splitPos = pointer.find('/', 1); + + std::string entry = pointer.substr(1, splitPos - 1); + std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); + + if(in.getType() == VCMI_LIB_WRAP_NAMESPACE(JsonNode)::JsonType::DATA_VECTOR) + { + if(entry.find_first_not_of("0123456789") != std::string::npos) // non-numbers in string + throw std::runtime_error("Invalid Json pointer"); + + if(entry.size() > 1 && entry[0] == '0') // leading zeros are not allowed + throw std::runtime_error("Invalid Json pointer"); + + auto index = boost::lexical_cast(entry); + + if(in.Vector().size() > index) + return in.Vector()[index].resolvePointer(remainer); + } + return in[entry].resolvePointer(remainer); +} + +VCMI_LIB_NAMESPACE_BEGIN + +static const JsonNode nullNode; + +class LibClasses; +class CModHandler; + +JsonNode::JsonNode(bool boolean) + : data(boolean) +{ +} + +JsonNode::JsonNode(int32_t number) + : data(static_cast(number)) +{ +} + +JsonNode::JsonNode(uint32_t number) + : data(static_cast(number)) +{ +} + +JsonNode::JsonNode(int64_t number) + : data(number) +{ +} + +JsonNode::JsonNode(double number) + : data(number) +{ +} + +JsonNode::JsonNode(const char * string) + : data(std::string(string)) +{ +} + +JsonNode::JsonNode(const std::string & string) + : data(string) +{ +} + +JsonNode::JsonNode(const std::byte * data, size_t datasize) + : JsonNode(data, datasize, JsonParsingSettings()) +{ +} + +JsonNode::JsonNode(const std::byte * data, size_t datasize, const JsonParsingSettings & parserSettings) +{ + JsonParser parser(data, datasize, parserSettings); + *this = parser.parse(""); +} + +JsonNode::JsonNode(const JsonPath & fileURI) + :JsonNode(fileURI, JsonParsingSettings()) +{ +} + +JsonNode::JsonNode(const JsonPath & fileURI, const JsonParsingSettings & parserSettings) +{ + auto file = CResourceHandler::get()->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second, parserSettings); + *this = parser.parse(fileURI.getName()); +} + +JsonNode::JsonNode(const JsonPath & fileURI, const std::string & idx) +{ + auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second, JsonParsingSettings()); + *this = parser.parse(fileURI.getName()); +} + +JsonNode::JsonNode(const JsonPath & fileURI, bool & isValidSyntax) +{ + auto file = CResourceHandler::get()->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second, JsonParsingSettings()); + *this = parser.parse(fileURI.getName()); + isValidSyntax = parser.isValid(); +} + +bool JsonNode::operator==(const JsonNode & other) const +{ + return data == other.data; +} + +bool JsonNode::operator!=(const JsonNode & other) const +{ + return !(*this == other); +} + +JsonNode::JsonType JsonNode::getType() const +{ + return static_cast(data.index()); +} + +const std::string & JsonNode::getModScope() const +{ + return modScope; +} + +void JsonNode::setOverrideFlag(bool value) +{ + overrideFlag = value; +} + +bool JsonNode::getOverrideFlag() const +{ + return overrideFlag; +} + +void JsonNode::setModScope(const std::string & metadata, bool recursive) +{ + modScope = metadata; + if(recursive) + { + switch(getType()) + { + break; + case JsonType::DATA_VECTOR: + { + for(auto & node : Vector()) + { + node.setModScope(metadata); + } + } + break; + case JsonType::DATA_STRUCT: + { + for(auto & node : Struct()) + { + node.second.setModScope(metadata); + } + } + } + } +} + +void JsonNode::setType(JsonType Type) +{ + if(getType() == Type) + return; + + //float<->int conversion + if(getType() == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) + { + si64 converted = static_cast(std::get(data)); + data = JsonData(converted); + return; + } + else if(getType() == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) + { + double converted = static_cast(std::get(data)); + data = JsonData(converted); + return; + } + + //Set new node type + switch(Type) + { + case JsonType::DATA_NULL: + data = JsonData(); + break; + case JsonType::DATA_BOOL: + data = JsonData(false); + break; + case JsonType::DATA_FLOAT: + data = JsonData(0.0); + break; + case JsonType::DATA_STRING: + data = JsonData(std::string()); + break; + case JsonType::DATA_VECTOR: + data = JsonData(JsonVector()); + break; + case JsonType::DATA_STRUCT: + data = JsonData(JsonMap()); + break; + case JsonType::DATA_INTEGER: + data = JsonData(static_cast(0)); + break; + } +} + +bool JsonNode::isNull() const +{ + return getType() == JsonType::DATA_NULL; +} + +bool JsonNode::isNumber() const +{ + return getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT; +} + +bool JsonNode::isString() const +{ + return getType() == JsonType::DATA_STRING; +} + +bool JsonNode::isVector() const +{ + return getType() == JsonType::DATA_VECTOR; +} + +bool JsonNode::isStruct() const +{ + return getType() == JsonType::DATA_STRUCT; +} + +bool JsonNode::containsBaseData() const +{ + switch(getType()) + { + case JsonType::DATA_NULL: + return false; + case JsonType::DATA_STRUCT: + for(const auto & elem : Struct()) + { + if(elem.second.containsBaseData()) + return true; + } + return false; + default: + //other types (including vector) cannot be extended via merge + return true; + } +} + +bool JsonNode::isCompact() const +{ + switch(getType()) + { + case JsonType::DATA_VECTOR: + for(const JsonNode & elem : Vector()) + { + if(!elem.isCompact()) + return false; + } + return true; + case JsonType::DATA_STRUCT: + { + auto propertyCount = Struct().size(); + if(propertyCount == 0) + return true; + else if(propertyCount == 1) + return Struct().begin()->second.isCompact(); + } + return false; + default: + return true; + } +} + +bool JsonNode::TryBoolFromString(bool & success) const +{ + success = true; + if(getType() == JsonNode::JsonType::DATA_BOOL) + return Bool(); + + success = getType() == JsonNode::JsonType::DATA_STRING; + if(success) + { + auto boolParamStr = String(); + boost::algorithm::trim(boolParamStr); + boost::algorithm::to_lower(boolParamStr); + success = boolParamStr == "true"; + + if(success) + return true; + + success = boolParamStr == "false"; + } + return false; +} + +void JsonNode::clear() +{ + setType(JsonType::DATA_NULL); +} + +bool & JsonNode::Bool() +{ + setType(JsonType::DATA_BOOL); + return std::get(data); +} + +double & JsonNode::Float() +{ + setType(JsonType::DATA_FLOAT); + return std::get(data); +} + +si64 & JsonNode::Integer() +{ + setType(JsonType::DATA_INTEGER); + return std::get(data); +} + +std::string & JsonNode::String() +{ + setType(JsonType::DATA_STRING); + return std::get(data); +} + +JsonVector & JsonNode::Vector() +{ + setType(JsonType::DATA_VECTOR); + return std::get(data); +} + +JsonMap & JsonNode::Struct() +{ + setType(JsonType::DATA_STRUCT); + return std::get(data); +} + +bool JsonNode::Bool() const +{ + static const bool boolDefault = false; + + assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_BOOL); + + if(getType() == JsonType::DATA_BOOL) + return std::get(data); + + return boolDefault; +} + +double JsonNode::Float() const +{ + static const double floatDefault = 0; + + assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT); + + if(getType() == JsonType::DATA_FLOAT) + return std::get(data); + + if(getType() == JsonType::DATA_INTEGER) + return static_cast(std::get(data)); + + return floatDefault; +} + +si64 JsonNode::Integer() const +{ + static const si64 integerDefault = 0; + + assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT); + + if(getType() == JsonType::DATA_INTEGER) + return std::get(data); + + if(getType() == JsonType::DATA_FLOAT) + return static_cast(std::get(data)); + + return integerDefault; +} + +const std::string & JsonNode::String() const +{ + static const std::string stringDefault; + + assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_STRING); + + if(getType() == JsonType::DATA_STRING) + return std::get(data); + + return stringDefault; +} + +const JsonVector & JsonNode::Vector() const +{ + static const JsonVector vectorDefault; + + assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_VECTOR); + + if(getType() == JsonType::DATA_VECTOR) + return std::get(data); + + return vectorDefault; +} + +const JsonMap & JsonNode::Struct() const +{ + static const JsonMap mapDefault; + + assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_STRUCT); + + if(getType() == JsonType::DATA_STRUCT) + return std::get(data); + + return mapDefault; +} + +JsonNode & JsonNode::operator[](const std::string & child) +{ + return Struct()[child]; +} + +const JsonNode & JsonNode::operator[](const std::string & child) const +{ + auto it = Struct().find(child); + if(it != Struct().end()) + return it->second; + return nullNode; +} + +JsonNode & JsonNode::operator[](size_t child) +{ + if(child >= Vector().size()) + Vector().resize(child + 1); + return Vector()[child]; +} + +const JsonNode & JsonNode::operator[](size_t child) const +{ + if(child < Vector().size()) + return Vector()[child]; + + return nullNode; +} + +const JsonNode & JsonNode::resolvePointer(const std::string & jsonPointer) const +{ + return ::resolvePointer(*this, jsonPointer); +} + +JsonNode & JsonNode::resolvePointer(const std::string & jsonPointer) +{ + return ::resolvePointer(*this, jsonPointer); +} + +std::vector JsonNode::toBytes() const +{ + std::string jsonString = toString(); + auto dataBegin = reinterpret_cast(jsonString.data()); + auto dataEnd = dataBegin + jsonString.size(); + std::vector result(dataBegin, dataEnd); + return result; +} + +std::string JsonNode::toCompactString() const +{ + std::ostringstream out; + JsonWriter writer(out, true); + writer.writeNode(*this); + return out.str(); +} + +std::string JsonNode::toString() const +{ + std::ostringstream out; + JsonWriter writer(out, false); + writer.writeNode(*this); + return out.str(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonNode.h b/lib/json/JsonNode.h new file mode 100644 index 000000000..c4afbcae1 --- /dev/null +++ b/lib/json/JsonNode.h @@ -0,0 +1,232 @@ +/* + * JsonNode.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +using JsonMap = std::map; +using JsonVector = std::vector; + +struct DLL_LINKAGE JsonParsingSettings +{ + enum class JsonFormatMode + { + JSON, // strict implementation of json format + JSONC, // json format that also allows comments that start from '//' + JSON5 // Partial support of 'json5' format + }; + + JsonFormatMode mode = JsonFormatMode::JSON5; + + /// Maximum depth of elements + uint32_t maxDepth = 30; + + /// If set to true, parser will throw on any encountered error + bool strict = false; +}; + +class DLL_LINKAGE JsonNode +{ +public: + enum class JsonType + { + DATA_NULL, + DATA_BOOL, + DATA_FLOAT, + DATA_STRING, + DATA_VECTOR, + DATA_STRUCT, + DATA_INTEGER + }; + +private: + using JsonData = std::variant; + + JsonData data; + + /// Mod-origin of this particular field + std::string modScope; + + bool overrideFlag = false; + +public: + JsonNode() = default; + + /// Create single node with specified value + explicit JsonNode(bool boolean); + explicit JsonNode(int32_t number); + explicit JsonNode(uint32_t number); + explicit JsonNode(int64_t number); + explicit JsonNode(double number); + explicit JsonNode(const char * string); + explicit JsonNode(const std::string & string); + + /// Create tree from Json-formatted input + explicit JsonNode(const std::byte * data, size_t datasize); + explicit JsonNode(const std::byte * data, size_t datasize, const JsonParsingSettings & parserSettings); + + /// Create tree from JSON file + explicit JsonNode(const JsonPath & fileURI); + explicit JsonNode(const JsonPath & fileURI, const JsonParsingSettings & parserSettings); + explicit JsonNode(const JsonPath & fileURI, const std::string & modName); + explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); + + bool operator==(const JsonNode & other) const; + bool operator!=(const JsonNode & other) const; + + const std::string & getModScope() const; + void setModScope(const std::string & metadata, bool recursive = true); + + void setOverrideFlag(bool value); + bool getOverrideFlag() const; + + /// Convert node to another type. Converting to nullptr will clear all data + void setType(JsonType Type); + JsonType getType() const; + + bool isNull() const; + bool isNumber() const; + bool isString() const; + bool isVector() const; + bool isStruct() const; + /// true if node contains not-null data that cannot be extended via merging + /// used for generating common base node from multiple nodes (e.g. bonuses) + bool containsBaseData() const; + bool isCompact() const; + /// removes all data from node and sets type to null + void clear(); + + /// returns bool or bool equivalent of string value if 'success' is true, or false otherwise + bool TryBoolFromString(bool & success) const; + + /// non-const accessors, node will change type on type mismatch + bool & Bool(); + double & Float(); + si64 & Integer(); + std::string & String(); + JsonVector & Vector(); + JsonMap & Struct(); + + /// const accessors, will cause assertion failure on type mismatch + bool Bool() const; + ///float and integer allowed + double Float() const; + ///only integer allowed + si64 Integer() const; + const std::string & String() const; + const JsonVector & Vector() const; + const JsonMap & Struct() const; + + /// returns resolved "json pointer" (string in format "/path/to/node") + const JsonNode & resolvePointer(const std::string & jsonPointer) const; + JsonNode & resolvePointer(const std::string & jsonPointer); + + /// convert json tree into specified type. Json tree must have same type as Type + /// Valid types: bool, string, any numeric, map and vector + /// example: convertTo< std::map< std::vector > >(); + template + Type convertTo() const; + + //operator [], for structs only - get child node by name + JsonNode & operator[](const std::string & child); + const JsonNode & operator[](const std::string & child) const; + + JsonNode & operator[](size_t child); + const JsonNode & operator[](size_t child) const; + + std::string toCompactString() const; + std::string toString() const; + std::vector toBytes() const; + + template + void serialize(Handler & h) + { + h & modScope; + + if(h.version >= Handler::Version::JSON_FLAGS) + { + h & overrideFlag; + } + else + { + std::vector oldFlags; + h & oldFlags; + } + h & data; + } +}; + +namespace JsonDetail +{ + +inline void convert(bool & value, const JsonNode & node) +{ + value = node.Bool(); +} + +template +auto convert(T & value, const JsonNode & node) -> std::enable_if_t> +{ + value = node.Integer(); +} + +template +auto convert(T & value, const JsonNode & node) -> std::enable_if_t> +{ + value = node.Float(); +} + +inline void convert(std::string & value, const JsonNode & node) +{ + value = node.String(); +} + +template +void convert(std::map & value, const JsonNode & node) +{ + value.clear(); + for(const JsonMap::value_type & entry : node.Struct()) + value.insert(entry.first, entry.second.convertTo()); +} + +template +void convert(std::set & value, const JsonNode & node) +{ + value.clear(); + for(const JsonVector::value_type & entry : node.Vector()) + { + value.insert(entry.convertTo()); + } +} + +template +void convert(std::vector & value, const JsonNode & node) +{ + value.clear(); + for(const JsonVector::value_type & entry : node.Vector()) + { + value.push_back(entry.convertTo()); + } +} + +} + +template +Type JsonNode::convertTo() const +{ + Type result; + JsonDetail::convert(result, *this); + return result; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonParser.cpp b/lib/json/JsonParser.cpp new file mode 100644 index 000000000..e4c5f6f8c --- /dev/null +++ b/lib/json/JsonParser.cpp @@ -0,0 +1,596 @@ +/* + * JsonParser.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "JsonParser.h" + +#include "../ScopeGuard.h" +#include "../TextOperations.h" +#include "JsonFormatException.h" + +VCMI_LIB_NAMESPACE_BEGIN + +JsonParser::JsonParser(const std::byte * inputString, size_t stringSize, const JsonParsingSettings & settings) + : settings(settings) + , input(reinterpret_cast(inputString), stringSize) + , lineCount(1) + , currentDepth(0) + , lineStart(0) + , pos(0) +{ +} + +JsonNode JsonParser::parse(const std::string & fileName) +{ + JsonNode root; + + if(input.empty()) + { + error("File is empty", false); + } + else + { + if(!TextOperations::isValidUnicodeString(input.data(), input.size())) + error("Not a valid UTF-8 file", false); + + // If file starts with BOM - skip it + uint32_t firstCharacter = TextOperations::getUnicodeCodepoint(input.data(), input.size()); + if (firstCharacter == 0xFEFF) + pos += TextOperations::getUnicodeCharacterSize(input[0]); + + extractValue(root); + extractWhitespace(false); + + //Warn if there are any non-whitespace symbols left + if(pos < input.size()) + error("Not all file was parsed!", true); + } + + if(!errors.empty()) + { + logMod->warn("File %s is not a valid JSON file!", fileName); + logMod->warn(errors); + } + return root; +} + +bool JsonParser::isValid() +{ + return errors.empty(); +} + +bool JsonParser::extractSeparator() +{ + if(!extractWhitespace()) + return false; + + if(input[pos] != ':') + return error("Separator expected"); + + pos++; + return true; +} + +bool JsonParser::extractValue(JsonNode & node) +{ + if(!extractWhitespace()) + return false; + + switch(input[pos]) + { + case '\"': + case '\'': + return extractString(node); + case 'n': + return extractNull(node); + case 't': + return extractTrue(node); + case 'f': + return extractFalse(node); + case '{': + return extractStruct(node); + case '[': + return extractArray(node); + case '-': + case '+': + case '.': + return extractFloat(node); + default: + { + if(input[pos] >= '0' && input[pos] <= '9') + return extractFloat(node); + return error("Value expected!"); + } + } +} + +bool JsonParser::extractWhitespace(bool verbose) +{ + //TODO: JSON5 - C-style multi-line comments + //TODO: JSON5 - Additional white space characters are allowed + + while(true) + { + while(pos < input.size() && static_cast(input[pos]) <= ' ') + { + if(input[pos] == '\n') + { + lineCount++; + lineStart = pos + 1; + } + pos++; + } + + if(pos >= input.size() || input[pos] != '/') + break; + + if(settings.mode == JsonParsingSettings::JsonFormatMode::JSON) + error("Comments are not permitted in json!", true); + + pos++; + if(pos == input.size()) + break; + if(input[pos] == '/') + pos++; + else + error("Comments must consist of two slashes!", true); + + while(pos < input.size() && input[pos] != '\n') + pos++; + } + + if(pos >= input.size() && verbose) + return error("Unexpected end of file!"); + return true; +} + +bool JsonParser::extractEscaping(std::string & str) +{ + // TODO: support unicode escaping: + // \u1234 + + switch(input[pos]) + { + case '\"': + str += '\"'; + break; + case '\\': + str += '\\'; + break; + case 'b': + str += '\b'; + break; + case 'f': + str += '\f'; + break; + case 'n': + str += '\n'; + break; + case 'r': + str += '\r'; + break; + case 't': + str += '\t'; + break; + case '/': + str += '/'; + break; + default: + return error("Unknown escape sequence!", true); + } + return true; +} + +bool JsonParser::extractString(std::string & str) +{ + //TODO: JSON5 - line breaks escaping + + if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5) + { + if(input[pos] != '\"') + return error("String expected!"); + } + else + { + if(input[pos] != '\"' && input[pos] != '\'') + return error("String expected!"); + } + + char lineTerminator = input[pos]; + pos++; + + size_t first = pos; + + while(pos != input.size()) + { + if(input[pos] == lineTerminator) // Correct end of string + { + str.append(&input[first], pos - first); + pos++; + return true; + } + if(input[pos] == '\\') // Escaping + { + str.append(&input[first], pos - first); + pos++; + if(pos == input.size()) + break; + extractEscaping(str); + first = pos + 1; + } + if(input[pos] == '\n') // end-of-line + { + str.append(&input[first], pos - first); + return error("Closing quote not found!", true); + } + if(static_cast(input[pos]) < ' ') // control character + { + str.append(&input[first], pos - first); + first = pos + 1; + error("Illegal character in the string!", true); + } + pos++; + } + return error("Unterminated string!"); +} + +bool JsonParser::extractString(JsonNode & node) +{ + std::string str; + if(!extractString(str)) + return false; + + node.setType(JsonNode::JsonType::DATA_STRING); + node.String() = str; + return true; +} + +bool JsonParser::extractLiteral(std::string & literal) +{ + while(pos < input.size()) + { + bool isUpperCase = input[pos] >= 'A' && input[pos] <= 'Z'; + bool isLowerCase = input[pos] >= 'a' && input[pos] <= 'z'; + bool isNumber = input[pos] >= '0' && input[pos] <= '9'; + + if(!isUpperCase && !isLowerCase && !isNumber) + break; + + literal += input[pos]; + pos++; + } + + return true; +} + +bool JsonParser::extractAndCompareLiteral(const std::string & expectedLiteral) +{ + std::string literal; + if(!extractLiteral(literal)) + return false; + + if(literal != expectedLiteral) + { + return error("Expected " + expectedLiteral + ", but unknown literal found", true); + return false; + } + + return true; +} + +bool JsonParser::extractNull(JsonNode & node) +{ + if(!extractAndCompareLiteral("null")) + return false; + + node.clear(); + return true; +} + +bool JsonParser::extractTrue(JsonNode & node) +{ + if(!extractAndCompareLiteral("true")) + return false; + + node.Bool() = true; + return true; +} + +bool JsonParser::extractFalse(JsonNode & node) +{ + if(!extractAndCompareLiteral("false")) + return false; + + node.Bool() = false; + return true; +} + +bool JsonParser::extractStruct(JsonNode & node) +{ + node.setType(JsonNode::JsonType::DATA_STRUCT); + + if(currentDepth > settings.maxDepth) + error("Maximum allowed depth of json structure has been reached", true); + + pos++; + currentDepth++; + auto guard = vstd::makeScopeGuard([this]() + { + currentDepth--; + }); + + if(!extractWhitespace()) + return false; + + //Empty struct found + if(input[pos] == '}') + { + pos++; + return true; + } + + while(true) + { + if(!extractWhitespace()) + return false; + + bool overrideFlag = false; + std::string key; + + if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5) + { + if(!extractString(key)) + return false; + } + else + { + if(input[pos] == '\'' || input[pos] == '\"') + { + if(!extractString(key)) + return false; + } + else + { + if(!extractLiteral(key)) + return false; + } + } + + if(key.find('#') != std::string::npos) + { + // split key string into actual key and meta-flags + std::vector keyAndFlags; + boost::split(keyAndFlags, key, boost::is_any_of("#")); + + key = keyAndFlags[0]; + + for(int i = 1; i < keyAndFlags.size(); i++) + { + if(keyAndFlags[i] == "override") + overrideFlag = true; + else + error("Encountered unknown flag #" + keyAndFlags[i], true); + } + } + + if(node.Struct().find(key) != node.Struct().end()) + error("Duplicate element encountered!", true); + + if(!extractSeparator()) + return false; + + if(!extractElement(node.Struct()[key], '}')) + return false; + + node.Struct()[key].setOverrideFlag(overrideFlag); + + if(input[pos] == '}') + { + pos++; + return true; + } + } +} + +bool JsonParser::extractArray(JsonNode & node) +{ + if(currentDepth > settings.maxDepth) + error("Macimum allowed depth of json structure has been reached", true); + + currentDepth++; + auto guard = vstd::makeScopeGuard([this]() + { + currentDepth--; + }); + + pos++; + node.setType(JsonNode::JsonType::DATA_VECTOR); + + if(!extractWhitespace()) + return false; + + //Empty array found + if(input[pos] == ']') + { + pos++; + return true; + } + + while(true) + { + //NOTE: currently 50% of time is this vector resizing. + //May be useful to use list during parsing and then swap() all items to vector + node.Vector().resize(node.Vector().size() + 1); + + if(!extractElement(node.Vector().back(), ']')) + return false; + + if(input[pos] == ']') + { + pos++; + return true; + } + } +} + +bool JsonParser::extractElement(JsonNode & node, char terminator) +{ + if(!extractValue(node)) + return false; + + if(!extractWhitespace()) + return false; + + bool comma = (input[pos] == ','); + if(comma) + { + pos++; + if(!extractWhitespace()) + return false; + } + + if(input[pos] == terminator) + { + if(comma && settings.mode < JsonParsingSettings::JsonFormatMode::JSON5) + error("Extra comma found!", true); + + return true; + } + + if(!comma) + error("Comma expected!", true); + + return true; +} + +bool JsonParser::extractFloat(JsonNode & node) +{ + //TODO: JSON5 - hexacedimal support + //TODO: JSON5 - Numbers may be IEEE 754 positive infinity, negative infinity, and NaN (why?) + + assert(input[pos] == '-' || (input[pos] >= '0' && input[pos] <= '9')); + bool negative = false; + double result = 0; + si64 integerPart = 0; + bool isFloat = false; + + if(input[pos] == '+') + { + if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5) + error("Positive numbers should not have plus sign!", true); + pos++; + } + else if(input[pos] == '-') + { + pos++; + negative = true; + } + + if(input[pos] < '0' || input[pos] > '9') + { + if(input[pos] != '.' && settings.mode < JsonParsingSettings::JsonFormatMode::JSON5) + return error("Number expected!"); + } + + //Extract integer part + while(input[pos] >= '0' && input[pos] <= '9') + { + integerPart = integerPart * 10 + (input[pos] - '0'); + pos++; + } + + result = static_cast(integerPart); + + if(input[pos] == '.') + { + //extract fractional part + isFloat = true; + pos++; + double fractMult = 0.1; + + if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5 && (input[pos] < '0' || input[pos] > '9')) + return error("Decimal part expected!"); + + while(input[pos] >= '0' && input[pos] <= '9') + { + result = result + fractMult * (input[pos] - '0'); + fractMult /= 10; + pos++; + } + } + + if(input[pos] == 'e') + { + //extract exponential part + pos++; + isFloat = true; + bool powerNegative = false; + double power = 0; + + if(input[pos] == '-') + { + pos++; + powerNegative = true; + } + else if(input[pos] == '+') + { + pos++; + } + + if(input[pos] < '0' || input[pos] > '9') + return error("Exponential part expected!"); + + while(input[pos] >= '0' && input[pos] <= '9') + { + power = power * 10 + (input[pos] - '0'); + pos++; + } + + if(powerNegative) + power = -power; + + result *= std::pow(10, power); + } + + if(isFloat) + { + if(negative) + result = -result; + + node.setType(JsonNode::JsonType::DATA_FLOAT); + node.Float() = result; + } + else + { + if(negative) + integerPart = -integerPart; + + node.setType(JsonNode::JsonType::DATA_INTEGER); + node.Integer() = integerPart; + } + + return true; +} + +bool JsonParser::error(const std::string & message, bool warning) +{ + if(settings.strict) + throw JsonFormatException(message); + + std::ostringstream stream; + std::string type(warning ? " warning: " : " error: "); + + stream << "At line " << lineCount << ", position " << pos - lineStart << type << message << "\n"; + errors += stream.str(); + + return warning; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonParser.h b/lib/json/JsonParser.h new file mode 100644 index 000000000..8bf3259c7 --- /dev/null +++ b/lib/json/JsonParser.h @@ -0,0 +1,60 @@ +/* + * JsonParser.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +//Internal class for string -> JsonNode conversion +class JsonParser +{ + const JsonParsingSettings settings; + + std::string errors; // Contains description of all encountered errors + std::string_view input; // Input data + uint32_t lineCount; // Currently parsed line, starting from 1 + uint32_t currentDepth; + size_t lineStart; // Position of current line start + size_t pos; // Current position of parser + + //Helpers + bool extractEscaping(std::string & str); + bool extractLiteral(std::string & literal); + bool extractAndCompareLiteral(const std::string & expectedLiteral); + bool extractString(std::string & string); + bool extractWhitespace(bool verbose = true); + bool extractSeparator(); + bool extractElement(JsonNode & node, char terminator); + + //Methods for extracting JSON data + bool extractArray(JsonNode & node); + bool extractFalse(JsonNode & node); + bool extractFloat(JsonNode & node); + bool extractNull(JsonNode & node); + bool extractString(JsonNode & node); + bool extractStruct(JsonNode & node); + bool extractTrue(JsonNode & node); + bool extractValue(JsonNode & node); + + //Add error\warning message to list + bool error(const std::string & message, bool warning = false); + +public: + JsonParser(const std::byte * inputString, size_t stringSize, const JsonParsingSettings & settings); + + /// do actual parsing. filename is name of file that will printed to console if any errors were found + JsonNode parse(const std::string & fileName); + + /// returns true if parsing was successful + bool isValid(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonRandom.cpp b/lib/json/JsonRandom.cpp similarity index 68% rename from lib/JsonRandom.cpp rename to lib/json/JsonRandom.cpp index 2e78df61f..df71283b2 100644 --- a/lib/JsonRandom.cpp +++ b/lib/json/JsonRandom.cpp @@ -13,26 +13,26 @@ #include -#include "JsonNode.h" -#include "CRandomGenerator.h" -#include "constants/StringConstants.h" -#include "VCMI_Lib.h" -#include "CArtHandler.h" -#include "CCreatureHandler.h" -#include "CCreatureSet.h" -#include "spells/CSpellHandler.h" -#include "CSkillHandler.h" -#include "CHeroHandler.h" -#include "IGameCallback.h" -#include "mapObjects/IObjectInterface.h" -#include "modding/IdentifierStorage.h" -#include "modding/ModScope.h" +#include "JsonBonus.h" + +#include "../CRandomGenerator.h" +#include "../constants/StringConstants.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CCreatureHandler.h" +#include "../CCreatureSet.h" +#include "../spells/CSpellHandler.h" +#include "../CSkillHandler.h" +#include "../CHeroHandler.h" +#include "../IGameCallback.h" +#include "../gameState/CGameState.h" +#include "../mapObjects/IObjectInterface.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN -namespace JsonRandom -{ - si32 loadVariable(std::string variableGroup, const std::string & value, const Variables & variables, si32 defaultValue) + si32 JsonRandom::loadVariable(const std::string & variableGroup, const std::string & value, const Variables & variables, si32 defaultValue) { if (value.empty() || value[0] != '@') { @@ -50,12 +50,12 @@ namespace JsonRandom return variables.at(variableID); } - si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue) + si32 JsonRandom::loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue) { if(value.isNull()) return defaultValue; if(value.isNumber()) - return static_cast(value.Float()); + return value.Integer(); if (value.isString()) return loadVariable("number", value.String(), variables, defaultValue); @@ -69,63 +69,63 @@ namespace JsonRandom if(value.isStruct()) { if (!value["amount"].isNull()) - return static_cast(loadValue(value["amount"], rng, variables, defaultValue)); - si32 min = static_cast(loadValue(value["min"], rng, variables, 0)); - si32 max = static_cast(loadValue(value["max"], rng, variables, 0)); + return loadValue(value["amount"], rng, variables, defaultValue); + si32 min = loadValue(value["min"], rng, variables, 0); + si32 max = loadValue(value["max"], rng, variables, 0); return rng.getIntRange(min, max)(); } return defaultValue; } template - IdentifierType decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) + IdentifierType JsonRandom::decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) { if (value.empty() || value[0] != '@') - return IdentifierType(*VLC->identifiers()->getIdentifier(modScope, IdentifierType::entityType(), value)); + return IdentifierType(VLC->identifiers()->getIdentifier(modScope, IdentifierType::entityType(), value).value_or(-1)); else return loadVariable(IdentifierType::entityType(), value, variables, IdentifierType::NONE); } template - IdentifierType decodeKey(const JsonNode & value, const Variables & variables) + IdentifierType JsonRandom::decodeKey(const JsonNode & value, const Variables & variables) { if (value.String().empty() || value.String()[0] != '@') - return IdentifierType(*VLC->identifiers()->getIdentifier(IdentifierType::entityType(), value)); + return IdentifierType(VLC->identifiers()->getIdentifier(IdentifierType::entityType(), value).value_or(-1)); else return loadVariable(IdentifierType::entityType(), value.String(), variables, IdentifierType::NONE); } template<> - PlayerColor decodeKey(const JsonNode & value, const Variables & variables) + PlayerColor JsonRandom::decodeKey(const JsonNode & value, const Variables & variables) { return PlayerColor(*VLC->identifiers()->getIdentifier("playerColor", value)); } template<> - PrimarySkill decodeKey(const JsonNode & value, const Variables & variables) + PrimarySkill JsonRandom::decodeKey(const JsonNode & value, const Variables & variables) { return PrimarySkill(*VLC->identifiers()->getIdentifier("primarySkill", value)); } template<> - PrimarySkill decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) + PrimarySkill JsonRandom::decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) { if (value.empty() || value[0] != '@') return PrimarySkill(*VLC->identifiers()->getIdentifier(modScope, "primarySkill", value)); else - return PrimarySkill(loadVariable("primarySkill", value, variables, static_cast(PrimarySkill::NONE))); + return PrimarySkill(loadVariable("primarySkill", value, variables, PrimarySkill::NONE.getNum())); } /// Method that allows type-specific object filtering /// Default implementation is to accept all input objects template - std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + std::set JsonRandom::filterKeysTyped(const JsonNode & value, const std::set & valuesSet) { return valuesSet; } template<> - std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + std::set JsonRandom::filterKeysTyped(const JsonNode & value, const std::set & valuesSet) { assert(value.isStruct()); @@ -155,7 +155,7 @@ namespace JsonRandom for (auto const & artID : valuesSet) { - CArtifact * art = VLC->arth->objects[artID]; + const CArtifact * art = artID.toArtifact(); if(!vstd::iswithin(art->getPrice(), minValue, maxValue)) continue; @@ -163,7 +163,7 @@ namespace JsonRandom if(!allowedClasses.empty() && !allowedClasses.count(art->aClass)) continue; - if(!IObjectInterface::cb->isAllowed(1, art->getIndex())) + if(!cb->isAllowed(art->getId())) continue; if(!allowedPositions.empty()) @@ -185,7 +185,7 @@ namespace JsonRandom } template<> - std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + std::set JsonRandom::filterKeysTyped(const JsonNode & value, const std::set & valuesSet) { std::set result = valuesSet; @@ -212,7 +212,7 @@ namespace JsonRandom } template - std::set filterKeys(const JsonNode & value, const std::set & valuesSet, const Variables & variables) + std::set JsonRandom::filterKeys(const JsonNode & value, const std::set & valuesSet, const Variables & variables) { if(value.isString()) return { decodeKey(value, variables) }; @@ -235,9 +235,9 @@ namespace JsonRandom filteredAnyOf.insert(subset.begin(), subset.end()); } - vstd::erase_if(filteredTypes, [&](const IdentifierType & value) + vstd::erase_if(filteredTypes, [&filteredAnyOf](const IdentifierType & filteredValue) { - return filteredAnyOf.count(value) == 0; + return filteredAnyOf.count(filteredValue) == 0; }); } @@ -256,7 +256,7 @@ namespace JsonRandom return valuesSet; } - TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + TResources JsonRandom::loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { TResources ret; @@ -274,7 +274,7 @@ namespace JsonRandom return ret; } - TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + TResources JsonRandom::loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::set defaultResources{ GameResID::WOOD, @@ -295,7 +295,7 @@ namespace JsonRandom return ret; } - PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + PrimarySkill JsonRandom::loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::set defaultSkills{ PrimarySkill::ATTACK, @@ -307,7 +307,7 @@ namespace JsonRandom return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } - std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret(GameConstants::PRIMARY_SKILLS, 0); std::set defaultSkills{ @@ -321,8 +321,8 @@ namespace JsonRandom { for(const auto & pair : value.Struct()) { - PrimarySkill id = decodeKey(pair.second.meta, pair.first, variables); - ret[static_cast(id)] += loadValue(pair.second, rng, variables); + PrimarySkill id = decodeKey(pair.second.getModScope(), pair.first, variables); + ret[id.getNum()] += loadValue(pair.second, rng, variables); } } if(value.isVector()) @@ -333,31 +333,31 @@ namespace JsonRandom PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); defaultSkills.erase(skillID); - ret[static_cast(skillID)] += loadValue(element, rng, variables); + ret[skillID.getNum()] += loadValue(element, rng, variables); } } return ret; } - SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + SecondarySkill JsonRandom::loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::set defaultSkills; for(const auto & skill : VLC->skillh->objects) - if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + if (cb->isAllowed(skill->getId())) defaultSkills.insert(skill->getId()); std::set potentialPicks = filterKeys(value, defaultSkills, variables); return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } - std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::map JsonRandom::loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::map ret; if(value.isStruct()) { for(const auto & pair : value.Struct()) { - SecondarySkill id = decodeKey(pair.second.meta, pair.first, variables); + SecondarySkill id = decodeKey(pair.second.getModScope(), pair.first, variables); ret[id] = loadValue(pair.second, rng, variables); } } @@ -365,7 +365,7 @@ namespace JsonRandom { std::set defaultSkills; for(const auto & skill : VLC->skillh->objects) - if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + if (cb->isAllowed(skill->getId())) defaultSkills.insert(skill->getId()); for(const auto & element : value.Vector()) @@ -380,18 +380,19 @@ namespace JsonRandom return ret; } - ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + ArtifactID JsonRandom::loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::set allowedArts; - for (auto const * artifact : VLC->arth->allowedArtifacts) - allowedArts.insert(artifact->getId()); + for(const auto & artifact : VLC->arth->objects) + if (cb->isAllowed(artifact->getId()) && VLC->arth->legalArtifact(artifact->getId())) + allowedArts.insert(artifact->getId()); std::set potentialPicks = filterKeys(value, allowedArts, variables); - return VLC->arth->pickRandomArtifact(rng, potentialPicks); + return cb->gameState()->pickRandomArtifact(rng, potentialPicks); } - std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & entry : value.Vector()) @@ -401,11 +402,11 @@ namespace JsonRandom return ret; } - SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + SpellID JsonRandom::loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::set defaultSpells; for(const auto & spell : VLC->spellh->objects) - if (IObjectInterface::cb->isAllowed(0, spell->getIndex())) + if (cb->isAllowed(spell->getId()) && !spell->isSpecial()) defaultSpells.insert(spell->getId()); std::set potentialPicks = filterKeys(value, defaultSpells, variables); @@ -418,7 +419,7 @@ namespace JsonRandom return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } - std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & entry : value.Vector()) @@ -428,7 +429,7 @@ namespace JsonRandom return ret; } - std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; std::set defaultPlayers; @@ -444,7 +445,7 @@ namespace JsonRandom return ret; } - std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng) + std::vector JsonRandom::loadHeroes(const JsonNode & value, CRandomGenerator & rng) { std::vector ret; for(auto & entry : value.Vector()) @@ -454,7 +455,7 @@ namespace JsonRandom return ret; } - std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng) + std::vector JsonRandom::loadHeroClasses(const JsonNode & value, CRandomGenerator & rng) { std::vector ret; for(auto & entry : value.Vector()) @@ -464,7 +465,7 @@ namespace JsonRandom return ret; } - CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + CStackBasicDescriptor JsonRandom::loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { CStackBasicDescriptor stack; @@ -481,19 +482,19 @@ namespace JsonRandom else logMod->warn("Failed to select suitable random creature!"); - stack.type = VLC->creh->objects[pickedCreature]; + stack.type = pickedCreature.toCreature(); stack.count = loadValue(value, rng, variables); if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) { if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade { - stack.type = VLC->creh->objects[*RandomGeneratorUtil::nextItem(stack.type->upgrades, rng)]; + stack.type = RandomGeneratorUtil::nextItem(stack.type->upgrades, rng)->toCreature(); } } return stack; } - std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector JsonRandom::loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) @@ -503,7 +504,7 @@ namespace JsonRandom return ret; } - std::vector evaluateCreatures(const JsonNode & value, const Variables & variables) + std::vector JsonRandom::evaluateCreatures(const JsonNode & value, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) @@ -517,19 +518,20 @@ namespace JsonRandom info.minAmount = static_cast(node["min"].Float()); info.maxAmount = static_cast(node["max"].Float()); } - const CCreature * crea = VLC->creh->objects[VLC->identifiers()->getIdentifier("creature", node["type"]).value()]; + CreatureID creatureID(VLC->identifiers()->getIdentifier("creature", node["type"]).value()); + const CCreature * crea = creatureID.toCreature(); info.allowedCreatures.push_back(crea); if (node["upgradeChance"].Float() > 0) { for(const auto & creaID : crea->upgrades) - info.allowedCreatures.push_back(VLC->creh->objects[creaID]); + info.allowedCreatures.push_back(creaID.toCreature()); } ret.push_back(info); } return ret; } - std::vector DLL_LINKAGE loadBonuses(const JsonNode & value) + std::vector JsonRandom::loadBonuses(const JsonNode & value) { std::vector ret; for (const JsonNode & entry : value.Vector()) @@ -540,6 +542,4 @@ namespace JsonRandom return ret; } -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonRandom.h b/lib/json/JsonRandom.h new file mode 100644 index 000000000..8129c11bb --- /dev/null +++ b/lib/json/JsonRandom.h @@ -0,0 +1,82 @@ +/* + * JsonRandom.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "GameCallbackHolder.h" +#include "GameConstants.h" +#include "ResourceSet.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +using JsonVector = std::vector; +class CRandomGenerator; + +struct Bonus; +struct Component; +class CStackBasicDescriptor; + +class JsonRandom : public GameCallbackHolder +{ +public: + using Variables = std::map; + +private: + template + std::set filterKeys(const JsonNode & value, const std::set & valuesSet, const Variables & variables); + + template + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet); + + template + IdentifierType decodeKey(const std::string & modScope, const std::string & value, const Variables & variables); + + template + IdentifierType decodeKey(const JsonNode & value, const Variables & variables); + + si32 loadVariable(const std::string & variableGroup, const std::string & value, const Variables & variables, si32 defaultValue); + +public: + using GameCallbackHolder::GameCallbackHolder; + + struct RandomStackInfo + { + std::vector allowedCreatures; + si32 minAmount; + si32 maxAmount; + }; + + si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue = 0); + + TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + + ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + + SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + + CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + std::vector evaluateCreatures(const JsonNode & value, const Variables & variables); + + std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); + std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); + + static std::vector loadBonuses(const JsonNode & value); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonUtils.cpp b/lib/json/JsonUtils.cpp new file mode 100644 index 000000000..c66823df5 --- /dev/null +++ b/lib/json/JsonUtils.cpp @@ -0,0 +1,268 @@ +/* + * JsonUtils.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "JsonUtils.h" + +#include "JsonValidator.h" + +#include "../filesystem/Filesystem.h" + +VCMI_LIB_USING_NAMESPACE + +static const JsonNode nullNode; + +static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName) +{ + const JsonNode & fieldProps = schema["properties"][fieldName]; + +#if defined(VCMI_IOS) + if (!fieldProps["defaultIOS"].isNull()) + return fieldProps["defaultIOS"]; +#elif defined(VCMI_ANDROID) + if (!fieldProps["defaultAndroid"].isNull()) + return fieldProps["defaultAndroid"]; +#elif defined(VCMI_WINDOWS) + if (!fieldProps["defaultWindows"].isNull()) + return fieldProps["defaultWindows"]; +#endif + +#if !defined(VCMI_MOBILE) + if (!fieldProps["defaultDesktop"].isNull()) + return fieldProps["defaultDesktop"]; +#endif + return fieldProps["default"]; +} + +static void eraseOptionalNodes(JsonNode & node, const JsonNode & schema) +{ + assert(schema["type"].String() == "object"); + + std::set foundEntries; + + for(const auto & entry : schema["required"].Vector()) + foundEntries.insert(entry.String()); + + vstd::erase_if(node.Struct(), [&foundEntries](const auto & structEntry){ + return !vstd::contains(foundEntries, structEntry.first); + }); +} + +static void minimizeNode(JsonNode & node, const JsonNode & schema) +{ + if (schema["type"].String() != "object") + return; + + for(const auto & entry : schema["required"].Vector()) + { + const std::string & name = entry.String(); + minimizeNode(node[name], schema["properties"][name]); + + if (vstd::contains(node.Struct(), name) && node[name] == getDefaultValue(schema, name)) + node.Struct().erase(name); + } + eraseOptionalNodes(node, schema); +} + +static void maximizeNode(JsonNode & node, const JsonNode & schema) +{ + // "required" entry can only be found in object/struct + if (schema["type"].String() != "object") + return; + + // check all required entries that have default version + for(const auto & entry : schema["required"].Vector()) + { + const std::string & name = entry.String(); + + if (node[name].isNull() && !getDefaultValue(schema, name).isNull()) + node[name] = getDefaultValue(schema, name); + + maximizeNode(node[name], schema["properties"][name]); + } + + eraseOptionalNodes(node, schema); +} + +VCMI_LIB_NAMESPACE_BEGIN + +void JsonUtils::minimize(JsonNode & node, const std::string & schemaName) +{ + minimizeNode(node, getSchema(schemaName)); +} + +void JsonUtils::maximize(JsonNode & node, const std::string & schemaName) +{ + maximizeNode(node, getSchema(schemaName)); +} + +bool JsonUtils::validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName) +{ + JsonValidator validator; + std::string log = validator.check(schemaName, node); + if (!log.empty()) + { + logMod->warn("Data in %s is invalid!", dataName); + logMod->warn(log); + logMod->trace("%s json: %s", dataName, node.toCompactString()); + } + return log.empty(); +} + +const JsonNode & getSchemaByName(const std::string & name) +{ + // cached schemas to avoid loading json data multiple times + static std::map loadedSchemas; + + if (vstd::contains(loadedSchemas, name)) + return loadedSchemas[name]; + + auto filename = JsonPath::builtin("config/schemas/" + name); + + if (CResourceHandler::get()->existsResource(filename)) + { + loadedSchemas[name] = JsonNode(filename); + return loadedSchemas[name]; + } + + logMod->error("Error: missing schema with name %s!", name); + assert(0); + return nullNode; +} + +const JsonNode & JsonUtils::getSchema(const std::string & URI) +{ + size_t posColon = URI.find(':'); + size_t posHash = URI.find('#'); + std::string filename; + if(posColon == std::string::npos) + { + filename = URI.substr(0, posHash); + } + else + { + std::string protocolName = URI.substr(0, posColon); + filename = URI.substr(posColon + 1, posHash - posColon - 1) + ".json"; + if(protocolName != "vcmi") + { + logMod->error("Error: unsupported URI protocol for schema: %s", URI); + return nullNode; + } + } + + // check if json pointer if present (section after hash in string) + if(posHash == std::string::npos || posHash == URI.size() - 1) + { + auto const & result = getSchemaByName(filename); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } + else + { + auto const & result = getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } +} + +void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, bool copyMeta) +{ + if (dest.getType() == JsonNode::JsonType::DATA_NULL) + { + std::swap(dest, source); + return; + } + + switch (source.getType()) + { + case JsonNode::JsonType::DATA_NULL: + { + dest.clear(); + break; + } + case JsonNode::JsonType::DATA_BOOL: + case JsonNode::JsonType::DATA_FLOAT: + case JsonNode::JsonType::DATA_INTEGER: + case JsonNode::JsonType::DATA_STRING: + case JsonNode::JsonType::DATA_VECTOR: + { + std::swap(dest, source); + break; + } + case JsonNode::JsonType::DATA_STRUCT: + { + if(!ignoreOverride && source.getOverrideFlag()) + { + std::swap(dest, source); + } + else + { + if (copyMeta) + dest.setModScope(source.getModScope(), false); + + //recursively merge all entries from struct + for(auto & node : source.Struct()) + merge(dest[node.first], node.second, ignoreOverride); + } + } + } +} + +void JsonUtils::mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride, bool copyMeta) +{ + // uses copy created in stack to safely merge two nodes + merge(dest, source, ignoreOverride, copyMeta); +} + +void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) +{ + JsonNode inheritedNode(base); + merge(inheritedNode, descendant, true, true); + std::swap(descendant, inheritedNode); +} + +JsonNode JsonUtils::assembleFromFiles(const std::vector & files) +{ + bool isValid = false; + return assembleFromFiles(files, isValid); +} + +JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bool & isValid) +{ + isValid = true; + JsonNode result; + + for(const auto & file : files) + { + bool isValidFile = false; + JsonNode section(JsonPath::builtinTODO(file), isValidFile); + merge(result, section); + isValid |= isValidFile; + } + return result; +} + +JsonNode JsonUtils::assembleFromFiles(const std::string & filename) +{ + JsonNode result; + JsonPath resID = JsonPath::builtinTODO(filename); + + for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) + { + auto textData = loader->load(resID)->readAll(); + JsonNode section(reinterpret_cast(textData.first.get()), textData.second); + merge(result, section); + } + return result; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonUtils.h b/lib/json/JsonUtils.h new file mode 100644 index 000000000..7f550b916 --- /dev/null +++ b/lib/json/JsonUtils.h @@ -0,0 +1,77 @@ +/* + * JsonUtils.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace JsonUtils +{ + /** + * @brief recursively merges source into dest, replacing identical fields + * struct : recursively calls this function + * arrays : each entry will be merged recursively + * values : value in source will replace value in dest + * null : if value in source is present but set to null it will delete entry in dest + * @note this function will destroy data in source + */ + DLL_LINKAGE void merge(JsonNode & dest, JsonNode & source, bool ignoreOverride = false, bool copyMeta = false); + + /** + * @brief recursively merges source into dest, replacing identical fields + * struct : recursively calls this function + * arrays : each entry will be merged recursively + * values : value in source will replace value in dest + * null : if value in source is present but set to null it will delete entry in dest + * @note this function will preserve data stored in source by creating copy + */ + DLL_LINKAGE void mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride = false, bool copyMeta = false); + + /** @brief recursively merges descendant into copy of base node + * Result emulates inheritance semantic + */ + DLL_LINKAGE void inherit(JsonNode & descendant, const JsonNode & base); + + /** + * @brief generate one Json structure from multiple files + * @param files - list of filenames with parts of json structure + */ + DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files); + DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, bool & isValid); + + /// This version loads all files with same name (overridden by mods) + DLL_LINKAGE JsonNode assembleFromFiles(const std::string & filename); + + /** + * @brief removes all nodes that are identical to default entry in schema + * @param node - JsonNode to minimize + * @param schemaName - name of schema to use + * @note for minimizing data must be valid against given schema + */ + DLL_LINKAGE void minimize(JsonNode & node, const std::string & schemaName); + /// opposed to minimize, adds all missing, required entries that have default value + DLL_LINKAGE void maximize(JsonNode & node, const std::string & schemaName); + + /** + * @brief validate node against specified schema + * @param node - JsonNode to check + * @param schemaName - name of schema to use + * @param dataName - some way to identify data (printed in console in case of errors) + * @returns true if data in node fully compilant with schema + */ + DLL_LINKAGE bool validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName); + + /// get schema by json URI: vcmi:# + /// example: schema "vcmi:settings" is used to check user settings + DLL_LINKAGE const JsonNode & getSchema(const std::string & URI); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonValidator.cpp b/lib/json/JsonValidator.cpp new file mode 100644 index 000000000..14dca501d --- /dev/null +++ b/lib/json/JsonValidator.cpp @@ -0,0 +1,658 @@ +/* + * JsonValidator.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "JsonValidator.h" + +#include "JsonUtils.h" + +#include "../VCMI_Lib.h" +#include "../filesystem/Filesystem.h" +#include "../modding/ModScope.h" +#include "../modding/CModHandler.h" +#include "../ScopeGuard.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static std::string emptyCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + // check is not needed - e.g. incorporated into another check + return ""; +} + +static std::string notImplementedCheck(JsonValidator & validator, + const JsonNode & baseSchema, + const JsonNode & schema, + const JsonNode & data) +{ + return "Not implemented entry in schema"; +} + +static std::string schemaListCheck(JsonValidator & validator, + const JsonNode & baseSchema, + const JsonNode & schema, + const JsonNode & data, + const std::string & errorMsg, + const std::function & isValid) +{ + std::string errors = "\n"; + size_t result = 0; + + for(const auto & schemaEntry : schema.Vector()) + { + std::string error = validator.check(schemaEntry, data); + if (error.empty()) + { + result++; + } + else + { + errors += error; + errors += "\n"; + } + } + if (isValid(result)) + return ""; + else + return validator.makeErrorMessage(errorMsg) + errors; +} + +static std::string allOfCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass all schemas", [&schema](size_t count) + { + return count == schema.Vector().size(); + }); +} + +static std::string anyOfCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass any schema", [](size_t count) + { + return count > 0; + }); +} + +static std::string oneOfCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass exactly one schema", [](size_t count) + { + return count == 1; + }); +} + +static std::string notCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (validator.check(schema, data).empty()) + return validator.makeErrorMessage("Successful validation against negative check"); + return ""; +} + +static std::string enumCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + for(const auto & enumEntry : schema.Vector()) + { + if (data == enumEntry) + return ""; + } + return validator.makeErrorMessage("Key must have one of predefined values"); +} + +static std::string typeCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + static const std::unordered_map stringToType = + { + {"null", JsonNode::JsonType::DATA_NULL}, + {"boolean", JsonNode::JsonType::DATA_BOOL}, + {"number", JsonNode::JsonType::DATA_FLOAT}, + {"integer", JsonNode::JsonType::DATA_INTEGER}, + {"string", JsonNode::JsonType::DATA_STRING}, + {"array", JsonNode::JsonType::DATA_VECTOR}, + {"object", JsonNode::JsonType::DATA_STRUCT} + }; + + const auto & typeName = schema.String(); + auto it = stringToType.find(typeName); + if(it == stringToType.end()) + { + return validator.makeErrorMessage("Unknown type in schema:" + typeName); + } + + JsonNode::JsonType type = it->second; + + // for "number" type both float and integer are allowed + if(type == JsonNode::JsonType::DATA_FLOAT && data.isNumber()) + return ""; + + if(type != data.getType() && data.getType() != JsonNode::JsonType::DATA_NULL) + return validator.makeErrorMessage("Type mismatch! Expected " + schema.String()); + return ""; +} + +static std::string refCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + std::string URI = schema.String(); + //node must be validated using schema pointed by this reference and not by data here + //Local reference. Turn it into more easy to handle remote ref + if (boost::algorithm::starts_with(URI, "#")) + { + const std::string name = validator.usedSchemas.back(); + const std::string nameClean = name.substr(0, name.find('#')); + URI = nameClean + URI; + } + return validator.check(URI, data); +} + +static std::string formatCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + auto formats = validator.getKnownFormats(); + std::string errors; + auto checker = formats.find(schema.String()); + if (checker != formats.end()) + { + if (data.isString()) + { + std::string result = checker->second(data); + if (!result.empty()) + errors += validator.makeErrorMessage(result); + } + else + { + errors += validator.makeErrorMessage("Format value must be string: " + schema.String()); + } + } + else + errors += validator.makeErrorMessage("Unsupported format type: " + schema.String()); + return errors; +} + +static std::string maxLengthCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.String().size() > schema.Float()) + return validator.makeErrorMessage((boost::format("String is longer than %d symbols") % schema.Float()).str()); + return ""; +} + +static std::string minLengthCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.String().size() < schema.Float()) + return validator.makeErrorMessage((boost::format("String is shorter than %d symbols") % schema.Float()).str()); + return ""; +} + +static std::string maximumCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.Float() > schema.Float()) + return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); + return ""; +} + +static std::string minimumCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.Float() < schema.Float()) + return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); + return ""; +} + +static std::string exclusiveMaximumCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.Float() >= schema.Float()) + return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); + return ""; +} + +static std::string exclusiveMinimumCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.Float() <= schema.Float()) + return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); + return ""; +} + +static std::string multipleOfCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + double result = data.Integer() / schema.Integer(); + if (!vstd::isAlmostEqual(floor(result), result)) + return validator.makeErrorMessage((boost::format("Value is not divisible by %d") % schema.Float()).str()); + return ""; +} + +static std::string itemEntryCheck(JsonValidator & validator, const JsonVector & items, const JsonNode & schema, size_t index) +{ + validator.currentPath.emplace_back(); + validator.currentPath.back().Float() = static_cast(index); + auto onExit = vstd::makeScopeGuard([&validator]() + { + validator.currentPath.pop_back(); + }); + + if (!schema.isNull()) + return validator.check(schema, items[index]); + return ""; +} + +static std::string itemsCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + std::string errors; + for (size_t i=0; i i) + errors += itemEntryCheck(validator, data.Vector(), schema.Vector()[i], i); + } + else + { + errors += itemEntryCheck(validator, data.Vector(), schema, i); + } + } + return errors; +} + +static std::string additionalItemsCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + std::string errors; + // "items" is struct or empty (defaults to empty struct) - validation always successful + const JsonNode & items = baseSchema["items"]; + if (items.getType() != JsonNode::JsonType::DATA_VECTOR) + return ""; + + for (size_t i=items.Vector().size(); i schema.Float()) + return validator.makeErrorMessage((boost::format("Length is bigger than %d") % schema.Float()).str()); + return ""; +} + +static std::string uniqueItemsCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (schema.Bool()) + { + for (auto itA = schema.Vector().begin(); itA != schema.Vector().end(); itA++) + { + auto itB = itA; + while (++itB != schema.Vector().end()) + { + if (*itA == *itB) + return validator.makeErrorMessage("List must consist from unique items"); + } + } + } + return ""; +} + +static std::string maxPropertiesCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.Struct().size() > schema.Float()) + return validator.makeErrorMessage((boost::format("Number of entries is bigger than %d") % schema.Float()).str()); + return ""; +} + +static std::string minPropertiesCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + if (data.Struct().size() < schema.Float()) + return validator.makeErrorMessage((boost::format("Number of entries is less than %d") % schema.Float()).str()); + return ""; +} + +static std::string uniquePropertiesCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + for (auto itA = data.Struct().begin(); itA != data.Struct().end(); itA++) + { + auto itB = itA; + while (++itB != data.Struct().end()) + { + if (itA->second == itB->second) + return validator.makeErrorMessage("List must consist from unique items"); + } + } + return ""; +} + +static std::string requiredCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + std::string errors; + for(const auto & required : schema.Vector()) + { + if (data[required.String()].isNull()) + errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); + } + return errors; +} + +static std::string dependenciesCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + std::string errors; + for(const auto & deps : schema.Struct()) + { + if (!data[deps.first].isNull()) + { + if (deps.second.getType() == JsonNode::JsonType::DATA_VECTOR) + { + JsonVector depList = deps.second.Vector(); + for(auto & depEntry : depList) + { + if (data[depEntry.String()].isNull()) + errors += validator.makeErrorMessage("Property " + depEntry.String() + " required for " + deps.first + " is missing"); + } + } + else + { + if (!validator.check(deps.second, data).empty()) + errors += validator.makeErrorMessage("Requirements for " + deps.first + " are not fulfilled"); + } + } + } + return errors; +} + +static std::string propertyEntryCheck(JsonValidator & validator, const JsonNode &node, const JsonNode & schema, const std::string & nodeName) +{ + validator.currentPath.emplace_back(); + validator.currentPath.back().String() = nodeName; + auto onExit = vstd::makeScopeGuard([&validator]() + { + validator.currentPath.pop_back(); + }); + + // there is schema specifically for this item + if (!schema.isNull()) + return validator.check(schema, node); + return ""; +} + +static std::string propertiesCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + std::string errors; + + for(const auto & entry : data.Struct()) + errors += propertyEntryCheck(validator, entry.second, schema[entry.first], entry.first); + return errors; +} + +static std::string additionalPropertiesCheck(JsonValidator & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) +{ + std::string errors; + for(const auto & entry : data.Struct()) + { + if (baseSchema["properties"].Struct().count(entry.first) == 0) + { + // try generic additionalItems schema + if (schema.getType() == JsonNode::JsonType::DATA_STRUCT) + errors += propertyEntryCheck(validator, entry.second, schema, entry.first); + + // or, additionalItems field can be bool which indicates if such items are allowed + else if(!schema.isNull() && !schema.Bool()) // present and set to false - error + errors += validator.makeErrorMessage("Unknown entry found: " + entry.first); + } + } + return errors; +} + +static bool testFilePresence(const std::string & scope, const ResourcePath & resource) +{ + std::set allowedScopes; + if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies + { + //NOTE: recursive dependencies are not allowed at the moment - update code if this changes + bool found = true; + allowedScopes = VLC->modh->getModDependencies(scope, found); + + if(!found) + return false; + + allowedScopes.insert(ModScope::scopeBuiltin()); // all mods can use H3 files + } + allowedScopes.insert(scope); // mods can use their own files + + for(const auto & entry : allowedScopes) + { + if (CResourceHandler::get(entry)->existsResource(resource)) + return true; + } + return false; +} + +#define TEST_FILE(scope, prefix, file, type) \ + if (testFilePresence(scope, ResourcePath(prefix + file, type))) \ + return "" + +static std::string testAnimation(const std::string & path, const std::string & scope) +{ + TEST_FILE(scope, "Sprites/", path, EResType::ANIMATION); + TEST_FILE(scope, "Sprites/", path, EResType::JSON); + return "Animation file \"" + path + "\" was not found"; +} + +static std::string textFile(const JsonNode & node) +{ + TEST_FILE(node.getModScope(), "", node.String(), EResType::JSON); + return "Text file \"" + node.String() + "\" was not found"; +} + +static std::string musicFile(const JsonNode & node) +{ + TEST_FILE(node.getModScope(), "Music/", node.String(), EResType::SOUND); + TEST_FILE(node.getModScope(), "", node.String(), EResType::SOUND); + return "Music file \"" + node.String() + "\" was not found"; +} + +static std::string soundFile(const JsonNode & node) +{ + TEST_FILE(node.getModScope(), "Sounds/", node.String(), EResType::SOUND); + return "Sound file \"" + node.String() + "\" was not found"; +} + +static std::string animationFile(const JsonNode & node) +{ + return testAnimation(node.String(), node.getModScope()); +} + +static std::string imageFile(const JsonNode & node) +{ + TEST_FILE(node.getModScope(), "Data/", node.String(), EResType::IMAGE); + TEST_FILE(node.getModScope(), "Sprites/", node.String(), EResType::IMAGE); + if (node.String().find(':') != std::string::npos) + return testAnimation(node.String().substr(0, node.String().find(':')), node.getModScope()); + return "Image file \"" + node.String() + "\" was not found"; +} + +static std::string videoFile(const JsonNode & node) +{ + TEST_FILE(node.getModScope(), "Video/", node.String(), EResType::VIDEO); + return "Video file \"" + node.String() + "\" was not found"; +} +#undef TEST_FILE + +JsonValidator::TValidatorMap createCommonFields() +{ + JsonValidator::TValidatorMap ret; + + ret["format"] = formatCheck; + ret["allOf"] = allOfCheck; + ret["anyOf"] = anyOfCheck; + ret["oneOf"] = oneOfCheck; + ret["enum"] = enumCheck; + ret["type"] = typeCheck; + ret["not"] = notCheck; + ret["$ref"] = refCheck; + + // fields that don't need implementation + ret["title"] = emptyCheck; + ret["$schema"] = emptyCheck; + ret["default"] = emptyCheck; + ret["defaultIOS"] = emptyCheck; + ret["defaultAndroid"] = emptyCheck; + ret["defaultWindows"] = emptyCheck; + ret["description"] = emptyCheck; + ret["definitions"] = emptyCheck; + + // Not implemented + ret["propertyNames"] = notImplementedCheck; + ret["contains"] = notImplementedCheck; + ret["const"] = notImplementedCheck; + ret["examples"] = notImplementedCheck; + + return ret; +} + +JsonValidator::TValidatorMap createStringFields() +{ + JsonValidator::TValidatorMap ret = createCommonFields(); + ret["maxLength"] = maxLengthCheck; + ret["minLength"] = minLengthCheck; + + ret["pattern"] = notImplementedCheck; + return ret; +} + +JsonValidator::TValidatorMap createNumberFields() +{ + JsonValidator::TValidatorMap ret = createCommonFields(); + ret["maximum"] = maximumCheck; + ret["minimum"] = minimumCheck; + ret["multipleOf"] = multipleOfCheck; + + ret["exclusiveMaximum"] = exclusiveMaximumCheck; + ret["exclusiveMinimum"] = exclusiveMinimumCheck; + return ret; +} + +JsonValidator::TValidatorMap createVectorFields() +{ + JsonValidator::TValidatorMap ret = createCommonFields(); + ret["items"] = itemsCheck; + ret["minItems"] = minItemsCheck; + ret["maxItems"] = maxItemsCheck; + ret["uniqueItems"] = uniqueItemsCheck; + ret["additionalItems"] = additionalItemsCheck; + return ret; +} + +JsonValidator::TValidatorMap createStructFields() +{ + JsonValidator::TValidatorMap ret = createCommonFields(); + ret["additionalProperties"] = additionalPropertiesCheck; + ret["uniqueProperties"] = uniquePropertiesCheck; + ret["maxProperties"] = maxPropertiesCheck; + ret["minProperties"] = minPropertiesCheck; + ret["dependencies"] = dependenciesCheck; + ret["properties"] = propertiesCheck; + ret["required"] = requiredCheck; + + ret["patternProperties"] = notImplementedCheck; + return ret; +} + +JsonValidator::TFormatMap createFormatMap() +{ + JsonValidator::TFormatMap ret; + ret["textFile"] = textFile; + ret["musicFile"] = musicFile; + ret["soundFile"] = soundFile; + ret["animationFile"] = animationFile; + ret["imageFile"] = imageFile; + ret["videoFile"] = videoFile; + + //TODO: + // uri-reference + // uri-template + // json-pointer + + return ret; +} + +std::string JsonValidator::makeErrorMessage(const std::string &message) +{ + std::string errors; + errors += "At "; + if (!currentPath.empty()) + { + for(const JsonNode &path : currentPath) + { + errors += "/"; + if (path.getType() == JsonNode::JsonType::DATA_STRING) + errors += path.String(); + else + errors += std::to_string(static_cast(path.Float())); + } + } + else + errors += ""; + errors += "\n\t Error: " + message + "\n"; + return errors; +} + +std::string JsonValidator::check(const std::string & schemaName, const JsonNode & data) +{ + usedSchemas.push_back(schemaName); + auto onscopeExit = vstd::makeScopeGuard([this]() + { + usedSchemas.pop_back(); + }); + return check(JsonUtils::getSchema(schemaName), data); +} + +std::string JsonValidator::check(const JsonNode & schema, const JsonNode & data) +{ + const TValidatorMap & knownFields = getKnownFieldsFor(data.getType()); + std::string errors; + for(const auto & entry : schema.Struct()) + { + auto checker = knownFields.find(entry.first); + if (checker != knownFields.end()) + errors += checker->second(*this, schema, entry.second, data); + } + return errors; +} + +const JsonValidator::TValidatorMap & JsonValidator::getKnownFieldsFor(JsonNode::JsonType type) +{ + static const TValidatorMap commonFields = createCommonFields(); + static const TValidatorMap numberFields = createNumberFields(); + static const TValidatorMap stringFields = createStringFields(); + static const TValidatorMap vectorFields = createVectorFields(); + static const TValidatorMap structFields = createStructFields(); + + switch (type) + { + case JsonNode::JsonType::DATA_FLOAT: + case JsonNode::JsonType::DATA_INTEGER: + return numberFields; + case JsonNode::JsonType::DATA_STRING: return stringFields; + case JsonNode::JsonType::DATA_VECTOR: return vectorFields; + case JsonNode::JsonType::DATA_STRUCT: return structFields; + default: return commonFields; + } +} + +const JsonValidator::TFormatMap & JsonValidator::getKnownFormats() +{ + static const TFormatMap knownFormats = createFormatMap(); + return knownFormats; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonValidator.h b/lib/json/JsonValidator.h new file mode 100644 index 000000000..562559dba --- /dev/null +++ b/lib/json/JsonValidator.h @@ -0,0 +1,43 @@ +/* + * JsonValidator.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// Class for Json validation. Mostly compilant with json-schema v6 draf +struct JsonValidator +{ + /// path from root node to current one. + /// JsonNode is used as variant - either string (name of node) or as float (index in list) + std::vector currentPath; + + /// Stack of used schemas. Last schema is the one used currently. + /// May contain multiple items in case if remote references were found + std::vector usedSchemas; + + /// generates error message + std::string makeErrorMessage(const std::string &message); + + using TFormatValidator = std::function; + using TFormatMap = std::unordered_map; + using TFieldValidator = std::function; + using TValidatorMap = std::unordered_map; + + /// map of known fields in schema + const TValidatorMap & getKnownFieldsFor(JsonNode::JsonType type); + const TFormatMap & getKnownFormats(); + + std::string check(const std::string & schemaName, const JsonNode & data); + std::string check(const JsonNode & schema, const JsonNode & data); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonWriter.cpp b/lib/json/JsonWriter.cpp new file mode 100644 index 000000000..6bc65bae5 --- /dev/null +++ b/lib/json/JsonWriter.cpp @@ -0,0 +1,144 @@ +/* + * JsonWriter.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "StdInc.h" +#include "JsonWriter.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template +void JsonWriter::writeContainer(Iterator begin, Iterator end) +{ + if(begin == end) + return; + + prefix += '\t'; + + writeEntry(begin++); + + while(begin != end) + { + out << (compactMode ? ", " : ",\n"); + writeEntry(begin++); + } + + out << (compactMode ? "" : "\n"); + prefix.resize(prefix.size() - 1); +} + +void JsonWriter::writeEntry(JsonMap::const_iterator entry) +{ + if(!compactMode) + { + if(!entry->second.getModScope().empty()) + out << prefix << " // " << entry->second.getModScope() << "\n"; + out << prefix; + } + writeString(entry->first); + out << " : "; + writeNode(entry->second); +} + +void JsonWriter::writeEntry(JsonVector::const_iterator entry) +{ + if(!compactMode) + { + if(!entry->getModScope().empty()) + out << prefix << " // " << entry->getModScope() << "\n"; + out << prefix; + } + writeNode(*entry); +} + +void JsonWriter::writeString(const std::string & string) +{ + static const std::string escaped = "\"\\\b\f\n\r\t"; + static const std::array escapedCode = {'\"', '\\', 'b', 'f', 'n', 'r', 't'}; + + out << '\"'; + size_t pos = 0; + size_t start = 0; + for(; pos < string.size(); pos++) + { + //we need to check if special character was been already escaped + if((string[pos] == '\\') && (pos + 1 < string.size()) && (std::find(escapedCode.begin(), escapedCode.end(), string[pos + 1]) != escapedCode.end())) + { + pos++; //write unchanged, next simbol also checked + } + else + { + size_t escapedPos = escaped.find(string[pos]); + + if(escapedPos != std::string::npos) + { + out.write(string.data() + start, pos - start); + out << '\\' << escapedCode[escapedPos]; + start = pos + 1; + } + } + } + out.write(string.data() + start, pos - start); + out << '\"'; +} + +void JsonWriter::writeNode(const JsonNode & node) +{ + bool originalMode = compactMode; + if(compact && !compactMode && node.isCompact()) + compactMode = true; + + switch(node.getType()) + { + case JsonNode::JsonType::DATA_NULL: + out << "null"; + break; + + case JsonNode::JsonType::DATA_BOOL: + if(node.Bool()) + out << "true"; + else + out << "false"; + break; + + case JsonNode::JsonType::DATA_FLOAT: + out << node.Float(); + break; + + case JsonNode::JsonType::DATA_STRING: + writeString(node.String()); + break; + + case JsonNode::JsonType::DATA_VECTOR: + out << "[" << (compactMode ? " " : "\n"); + writeContainer(node.Vector().begin(), node.Vector().end()); + out << (compactMode ? " " : prefix) << "]"; + break; + + case JsonNode::JsonType::DATA_STRUCT: + out << "{" << (compactMode ? " " : "\n"); + writeContainer(node.Struct().begin(), node.Struct().end()); + out << (compactMode ? " " : prefix) << "}"; + break; + + case JsonNode::JsonType::DATA_INTEGER: + out << node.Integer(); + break; + } + + compactMode = originalMode; +} + +JsonWriter::JsonWriter(std::ostream & output, bool compact) + : out(output) + , compact(compact) +{ +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonWriter.h b/lib/json/JsonWriter.h new file mode 100644 index 000000000..c9cbc6718 --- /dev/null +++ b/lib/json/JsonWriter.h @@ -0,0 +1,36 @@ +/* + * JsonWriter.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonWriter +{ + //prefix for each line (tabulation) + std::string prefix; + std::ostream & out; + //sets whether compact nodes are written in single-line format + bool compact; + //tracks whether we are currently using single-line format + bool compactMode = false; + +public: + template + void writeContainer(Iterator begin, Iterator end); + void writeEntry(JsonMap::const_iterator entry); + void writeEntry(JsonVector::const_iterator entry); + void writeString(const std::string & string); + void writeNode(const JsonNode & node); + JsonWriter(std::ostream & output, bool compact); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/logging/CLogger.cpp b/lib/logging/CLogger.cpp index 2f9ccdf6f..aff64cea3 100644 --- a/lib/logging/CLogger.cpp +++ b/lib/logging/CLogger.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" #include "CLogger.h" +#include "../CThreadHelper.h" #ifdef VCMI_ANDROID #include @@ -427,8 +428,7 @@ void CLogConsoleTarget::setColorMapping(const CColorMapping & colorMapping) { th CLogFileTarget::CLogFileTarget(const boost::filesystem::path & filePath, bool append): file(filePath.c_str(), append ? std::ios_base::app : std::ios_base::out) { -// formatter.setPattern("%d %l %n [%t] - %m"); - formatter.setPattern("%l %n [%t] - %m"); + formatter.setPattern("[%c] %l %n [%t] - %m"); } void CLogFileTarget::write(const LogRecord & record) @@ -446,4 +446,14 @@ CLogFileTarget::~CLogFileTarget() file.close(); } +LogRecord::LogRecord(const CLoggerDomain & domain, ELogLevel::ELogLevel level, const std::string & message) + : domain(domain), + level(level), + message(message), + timeStamp(boost::posix_time::microsec_clock::local_time()), + threadId(getThreadName()) +{ + +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/logging/CLogger.h b/lib/logging/CLogger.h index 72a0b929e..22c3035f1 100644 --- a/lib/logging/CLogger.h +++ b/lib/logging/CLogger.h @@ -107,12 +107,7 @@ private: /// The struct LogRecord holds the log message and additional logging information. struct DLL_LINKAGE LogRecord { - LogRecord(const CLoggerDomain & domain, ELogLevel::ELogLevel level, const std::string & message) - : domain(domain), - level(level), - message(message), - timeStamp(boost::posix_time::microsec_clock::local_time()), - threadId(boost::lexical_cast(boost::this_thread::get_id())) { } + LogRecord(const CLoggerDomain & domain, ELogLevel::ELogLevel level, const std::string & message); CLoggerDomain domain; ELogLevel::ELogLevel level; diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 9766f4123..c3573b761 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -13,13 +13,17 @@ #include "IObjectInfo.h" #include "../CGeneralTextHandler.h" -#include "../modding/IdentifierStorage.h" #include "../VCMI_Lib.h" +#include "../json/JsonUtils.h" +#include "../modding/IdentifierStorage.h" #include "../mapObjects/CGObjectInstance.h" #include "../mapObjects/ObjectTemplate.h" VCMI_LIB_NAMESPACE_BEGIN +AObjectTypeHandler::AObjectTypeHandler() = default; +AObjectTypeHandler::~AObjectTypeHandler() = default; + std::string AObjectTypeHandler::getJsonKey() const { return modScope + ':' + subTypeName; @@ -55,7 +59,8 @@ static ui32 loadJsonOrMax(const JsonNode & input) void AObjectTypeHandler::init(const JsonNode & input) { - base = input["base"]; + if (!input["base"].isNull()) + base = std::make_unique(input["base"]); if (!input["rmg"].isNull()) { @@ -72,14 +77,15 @@ void AObjectTypeHandler::init(const JsonNode & input) for (auto entry : input["templates"].Struct()) { entry.second.setType(JsonNode::JsonType::DATA_STRUCT); - JsonUtils::inherit(entry.second, base); + if (base) + JsonUtils::inherit(entry.second, *base); - auto * tmpl = new ObjectTemplate; + auto tmpl = std::make_shared(); tmpl->id = Obj(type); tmpl->subid = subtype; tmpl->stringID = entry.first; // FIXME: create "fullID" - type.object.template? tmpl->readJson(entry.second); - templates.push_back(std::shared_ptr(tmpl)); + templates.push_back(tmpl); } for(const JsonNode & node : input["sounds"]["ambient"].Vector()) @@ -96,6 +102,10 @@ void AObjectTypeHandler::init(const JsonNode & input) else aiValue = static_cast>(input["aiValue"].Integer()); + // TODO: Define properties, move them to actual object instance + blockVisit = input["blockVisit"].Bool(); + removable = input["removable"].Bool(); + battlefield = BattleField::NONE; if(!input["battleground"].isNull()) @@ -120,6 +130,8 @@ void AObjectTypeHandler::preInitObject(CGObjectInstance * obj) const obj->subID = subtype; obj->typeName = typeName; obj->subTypeName = subTypeName; + obj->blockVisit = blockVisit; + obj->removable = removable; } void AObjectTypeHandler::initTypeData(const JsonNode & input) @@ -165,13 +177,15 @@ void AObjectTypeHandler::addTemplate(const std::shared_ptr void AObjectTypeHandler::addTemplate(JsonNode config) { config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not null - JsonUtils::inherit(config, base); - auto * tmpl = new ObjectTemplate; + if (base) + JsonUtils::inherit(config, *base); + + auto tmpl = std::make_shared(); tmpl->id = Obj(type); tmpl->subid = subtype; tmpl->stringID.clear(); // TODO? tmpl->readJson(config); - templates.emplace_back(tmpl); + templates.push_back(tmpl); } std::vector> AObjectTypeHandler::getTemplates() const @@ -201,6 +215,26 @@ std::vector>AObjectTypeHandler::getTemplat return filtered; } +std::vector>AObjectTypeHandler::getMostSpecificTemplates(TerrainId terrainType) const +{ + auto templates = getTemplates(terrainType); + if (!templates.empty()) + { + //Get terrain-specific template if possible + int leastTerrains = (*boost::min_element(templates, [](const std::shared_ptr & tmp1, const std::shared_ptr & tmp2) + { + return tmp1->getAllowedTerrains().size() < tmp2->getAllowedTerrains().size(); + }))->getAllowedTerrains().size(); + + vstd::erase_if(templates, [leastTerrains](const std::shared_ptr & tmp) + { + return tmp->getAllowedTerrains().size() > leastTerrains; + }); + } + + return templates; +} + std::shared_ptr AObjectTypeHandler::getOverride(TerrainId terrainType, const CGObjectInstance * object) const { std::vector> ret = getTemplates(terrainType); diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.h b/lib/mapObjectConstructors/AObjectTypeHandler.h index ce35a9f82..ba298d13f 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.h +++ b/lib/mapObjectConstructors/AObjectTypeHandler.h @@ -9,9 +9,9 @@ */ #pragma once +#include "../constants/EntityIdentifiers.h" #include "RandomMapInfo.h" #include "SObjectSounds.h" -#include "../JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -19,6 +19,7 @@ class ObjectTemplate; class CGObjectInstance; class CRandomGenerator; class IObjectInfo; +class IGameCallback; /// Class responsible for creation of objects of specific type & subtype class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable @@ -27,7 +28,7 @@ class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable RandomMapInfo rmgInfo; - JsonNode base; /// describes base template + std::unique_ptr base; /// describes base template std::vector> templates; @@ -43,6 +44,9 @@ class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable si32 type; si32 subtype; + bool blockVisit; + bool removable; + protected: void preInitObject(CGObjectInstance * obj) const; virtual bool objectFilter(const CGObjectInstance * obj, std::shared_ptr tmpl) const; @@ -51,7 +55,8 @@ protected: virtual void initTypeData(const JsonNode & input); public: - virtual ~AObjectTypeHandler() = default; + AObjectTypeHandler(); + virtual ~AObjectTypeHandler(); si32 getIndex() const; si32 getSubIndex() const; @@ -75,6 +80,7 @@ public: /// returns all templates matching parameters std::vector> getTemplates() const; std::vector> getTemplates(const TerrainId terrainType) const; + std::vector> getMostSpecificTemplates(TerrainId terrainType) const; /// returns preferred template for this object, if present (e.g. one of 3 possible templates for town - village, fort and castle) /// note that appearance will not be changed - this must be done separately (either by assignment or via pack from server) @@ -104,7 +110,7 @@ public: /// Creates object and set up core properties (like ID/subID). Object is NOT initialized /// to allow creating objects before game start (e.g. map loading) - virtual CGObjectInstance * create(std::shared_ptr tmpl = nullptr) const = 0; + virtual CGObjectInstance * create(IGameCallback * cb, std::shared_ptr tmpl) const = 0; /// Configures object properties. Should be re-entrable, resetting state of the object if necessarily /// This should set remaining properties, including randomized or depending on map @@ -112,20 +118,6 @@ public: /// Returns object configuration, if available. Otherwise returns NULL virtual std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const; - - template void serialize(Handler &h, const int version) - { - h & type; - h & subtype; - h & templates; - h & rmgInfo; - h & modScope; - h & typeName; - h & subTypeName; - h & sounds; - h & aiValue; - h & battlefield; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp index f61641a85..00c800173 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp @@ -10,9 +10,10 @@ #include "StdInc.h" #include "CBankInstanceConstructor.h" -#include "../JsonRandom.h" +#include "../json/JsonRandom.h" #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" +#include "../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,7 +27,7 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input) if (input.Struct().count("name") == 0) logMod->warn("Bank %s missing name!", getJsonKey()); - VLC->generaltexth->registerString(input.meta, getNameTextID(), input["name"].String()); + VLC->generaltexth->registerString(input.getModScope(), getNameTextID(), input["name"].String()); levels = input["levels"].Vector(); bankResetDuration = static_cast(input["resetDuration"].Float()); @@ -34,18 +35,19 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input) coastVisitable = input["coastVisitable"].Bool(); } -BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRandomGenerator & rng) const +BankConfig CBankInstanceConstructor::generateConfig(IGameCallback * cb, const JsonNode & level, CRandomGenerator & rng) const { BankConfig bc; + JsonRandom randomizer(cb); JsonRandom::Variables emptyVariables; bc.chance = static_cast(level["chance"].Float()); - bc.guards = JsonRandom::loadCreatures(level["guards"], rng, emptyVariables); + bc.guards = randomizer.loadCreatures(level["guards"], rng, emptyVariables); bc.resources = ResourceSet(level["reward"]["resources"]); - bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng, emptyVariables); - bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng, emptyVariables); - bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, emptyVariables); + bc.creatures = randomizer.loadCreatures(level["reward"]["creatures"], rng, emptyVariables); + bc.artifacts = randomizer.loadArtifacts(level["reward"]["artifacts"], rng, emptyVariables); + bc.spells = randomizer.loadSpells(level["reward"]["spells"], rng, emptyVariables); return bc; } @@ -70,7 +72,7 @@ void CBankInstanceConstructor::randomizeObject(CBank * bank, CRandomGenerator & cumulativeChance += static_cast(node["chance"].Float()); if(selectedChance < cumulativeChance) { - bank->setConfig(generateConfig(node, rng)); + bank->setConfig(generateConfig(bank->cb, node, rng)); break; } } @@ -82,80 +84,16 @@ CBankInfo::CBankInfo(const JsonVector & Config) : assert(!Config.empty()); } -static void addStackToArmy(IObjectInfo::CArmyStructure & army, const CCreature * crea, si32 amount) -{ - army.totalStrength += crea->getFightValue() * amount; - - bool walker = true; - if(crea->hasBonusOfType(BonusType::SHOOTER)) - { - army.shootersStrength += crea->getFightValue() * amount; - walker = false; - } - if(crea->hasBonusOfType(BonusType::FLYING)) - { - army.flyersStrength += crea->getFightValue() * amount; - walker = false; - } - if(walker) - army.walkersStrength += crea->getFightValue() * amount; -} - -IObjectInfo::CArmyStructure CBankInfo::minGuards() const -{ - JsonRandom::Variables emptyVariables; - - std::vector armies; - for(auto configEntry : config) - { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); - IObjectInfo::CArmyStructure army; - for(auto & stack : stacks) - { - assert(!stack.allowedCreatures.empty()); - auto weakest = boost::range::min_element(stack.allowedCreatures, [](const CCreature * a, const CCreature * b) - { - return a->getFightValue() < b->getFightValue(); - }); - addStackToArmy(army, *weakest, stack.minAmount); - } - armies.push_back(army); - } - return *boost::range::min_element(armies); -} - -IObjectInfo::CArmyStructure CBankInfo::maxGuards() const -{ - JsonRandom::Variables emptyVariables; - - std::vector armies; - for(auto configEntry : config) - { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); - IObjectInfo::CArmyStructure army; - for(auto & stack : stacks) - { - assert(!stack.allowedCreatures.empty()); - auto strongest = boost::range::max_element(stack.allowedCreatures, [](const CCreature * a, const CCreature * b) - { - return a->getFightValue() < b->getFightValue(); - }); - addStackToArmy(army, *strongest, stack.maxAmount); - } - armies.push_back(army); - } - return *boost::range::max_element(armies); -} - -TPossibleGuards CBankInfo::getPossibleGuards() const +TPossibleGuards CBankInfo::getPossibleGuards(IGameCallback * cb) const { JsonRandom::Variables emptyVariables; + JsonRandom randomizer(cb); TPossibleGuards out; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["guards"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); + auto stacks = randomizer.evaluateCreatures(guardsInfo, emptyVariables); IObjectInfo::CArmyStructure army; @@ -188,15 +126,16 @@ std::vector> CBankInfo::getPossibleResourcesReward() return result; } -std::vector> CBankInfo::getPossibleCreaturesReward() const +std::vector> CBankInfo::getPossibleCreaturesReward(IGameCallback * cb) const { JsonRandom::Variables emptyVariables; + JsonRandom randomizer(cb); std::vector> aproximateReward; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["reward"]["creatures"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); + auto stacks = randomizer.evaluateCreatures(guardsInfo, emptyVariables); for(auto stack : stacks) { diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.h b/lib/mapObjectConstructors/CBankInstanceConstructor.h index f926a65f2..5a609f5d2 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.h +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.h @@ -14,6 +14,7 @@ #include "../CCreatureSet.h" #include "../ResourceSet.h" +#include "../json/JsonNode.h" #include "../mapObjects/CBank.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,7 +28,7 @@ struct BankConfig std::vector artifacts; //artifacts given in case of victory std::vector spells; // granted spell(s), for Pyramid - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & chance; h & guards; @@ -55,13 +56,9 @@ class DLL_LINKAGE CBankInfo : public IObjectInfo public: CBankInfo(const JsonVector & Config); - TPossibleGuards getPossibleGuards() const; + TPossibleGuards getPossibleGuards(IGameCallback * cb) const; std::vector> getPossibleResourcesReward() const; - std::vector> getPossibleCreaturesReward() const; - - // These functions should try to evaluate minimal possible/max possible guards to give provide information on possible thread to AI - CArmyStructure minGuards() const override; - CArmyStructure maxGuards() const override; + std::vector> getPossibleCreaturesReward(IGameCallback * cb) const; bool givesResources() const override; bool givesArtifacts() const override; @@ -71,7 +68,7 @@ public: class CBankInstanceConstructor : public CDefaultObjectTypeHandler { - BankConfig generateConfig(const JsonNode & conf, CRandomGenerator & rng) const; + BankConfig generateConfig(IGameCallback * cb, const JsonNode & conf, CRandomGenerator & rng) const; JsonVector levels; @@ -92,15 +89,6 @@ public: bool hasNameTextID() const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; - - template void serialize(Handler &h, const int version) - { - h & levels; - h & bankResetDuration; - h & blockVisit; - h & coastVisitable; - h & static_cast&>(*this); - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h b/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h index 15c545906..c56e5eee5 100644 --- a/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h +++ b/lib/mapObjectConstructors/CDefaultObjectTypeHandler.h @@ -27,9 +27,9 @@ class CDefaultObjectTypeHandler : public AObjectTypeHandler randomizeObject(castedObject, rng); } - CGObjectInstance * create(std::shared_ptr tmpl = nullptr) const final + CGObjectInstance * create(IGameCallback * cb, std::shared_ptr tmpl) const final { - ObjectType * result = createObject(); + ObjectType * result = createObject(cb); preInitObject(result); @@ -44,9 +44,9 @@ class CDefaultObjectTypeHandler : public AObjectTypeHandler protected: virtual void initializeObject(ObjectType * object) const {} virtual void randomizeObject(ObjectType * object, CRandomGenerator & rng) const {} - virtual ObjectType * createObject() const + virtual ObjectType * createObject(IGameCallback * cb) const { - return new ObjectType(); + return new ObjectType(cb); } }; diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 9d62b028a..147590e2b 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -12,12 +12,12 @@ #include "../filesystem/Filesystem.h" #include "../filesystem/CBinaryReader.h" +#include "../json/JsonUtils.h" #include "../VCMI_Lib.h" #include "../GameConstants.h" #include "../constants/StringConstants.h" #include "../CGeneralTextHandler.h" #include "../GameSettings.h" -#include "../JsonNode.h" #include "../CSoundBase.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" @@ -36,12 +36,13 @@ #include "../mapObjects/CGTownInstance.h" #include "../modding/IdentifierStorage.h" #include "../modding/CModHandler.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN CObjectClassesHandler::CObjectClassesHandler() { -#define SET_HANDLER_CLASS(STRING, CLASSNAME) handlerConstructors[STRING] = std::make_shared; +#define SET_HANDLER_CLASS(STRING, CLASSNAME) handlerConstructors[STRING] = std::make_shared #define SET_HANDLER(STRING, TYPENAME) handlerConstructors[STRING] = std::make_shared> // list of all known handlers, hardcoded for now since the only way to add new objects is via C++ code @@ -96,11 +97,7 @@ CObjectClassesHandler::CObjectClassesHandler() #undef SET_HANDLER } -CObjectClassesHandler::~CObjectClassesHandler() -{ - for(auto * p : objects) - delete p; -} +CObjectClassesHandler::~CObjectClassesHandler() = default; std::vector CObjectClassesHandler::loadLegacyData() { @@ -112,13 +109,13 @@ std::vector CObjectClassesHandler::loadLegacyData() for (size_t i = 0; i < totalNumber; i++) { - auto * tmpl = new ObjectTemplate; + auto tmpl = std::make_shared(); tmpl->readTxt(parser); parser.endLine(); - std::pair key(tmpl->id.num, tmpl->subid); - legacyTemplates.insert(std::make_pair(key, std::shared_ptr(tmpl))); + std::pair key(tmpl->id, tmpl->subid); + legacyTemplates.insert(std::make_pair(key, tmpl)); } objects.resize(256); @@ -180,8 +177,10 @@ void CObjectClassesHandler::loadSubObject(const std::string & scope, const std:: auto object = loadSubObjectFromJson(scope, identifier, entry, obj, index); assert(object); - assert(obj->objects[index] == nullptr); // ensure that this id was not loaded before - obj->objects[index] = object; + if (obj->objects.at(index) != nullptr) + throw std::runtime_error("Attempt to load already loaded object:" + identifier); + + obj->objects.at(index) = object; registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype); for(const auto & compatID : entry["compatibilityIdentifiers"].Vector()) @@ -205,7 +204,7 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin auto createdObject = handlerConstructors.at(handler)(); createdObject->modScope = scope; - createdObject->typeName = obj->identifier;; + createdObject->typeName = obj->identifier; createdObject->subTypeName = identifier; createdObject->type = obj->id; @@ -224,6 +223,9 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin return createdObject; } +ObjectClass::ObjectClass() = default; +ObjectClass::~ObjectClass() = default; + std::string ObjectClass::getJsonKey() const { return modScope + ':' + identifier; @@ -239,9 +241,9 @@ std::string ObjectClass::getNameTranslated() const return VLC->generaltexth->translate(getNameTextID()); } -ObjectClass * CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index) +std::unique_ptr CObjectClassesHandler::loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index) { - auto * obj = new ObjectClass(); + auto obj = std::make_unique(); obj->modScope = scope; obj->identifier = name; @@ -257,65 +259,73 @@ ObjectClass * CObjectClassesHandler::loadFromJson(const std::string & scope, con { if (!subData.second["index"].isNull()) { - const std::string & subMeta = subData.second["index"].meta; + const std::string & subMeta = subData.second["index"].getModScope(); - if ( subMeta != "core") - logMod->warn("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first ); - size_t subIndex = subData.second["index"].Integer(); - loadSubObject(subData.second.meta, subData.first, subData.second, obj, subIndex); + if ( subMeta == "core") + { + size_t subIndex = subData.second["index"].Integer(); + loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get(), subIndex); + } + else + { + logMod->error("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first ); + loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get()); + } } else - loadSubObject(subData.second.meta, subData.first, subData.second, obj); + loadSubObject(subData.second.getModScope(), subData.first, subData.second, obj.get()); } + + if (obj->id == MapObjectID::MONOLITH_TWO_WAY) + generateExtraMonolithsForRMG(obj.get()); + return obj; } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { - auto * object = loadFromJson(scope, data, name, objects.size()); - objects.push_back(object); - VLC->identifiersHandler->registerObject(scope, "object", name, object->id); + objects.push_back(loadFromJson(scope, data, name, objects.size())); + + VLC->identifiersHandler->registerObject(scope, "object", name, objects.back()->id); } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - auto * object = loadFromJson(scope, data, name, index); - assert(objects[(si32)index] == nullptr); // ensure that this id was not loaded before - objects[static_cast(index)] = object; - VLC->identifiersHandler->registerObject(scope, "object", name, object->id); + assert(objects.at(index) == nullptr); // ensure that this id was not loaded before + + objects.at(index) = loadFromJson(scope, data, name, index); + VLC->identifiersHandler->registerObject(scope, "object", name, objects.at(index)->id); } -void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID) +void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID) { config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL - assert(ID < objects.size()); - assert(objects[ID]); + assert(objects.at(ID.getNum())); - if ( subID >= objects[ID]->objects.size()) - objects[ID]->objects.resize(subID+1); + if ( subID.getNum() >= objects.at(ID.getNum())->objects.size()) + objects.at(ID.getNum())->objects.resize(subID.getNum()+1); - JsonUtils::inherit(config, objects.at(ID)->base); - loadSubObject(config.meta, identifier, config, objects[ID], subID); + JsonUtils::inherit(config, objects.at(ID.getNum())->base); + loadSubObject(config.getModScope(), identifier, config, objects.at(ID.getNum()).get(), subID.getNum()); } -void CObjectClassesHandler::removeSubObject(si32 ID, si32 subID) +void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID) { - assert(ID < objects.size()); - assert(objects[ID]); - assert(subID < objects[ID]->objects.size()); - objects[ID]->objects[subID] = nullptr; + assert(objects.at(ID.getNum())); + objects.at(ID.getNum())->objects.at(subID.getNum()) = nullptr; } -std::vector CObjectClassesHandler::getDefaultAllowed() const -{ - return std::vector(); //TODO? -} - -TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) const +TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const { try { - auto result = objects.at(type)->objects.at(subtype); + if (objects.at(type.getNum()) == nullptr) + return objects.front()->objects.front(); + + auto subID = subtype.getNum(); + if (type == Obj::PRISON) + subID = 0; + auto result = objects.at(type.getNum())->objects.at(subID); if (result != nullptr) return result; @@ -325,7 +335,7 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) // Leave catch block silently } - std::string errorString = "Failed to find object of type " + std::to_string(type) + "::" + std::to_string(subtype); + std::string errorString = "Failed to find object of type " + std::to_string(type.getNum()) + "::" + std::to_string(subtype.getNum()); logGlobal->error(errorString); throw std::runtime_error(errorString); } @@ -335,11 +345,11 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scop std::optional id = VLC->identifiers()->getIdentifier(scope, "object", type); if(id) { - auto * object = objects[id.value()]; + const auto & object = objects.at(id.value()); std::optional subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype); if (subID) - return object->objects[subID.value()]; + return object->objects.at(subID.value()); } std::string errorString = "Failed to find object of type " + type + "::" + subtype; @@ -352,28 +362,28 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID comp return getHandlerFor(compoundIdentifier.primaryID, compoundIdentifier.secondaryID); } -std::set CObjectClassesHandler::knownObjects() const +std::set CObjectClassesHandler::knownObjects() const { - std::set ret; + std::set ret; - for(auto * entry : objects) + for(auto & entry : objects) if (entry) ret.insert(entry->id); return ret; } -std::set CObjectClassesHandler::knownSubObjects(si32 primaryID) const +std::set CObjectClassesHandler::knownSubObjects(MapObjectID primaryID) const { - std::set ret; + std::set ret; - if (!objects.at(primaryID)) + if (!objects.at(primaryID.getNum())) { logGlobal->error("Failed to find object %d", primaryID); return ret; } - for(const auto & entry : objects.at(primaryID)->objects) + for(const auto & entry : objects.at(primaryID.getNum())->objects) if (entry) ret.insert(entry->subtype); @@ -406,7 +416,7 @@ void CObjectClassesHandler::beforeValidate(JsonNode & object) void CObjectClassesHandler::afterLoadFinalization() { - for(auto * entry : objects) + for(auto & entry : objects) { if (!entry) continue; @@ -421,14 +431,12 @@ void CObjectClassesHandler::afterLoadFinalization() logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey()); } } - - generateExtraMonolithsForRMG(); } -void CObjectClassesHandler::generateExtraMonolithsForRMG() +void CObjectClassesHandler::generateExtraMonolithsForRMG(ObjectClass * container) { //duplicate existing two-way portals to make reserve for RMG - auto& portalVec = objects[Obj::MONOLITH_TWO_WAY]->objects; + auto& portalVec = container->objects; //FIXME: Monoliths in this vector can be already not useful for every terrain const size_t portalCount = portalVec.size(); @@ -446,7 +454,6 @@ void CObjectClassesHandler::generateExtraMonolithsForRMG() //deep copy of noncopyable object :? auto newPortal = std::make_shared>(); newPortal->rmgInfo = portal->getRMGInfo(); - newPortal->base = portal->base; //not needed? newPortal->templates = portal->getTemplates(); newPortal->sounds = portal->getSounds(); newPortal->aiValue = portal->getAiValue(); @@ -456,21 +463,30 @@ void CObjectClassesHandler::generateExtraMonolithsForRMG() newPortal->subTypeName = std::string("monolith") + std::to_string(portalVec.size()); newPortal->type = portal->getIndex(); - newPortal->subtype = portalVec.size(); //indexes must be unique, they are returned as a set + // Inconsintent original indexing: monolith1 has index 0 + newPortal->subtype = portalVec.size() - 1; //indexes must be unique, they are returned as a set + newPortal->blockVisit = portal->blockVisit; + newPortal->removable = portal->removable; + portalVec.push_back(newPortal); + + registerObject(newPortal->modScope, container->getJsonKey(), newPortal->subTypeName, newPortal->subtype); } } -std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const +std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubID subtype) const { const auto handler = getHandlerFor(type, subtype); if (handler && handler->hasNameTextID()) return handler->getNameTranslated(); - else - return objects[type]->getNameTranslated(); + + if (objects.at(type.getNum())) + return objects.at(type.getNum())->getNameTranslated(); + + return objects.front()->getNameTranslated(); } -SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) const +SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObjectSubID subtype) const { // TODO: these objects may have subID's that does not have associated handler: // Prison: uses hero type as subID @@ -479,21 +495,27 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) co if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL) subtype = 0; - assert(type < objects.size()); - assert(objects[type]); - assert(subtype < objects[type]->objects.size()); - - return getHandlerFor(type, subtype)->getSounds(); + if(objects.at(type.getNum())) + return getHandlerFor(type, subtype)->getSounds(); + else + return objects.front()->objects.front()->getSounds(); } -std::string CObjectClassesHandler::getObjectHandlerName(si32 type) const +std::string CObjectClassesHandler::getObjectHandlerName(MapObjectID type) const { - return objects.at(type)->handlerName; + if (objects.at(type.getNum())) + return objects.at(type.getNum())->handlerName; + else + return objects.front()->handlerName; } -std::string CObjectClassesHandler::getJsonKey(si32 type) const +std::string CObjectClassesHandler::getJsonKey(MapObjectID type) const { - return objects.at(type)->getJsonKey(); + if (objects.at(type.getNum()) != nullptr) + return objects.at(type.getNum())->getJsonKey(); + + logGlobal->warn("Unknown object of type %d!", type); + return objects.front()->getJsonKey(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index f75f9c434..b1cb1ca64 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -9,8 +9,9 @@ */ #pragma once +#include "../constants/EntityIdentifiers.h" #include "../IHandlerBase.h" -#include "../JsonNode.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -45,7 +46,7 @@ class CGObjectInstance; using TObjectTypeHandler = std::shared_ptr; /// Class responsible for creation of adventure map objects of specific type -class DLL_LINKAGE ObjectClass +class DLL_LINKAGE ObjectClass : boost::noncopyable { public: std::string modScope; @@ -57,34 +58,25 @@ public: JsonNode base; std::vector objects; - ObjectClass() = default; + ObjectClass(); + ~ObjectClass(); std::string getJsonKey() const; std::string getNameTextID() const; std::string getNameTranslated() const; - - template void serialize(Handler &h, const int version) - { - h & id; - h & handlerName; - h & base; - h & objects; - h & identifier; - h & modScope; - } }; /// Main class responsible for creation of all adventure map objects -class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase +class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase, boost::noncopyable { /// list of object handlers, each of them handles only one type - std::vector objects; + std::vector< std::unique_ptr > objects; /// map that is filled during contruction with all known handlers. Not serializeable due to usage of std::function std::map > handlerConstructors; /// container with H3 templates, used only during loading, no need to serialize it - using TTemplatesContainer = std::multimap, std::shared_ptr>; + using TTemplatesContainer = std::multimap, std::shared_ptr>; TTemplatesContainer legacyTemplates; TObjectTypeHandler loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index); @@ -92,9 +84,9 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase void loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj); void loadSubObject(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index); - ObjectClass * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index); + std::unique_ptr loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index); - void generateExtraMonolithsForRMG(); + void generateExtraMonolithsForRMG(ObjectClass * container); public: CObjectClassesHandler(); @@ -105,36 +97,29 @@ public: void loadObject(std::string scope, std::string name, const JsonNode & data) override; void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; - void loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID); - void removeSubObject(si32 ID, si32 subID); + void loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID); + void removeSubObject(MapObjectID ID, MapObjectSubID subID); void beforeValidate(JsonNode & object) override; void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; - /// Queries to detect loaded objects - std::set knownObjects() const; - std::set knownSubObjects(si32 primaryID) const; + std::set knownObjects() const; + std::set knownSubObjects(MapObjectID primaryID) const; /// returns handler for specified object (ID-based). ObjectHandler keeps ownership - TObjectTypeHandler getHandlerFor(si32 type, si32 subtype) const; + TObjectTypeHandler getHandlerFor(MapObjectID type, MapObjectSubID subtype) const; TObjectTypeHandler getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const; TObjectTypeHandler getHandlerFor(CompoundMapObjectID compoundIdentifier) const; - std::string getObjectName(si32 type, si32 subtype) const; + std::string getObjectName(MapObjectID type, MapObjectSubID subtype) const; - SObjectSounds getObjectSounds(si32 type, si32 subtype) const; + SObjectSounds getObjectSounds(MapObjectID type, MapObjectSubID subtype) const; /// Returns handler string describing the handler (for use in client) - std::string getObjectHandlerName(si32 type) const; + std::string getObjectHandlerName(MapObjectID type) const; - std::string getJsonKey(si32 type) const; - - template void serialize(Handler &h, const int version) - { - h & objects; - } + std::string getJsonKey(MapObjectID type) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index c8b802586..72d61e796 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -22,7 +22,7 @@ void CRewardableConstructor::initTypeData(const JsonNode & config) blockVisit = config["blockedVisitable"].Bool(); if (!config["name"].isNull()) - VLC->generaltexth->registerString( config.meta, getNameTextID(), config["name"].String()); + VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"].String()); } @@ -31,9 +31,9 @@ bool CRewardableConstructor::hasNameTextID() const return !objectInfo.getParameters()["name"].isNull(); } -CGObjectInstance * CRewardableConstructor::create(std::shared_ptr tmpl) const +CGObjectInstance * CRewardableConstructor::create(IGameCallback * cb, std::shared_ptr tmpl) const { - auto * ret = new CRewardableObject(); + auto * ret = new CRewardableObject(cb); preInitObject(ret); ret->appearance = tmpl; ret->blockVisit = blockVisit; @@ -44,7 +44,7 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, CRandomG { if(auto * rewardableObject = dynamic_cast(object)) { - objectInfo.configureObject(rewardableObject->configuration, rng); + objectInfo.configureObject(rewardableObject->configuration, rng, object->cb); for(auto & rewardInfo : rewardableObject->configuration.info) { for (auto & bonus : rewardInfo.reward.bonuses) diff --git a/lib/mapObjectConstructors/CRewardableConstructor.h b/lib/mapObjectConstructors/CRewardableConstructor.h index 0461148fa..81fd286e6 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.h +++ b/lib/mapObjectConstructors/CRewardableConstructor.h @@ -25,18 +25,11 @@ class DLL_LINKAGE CRewardableConstructor : public AObjectTypeHandler public: bool hasNameTextID() const override; - CGObjectInstance * create(std::shared_ptr tmpl = nullptr) const override; + CGObjectInstance * create(IGameCallback * cb, std::shared_ptr tmpl = nullptr) const override; void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; - - template void serialize(Handler &h, const int version) - { - AObjectTypeHandler::serialize(h, version); - - h & objectInfo; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 800e930af..806b32194 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -14,7 +14,7 @@ #include "../CHeroHandler.h" #include "../CTownHandler.h" #include "../IGameCallback.h" -#include "../JsonRandom.h" +#include "../json/JsonRandom.h" #include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../VCMI_Lib.h" @@ -67,7 +67,7 @@ void CTownInstanceConstructor::initTypeData(const JsonNode & input) // change scope of "filters" to scope of object that is being loaded // since this filters require to resolve building ID's - filtersJson.setMeta(input["faction"].meta); + filtersJson.setModScope(input["faction"].getModScope()); } void CTownInstanceConstructor::afterLoadFinalization() @@ -77,7 +77,7 @@ void CTownInstanceConstructor::afterLoadFinalization() { filters[entry.first] = LogicalExpression(entry.second, [this](const JsonNode & node) { - return BuildingID(VLC->identifiers()->getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value()); + return BuildingID(VLC->identifiers()->getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value_or(-1)); }); } } @@ -102,7 +102,7 @@ void CTownInstanceConstructor::initializeObject(CGTownInstance * obj) const void CTownInstanceConstructor::randomizeObject(CGTownInstance * object, CRandomGenerator & rng) const { - auto templ = getOverride(CGObjectInstance::cb->getTile(object->pos)->terType->getId(), object); + auto templ = getOverride(object->cb->getTile(object->pos)->terType->getId(), object); if(templ) object->appearance = templ; } @@ -122,7 +122,7 @@ void CHeroInstanceConstructor::initTypeData(const JsonNode & input) VLC->identifiers()->requestIdentifier( "heroClass", input["heroClass"], - [&](si32 index) { heroClass = VLC->heroh->classes[index]; }); + [&](si32 index) { heroClass = HeroClassID(index).toHeroClass(); }); filtersJson = input["filters"]; } @@ -133,7 +133,7 @@ void CHeroInstanceConstructor::afterLoadFinalization() { filters[entry.first] = LogicalExpression(entry.second, [](const JsonNode & node) { - return HeroTypeID(VLC->identifiers()->getIdentifier("hero", node.Vector()[0]).value()); + return HeroTypeID(VLC->identifiers()->getIdentifier("hero", node.Vector()[0]).value_or(-1)); }); } } @@ -224,7 +224,7 @@ void MarketInstanceConstructor::initTypeData(const JsonNode & input) speech = input["speech"].String(); } -CGMarket * MarketInstanceConstructor::createObject() const +CGMarket * MarketInstanceConstructor::createObject(IGameCallback * cb) const { if(marketModes.size() == 1) { @@ -232,13 +232,18 @@ CGMarket * MarketInstanceConstructor::createObject() const { case EMarketMode::ARTIFACT_RESOURCE: case EMarketMode::RESOURCE_ARTIFACT: - return new CGBlackMarket; + return new CGBlackMarket(cb); case EMarketMode::RESOURCE_SKILL: - return new CGUniversity; + return new CGUniversity(cb); } } - return new CGMarket; + else if(marketModes.size() == 2) + { + if(vstd::contains(marketModes, EMarketMode::ARTIFACT_EXP)) + return new CGArtifactsAltar(cb); + } + return new CGMarket(cb); } void MarketInstanceConstructor::initializeObject(CGMarket * market) const @@ -256,12 +261,13 @@ void MarketInstanceConstructor::initializeObject(CGMarket * market) const void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenerator & rng) const { + JsonRandom randomizer(object->cb); JsonRandom::Variables emptyVariables; if(auto * university = dynamic_cast(object)) { - for(auto skill : JsonRandom::loadSecondaries(predefinedOffer, rng, emptyVariables)) - university->skills.push_back(skill.first.getNum()); + for(auto skill : randomizer.loadSecondaries(predefinedOffer, rng, emptyVariables)) + university->skills.push_back(skill.first); } } diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 5c50ded30..ac5105b27 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -57,7 +57,7 @@ protected: void initTypeData(const JsonNode & input) override; public: - CFaction * faction = nullptr; + const CFaction * faction = nullptr; std::map> filters; void initializeObject(CGTownInstance * object) const override; @@ -66,14 +66,6 @@ public: bool hasNameTextID() const override; std::string getNameTextID() const override; - - template void serialize(Handler &h, const int version) - { - h & filtersJson; - h & faction; - h & filters; - h & static_cast&>(*this); - } }; class CHeroInstanceConstructor : public CDefaultObjectTypeHandler @@ -84,7 +76,7 @@ protected: void initTypeData(const JsonNode & input) override; public: - CHeroClass * heroClass = nullptr; + const CHeroClass * heroClass = nullptr; std::map> filters; void initializeObject(CGHeroInstance * object) const override; @@ -93,14 +85,6 @@ public: bool hasNameTextID() const override; std::string getNameTextID() const override; - - template void serialize(Handler &h, const int version) - { - h & filtersJson; - h & heroClass; - h & filters; - h & static_cast&>(*this); - } }; class DLL_LINKAGE BoatInstanceConstructor : public CDefaultObjectTypeHandler @@ -122,18 +106,6 @@ public: /// Returns boat preview animation, for use in Shipyards AnimationPath getBoatAnimationName() const; - - template void serialize(Handler &h, const int version) - { - h & static_cast&>(*this); - h & layer; - h & onboardAssaultAllowed; - h & onboardVisitAllowed; - h & bonuses; - h & actualAnimation; - h & overlayAnimation; - h & flagAnimations; - } }; class MarketInstanceConstructor : public CDefaultObjectTypeHandler @@ -145,19 +117,14 @@ protected: JsonNode predefinedOffer; int marketEfficiency; - std::string title, speech; + std::string title; + std::string speech; public: - CGMarket * createObject() const override; + CGMarket * createObject(IGameCallback * cb) const override; void initializeObject(CGMarket * object) const override; void randomizeObject(CGMarket * object, CRandomGenerator & rng) const override; - template void serialize(Handler &h, const int version) - { - h & static_cast&>(*this); - h & marketModes; - h & marketEfficiency; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp index 6a137b976..83f20becf 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp @@ -12,7 +12,7 @@ #include "../CCreatureHandler.h" #include "../CGeneralTextHandler.h" -#include "../JsonRandom.h" +#include "../json/JsonRandom.h" #include "../VCMI_Lib.h" #include "../mapObjects/CGDwelling.h" #include "../modding/IdentifierStorage.h" @@ -29,23 +29,23 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input) if (input.Struct().count("name") == 0) logMod->warn("Dwelling %s missing name!", getJsonKey()); - VLC->generaltexth->registerString( input.meta, getNameTextID(), input["name"].String()); + VLC->generaltexth->registerString( input.getModScope(), getNameTextID(), input["name"].String()); const JsonVector & levels = input["creatures"].Vector(); const auto totalLevels = levels.size(); availableCreatures.resize(totalLevels); - for(auto currentLevel = 0; currentLevel < totalLevels; currentLevel++) + for(int currentLevel = 0; currentLevel < totalLevels; currentLevel++) { const JsonVector & creaturesOnLevel = levels[currentLevel].Vector(); const auto creaturesNumber = creaturesOnLevel.size(); availableCreatures[currentLevel].resize(creaturesNumber); - for(auto currentCreature = 0; currentCreature < creaturesNumber; currentCreature++) + for(int currentCreature = 0; currentCreature < creaturesNumber; currentCreature++) { - VLC->identifiers()->requestIdentifier("creature", creaturesOnLevel[currentCreature], [=] (si32 index) + VLC->identifiers()->requestIdentifier("creature", creaturesOnLevel[currentCreature], [this, currentLevel, currentCreature] (si32 index) { - availableCreatures[currentLevel][currentCreature] = VLC->creh->objects[index]; + availableCreatures.at(currentLevel).at(currentCreature) = CreatureID(index).toCreature(); }); } assert(!availableCreatures[currentLevel].empty()); @@ -68,9 +68,9 @@ void DwellingInstanceConstructor::initializeObject(CGDwelling * obj) const } } -void DwellingInstanceConstructor::randomizeObject(CGDwelling * object, CRandomGenerator &rng) const +void DwellingInstanceConstructor::randomizeObject(CGDwelling * dwelling, CRandomGenerator &rng) const { - auto * dwelling = dynamic_cast(object); + JsonRandom randomizer(dwelling->cb); dwelling->creatures.clear(); dwelling->creatures.reserve(availableCreatures.size()); @@ -94,7 +94,7 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * object, CRandomGe else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) //custom guards (eg. Elemental Conflux) { JsonRandom::Variables emptyVariables; - for(auto & stack : JsonRandom::loadCreatures(guards, rng, emptyVariables)) + for(auto & stack : randomizer.loadCreatures(guards, rng, emptyVariables)) { dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.type->getId(), stack.count)); } diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.h b/lib/mapObjectConstructors/DwellingInstanceConstructor.h index 4022be309..bf68fdb26 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.h +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.h @@ -11,6 +11,9 @@ #include "CDefaultObjectTypeHandler.h" +#include "../json/JsonNode.h" +#include "../mapObjects/CGDwelling.h" + VCMI_LIB_NAMESPACE_BEGIN class CGDwelling; @@ -33,13 +36,6 @@ public: bool producesCreature(const CCreature * crea) const; std::vector getProducedCreatures() const; - - template void serialize(Handler &h, const int version) - { - h & availableCreatures; - h & guards; - h & static_cast&>(*this); - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/HillFortInstanceConstructor.h b/lib/mapObjectConstructors/HillFortInstanceConstructor.h index 526e66571..645270f17 100644 --- a/lib/mapObjectConstructors/HillFortInstanceConstructor.h +++ b/lib/mapObjectConstructors/HillFortInstanceConstructor.h @@ -10,6 +10,8 @@ #pragma once #include "CDefaultObjectTypeHandler.h" +#include "../json/JsonNode.h" +#include "../mapObjects/MiscObjects.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,11 +26,6 @@ protected: void initializeObject(HillFort * object) const override; public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/IObjectInfo.h b/lib/mapObjectConstructors/IObjectInfo.h index 29c0ff66f..280402fa8 100644 --- a/lib/mapObjectConstructors/IObjectInfo.h +++ b/lib/mapObjectConstructors/IObjectInfo.h @@ -34,11 +34,6 @@ public: } }; - /// Returns possible composition of guards. Actual guards would be - /// somewhere between these two values - virtual CArmyStructure minGuards() const { return CArmyStructure(); } - virtual CArmyStructure maxGuards() const { return CArmyStructure(); } - virtual bool givesResources() const { return false; } virtual bool givesExperience() const { return false; } diff --git a/lib/mapObjectConstructors/RandomMapInfo.h b/lib/mapObjectConstructors/RandomMapInfo.h index 5bb98daf6..fe5dc2bd3 100644 --- a/lib/mapObjectConstructors/RandomMapInfo.h +++ b/lib/mapObjectConstructors/RandomMapInfo.h @@ -33,14 +33,6 @@ struct DLL_LINKAGE RandomMapInfo {} void setMapLimit(ui32 val) { mapLimit = val; } - - template void serialize(Handler &h, const int version) - { - h & value; - h & mapLimit; - h & zoneLimit; - h & rarity; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/SObjectSounds.h b/lib/mapObjectConstructors/SObjectSounds.h index 8d41817c0..5f94cd44a 100644 --- a/lib/mapObjectConstructors/SObjectSounds.h +++ b/lib/mapObjectConstructors/SObjectSounds.h @@ -18,13 +18,6 @@ struct SObjectSounds std::vector ambient; std::vector visit; std::vector removal; - - template void serialize(Handler &h, const int version) - { - h & ambient; - h & visit; - h & removal; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShipyardInstanceConstructor.h b/lib/mapObjectConstructors/ShipyardInstanceConstructor.h index c0d47be4f..8d33b7a76 100644 --- a/lib/mapObjectConstructors/ShipyardInstanceConstructor.h +++ b/lib/mapObjectConstructors/ShipyardInstanceConstructor.h @@ -10,6 +10,8 @@ #pragma once #include "CDefaultObjectTypeHandler.h" +#include "../json/JsonNode.h" +#include "../mapObjects/MiscObjects.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,11 +26,6 @@ protected: void initializeObject(CGShipyard * object) const override; public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index 39f7ed971..b936863be 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -16,10 +16,11 @@ #include "../CGeneralTextHandler.h" #include "../gameState/CGameState.h" #include "../CPlayerState.h" +#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN -void CArmedInstance::randomizeArmy(int type) +void CArmedInstance::randomizeArmy(FactionID type) { for (auto & elem : stacks) { @@ -39,13 +40,14 @@ void CArmedInstance::randomizeArmy(int type) // Take Angelic Alliance troop-mixing freedom of non-evil units into account. CSelector CArmedInstance::nonEvilAlignmentMixSelector = Selector::type()(BonusType::NONEVIL_ALIGNMENT_MIX); -CArmedInstance::CArmedInstance() - :CArmedInstance(false) +CArmedInstance::CArmedInstance(IGameCallback *cb) + :CArmedInstance(cb, false) { } -CArmedInstance::CArmedInstance(bool isHypotetic): - CBonusSystemNode(isHypotetic), +CArmedInstance::CArmedInstance(IGameCallback *cb, bool isHypothetic): + CGObjectInstance(cb), + CBonusSystemNode(isHypothetic), nonEvilAlignmentMix(this, nonEvilAlignmentMixSelector), battle(nullptr) { @@ -73,7 +75,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() for(const auto & slot : Slots()) { const CStackInstance * inst = slot.second; - const CCreature * creature = VLC->creh->objects[inst->getCreatureID()]; + const auto * creature = inst->getCreatureID().toEntity(VLC); factions.insert(creature->getFaction()); // Check for undead flag instead of faction (undead mummies are neutral) @@ -92,7 +94,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() for(auto f : factions) { - if (VLC->factions()->getByIndex(f)->getAlignment() != EAlignment::EVIL) + if (VLC->factions()->getById(f)->getAlignment() != EAlignment::EVIL) mixableFactions++; } if (mixableFactions > 0) @@ -110,8 +112,13 @@ void CArmedInstance::updateMoraleBonusFromArmy() else if (!factions.empty()) // no bonus from empty garrison { b->val = 2 - static_cast(factionsInArmy); - description = boost::str(boost::format(VLC->generaltexth->arraytxt[114]) % factionsInArmy % b->val); //Troops of %d alignments %d - description = b->description.substr(0, description.size()-2);//trim value + MetaString formatter; + formatter.appendTextID("core.arraytxt.114"); //Troops of %d alignments %d + formatter.replaceNumber(factionsInArmy); + formatter.replaceNumber(b->val); + + description = formatter.toString(); + description = description.substr(0, description.size()-3);//trim value } boost::algorithm::trim(description); diff --git a/lib/mapObjects/CArmedInstance.h b/lib/mapObjects/CArmedInstance.h index 378dcf96c..8c50cba16 100644 --- a/lib/mapObjects/CArmedInstance.h +++ b/lib/mapObjects/CArmedInstance.h @@ -29,7 +29,7 @@ private: public: BattleInfo *battle; //set to the current battle, if engaged - void randomizeArmy(int type); + void randomizeArmy(FactionID type); virtual void updateMoraleBonusFromArmy(); void armyChanged() override; @@ -42,8 +42,8 @@ public: virtual CBonusSystemNode & whatShouldBeAttached(); ////////////////////////////////////////////////////////////////////////// - CArmedInstance(); - CArmedInstance(bool isHypotetic); + CArmedInstance(IGameCallback *cb); + CArmedInstance(IGameCallback *cb, bool isHypothetic); PlayerColor getOwner() const override { @@ -52,7 +52,7 @@ public: void serializeJsonOptions(JsonSerializeFormat & handler) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 53f219939..9dc7f48f4 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -37,8 +37,10 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } -//must be instantiated in .cpp file for access to complete types of all member fields -CBank::CBank() = default; +CBank::CBank(IGameCallback *cb) + : CArmedInstance(cb) +{} + //must be instantiated in .cpp file for access to complete types of all member fields CBank::~CBank() = default; @@ -46,7 +48,7 @@ void CBank::initObj(CRandomGenerator & rand) { daycounter = 0; resetDuration = 0; - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } bool CBank::isCoastVisitable() const @@ -70,6 +72,9 @@ std::vector CBank::getPopupComponents(PlayerColor player) const if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) return {}; + if (bc == nullptr) + return {}; + std::map guardsAmounts; std::vector result; @@ -79,11 +84,7 @@ std::vector CBank::getPopupComponents(PlayerColor player) const for (auto const & guard : guardsAmounts) { - Component comp; - comp.id = Component::EComponentType::CREATURE; - comp.subtype = guard.first.getNum(); - comp.val = guard.second; - + Component comp(ComponentType::CREATURE, guard.first, guard.second); result.push_back(comp); } return result; @@ -98,12 +99,12 @@ void CBank::setConfig(const BankConfig & config) setCreature (SlotID(stacksCount()), stack.type->getId(), stack.count); } -void CBank::setPropertyDer (ui8 what, ui32 val) +void CBank::setPropertyDer (ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::BANK_DAYCOUNTER: //daycounter - daycounter+=val; + daycounter+= identifier.getNum(); break; case ObjProperty::BANK_RESET: // FIXME: Object reset must be done by separate netpack from server @@ -123,9 +124,9 @@ void CBank::newTurn(CRandomGenerator & rand) const if (resetDuration != 0) { if (daycounter >= resetDuration) - cb->setObjProperty (id, ObjProperty::BANK_RESET, 0); //daycounter 0 + cb->setObjPropertyValue(id, ObjProperty::BANK_RESET); //daycounter 0 else - cb->setObjProperty (id, ObjProperty::BANK_DAYCOUNTER, 1); //daycounter++ + cb->setObjPropertyValue(id, ObjProperty::BANK_DAYCOUNTER, 1); //daycounter++ } } } @@ -141,7 +142,7 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const cb->sendAndApply(&cov); int banktext = 0; - switch (ID) + switch (ID.toEnum()) { case Obj::DERELICT_SHIP: banktext = 41; @@ -184,7 +185,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const if (bc) { - switch (ID) + switch (ID.toEnum()) { case Obj::DERELICT_SHIP: textID = 43; @@ -207,20 +208,20 @@ void CBank::doVisit(const CGHeroInstance * hero) const } else { - switch (ID) + switch (ID.toEnum()) { case Obj::SHIPWRECK: case Obj::DERELICT_SHIP: case Obj::CRYPT: { GiveBonus gbonus; - gbonus.id = hero->id.getNum(); + gbonus.id = hero->id; gbonus.bonus.duration = BonusDuration::ONE_BATTLE; gbonus.bonus.source = BonusSource::OBJECT_TYPE; gbonus.bonus.sid = BonusSourceID(ID); gbonus.bonus.type = BonusType::MORALE; gbonus.bonus.val = -1; - switch (ID) + switch (ID.toEnum()) { case Obj::SHIPWRECK: textID = 123; @@ -236,7 +237,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const break; } cb->giveHeroBonus(&gbonus); - iw.components.emplace_back(Component::EComponentType::MORALE, 0, -1, 0); + iw.components.emplace_back(ComponentType::MORALE, -1); iw.soundID = soundBase::GRAVEYARD; break; } @@ -244,10 +245,10 @@ void CBank::doVisit(const CGHeroInstance * hero) const { GiveBonus gb; gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, -2, BonusSourceID(id), VLC->generaltexth->arraytxt[70]); - gb.id = hero->id.getNum(); + gb.id = hero->id; cb->giveHeroBonus(&gb); textID = 107; - iw.components.emplace_back(Component::EComponentType::LUCK, 0, -2, 0); + iw.components.emplace_back(ComponentType::LUCK, -2); break; } case Obj::CREATURE_BANK: @@ -267,24 +268,24 @@ void CBank::doVisit(const CGHeroInstance * hero) const //grant resources if (bc) { - for (int it = 0; it < bc->resources.size(); it++) + for (GameResID it : GameResID::ALL_RESOURCES()) { if (bc->resources[it] != 0) { - iw.components.emplace_back(Component::EComponentType::RESOURCE, it, bc->resources[it], 0); + iw.components.emplace_back(ComponentType::RESOURCE, it, bc->resources[it]); loot.appendRawString("%d %s"); - loot.replaceNumber(iw.components.back().val); - loot.replaceLocalString(EMetaText::RES_NAMES, iw.components.back().subtype); - cb->giveResource(hero->getOwner(), static_cast(it), bc->resources[it]); + loot.replaceNumber(bc->resources[it]); + loot.replaceName(it); + cb->giveResource(hero->getOwner(), it, bc->resources[it]); } } //grant artifacts for (auto & elem : bc->artifacts) { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, elem); loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - cb->giveHeroNewArtifact(hero, VLC->arth->objects[elem], ArtifactPosition::FIRST_AVAILABLE); + loot.replaceName(elem); + cb->giveHeroNewArtifact(hero, elem.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); } //display loot if (!iw.components.empty()) @@ -297,7 +298,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const return a.type->getFightValue() < b.type->getFightValue(); })->type; - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, strongest->getId()); + iw.text.replaceNamePlural(strongest->getId()); iw.text.replaceRawString(loot.buildList()); } cb->showInfoDialog(&iw); @@ -318,14 +319,14 @@ void CBank::doVisit(const CGHeroInstance * hero) const } for(const SpellID & spellId : bc->spells) { - const auto * spell = spellId.toSpell(VLC->spells()); - iw.text.appendLocalString(EMetaText::SPELL_NAME, spellId); + const auto * spell = spellId.toEntity(VLC); + iw.text.appendName(spellId); if(spell->getLevel() <= hero->maxSpellLevel()) { if(hero->canLearnSpell(spell)) { spells.insert(spellId); - iw.components.emplace_back(Component::EComponentType::SPELL, spellId, 0, 0); + iw.components.emplace_back(ComponentType::SPELL, spellId); } } else @@ -356,9 +357,9 @@ void CBank::doVisit(const CGHeroInstance * hero) const for(const auto & elem : ourArmy.Slots()) { - iw.components.emplace_back(*elem.second); + iw.components.emplace_back(ComponentType::CREATURE, elem.second->getId(), elem.second->getCount()); loot.appendRawString("%s"); - loot.replaceCreatureName(*elem.second); + loot.replaceName(*elem.second); } if(ourArmy.stacksCount()) @@ -373,7 +374,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const cb->showInfoDialog(&iw); cb->giveCreatures(this, hero, ourArmy, false); } - cb->setObjProperty(id, ObjProperty::BANK_CLEAR, 0); //bc = nullptr + cb->setObjPropertyValue(id, ObjProperty::BANK_CLEAR); //bc = nullptr } } diff --git a/lib/mapObjects/CBank.h b/lib/mapObjects/CBank.h index 8f7105689..9377e83ed 100644 --- a/lib/mapObjects/CBank.h +++ b/lib/mapObjects/CBank.h @@ -23,11 +23,11 @@ class DLL_LINKAGE CBank : public CArmedInstance ui32 resetDuration; bool coastVisitable; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void doVisit(const CGHeroInstance * hero) const; public: - CBank(); + CBank(IGameCallback *cb); ~CBank() override; void setConfig(const BankConfig & bc); @@ -43,7 +43,7 @@ public: std::vector getPopupComponents(PlayerColor player) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & daycounter; diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index b0c9c8384..40edde0ca 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -20,6 +20,7 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/StackLocation.h" #include "../serializer/JsonSerializeFormat.h" +#include "../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -28,11 +29,10 @@ std::string CGCreature::getHoverText(PlayerColor player) const if(stacks.empty()) { //should not happen... - logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), subID, id.getNum()); + logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), getCreature(), id.getNum()); return "INVALID_STACK"; } - std::string hoverName; MetaString ms; CCreature::CreatureQuantityId monsterQuantityId = stacks.begin()->second->getQuantityID(); int quantityTextIndex = 172 + 3 * (int)monsterQuantityId; @@ -41,21 +41,34 @@ std::string CGCreature::getHoverText(PlayerColor player) const else ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID); - hoverName = ms.toString(); - return hoverName; + ms.appendNamePlural(getCreature()); + + return ms.toString(); } std::string CGCreature::getHoverText(const CGHeroInstance * hero) const { - std::string hoverName; if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters)) { MetaString ms; ms.appendNumber(stacks.begin()->second->count); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID); + ms.appendName(getCreature(), stacks.begin()->second->count); + return ms.toString(); + } + else + { + return getHoverText(hero->tempOwner); + } +} +std::string CGCreature::getPopupText(const CGHeroInstance * hero) const +{ + std::string hoverName; + if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters)) + { + MetaString ms; + ms.appendRawString(getHoverText(hero)); ms.appendRawString("\n\n"); int decision = takenAction(hero, true); @@ -72,10 +85,10 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const ms.appendLocalString(EMetaText::GENERAL_TXT,243); break; default: //decision = cost in gold - ms.appendRawString(boost::str(boost::format(VLC->generaltexth->allTexts[244]) % decision)); + ms.appendLocalString(EMetaText::GENERAL_TXT,244); + ms.replaceNumber(decision); break; } - hoverName = ms.toString(); } else @@ -83,27 +96,42 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const hoverName = getHoverText(hero->tempOwner); } - hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); + if (settings["general"]["enableUiEnhancements"].Bool()) + { + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); - int choice; - double ratio = (static_cast(getArmyStrength()) / hero->getTotalStrength()); - if (ratio < 0.1) choice = 0; - else if (ratio < 0.25) choice = 1; - else if (ratio < 0.6) choice = 2; - else if (ratio < 0.9) choice = 3; - else if (ratio < 1.1) choice = 4; - else if (ratio < 1.3) choice = 5; - else if (ratio < 1.8) choice = 6; - else if (ratio < 2.5) choice = 7; - else if (ratio < 4) choice = 8; - else if (ratio < 8) choice = 9; - else if (ratio < 20) choice = 10; - else choice = 11; + int choice; + double ratio = (static_cast(getArmyStrength()) / hero->getTotalStrength()); + if (ratio < 0.1) choice = 0; + else if (ratio < 0.25) choice = 1; + else if (ratio < 0.6) choice = 2; + else if (ratio < 0.9) choice = 3; + else if (ratio < 1.1) choice = 4; + else if (ratio < 1.3) choice = 5; + else if (ratio < 1.8) choice = 6; + else if (ratio < 2.5) choice = 7; + else if (ratio < 4) choice = 8; + else if (ratio < 8) choice = 9; + else if (ratio < 20) choice = 10; + else choice = 11; - hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice)); + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice)); + } return hoverName; } +std::string CGCreature::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} + +std::vector CGCreature::getPopupComponents(PlayerColor player) const +{ + return { + Component(ComponentType::CREATURE, getCreature()) + }; +} + void CGCreature::onHeroVisit( const CGHeroInstance * h ) const { //show message @@ -132,7 +160,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT, 86); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID); + ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); cb->showBlockingDialog(&ynd); break; } @@ -146,13 +174,50 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const std::string tmp = VLC->generaltexth->advobtxt[90]; boost::algorithm::replace_first(tmp, "%d", std::to_string(getStackCount(SlotID(0)))); boost::algorithm::replace_first(tmp, "%d", std::to_string(action)); - boost::algorithm::replace_first(tmp,"%s",VLC->creh->objects[subID]->getNamePluralTranslated()); + boost::algorithm::replace_first(tmp,"%s",VLC->creatures()->getById(getCreature())->getNamePluralTranslated()); ynd.text.appendRawString(tmp); cb->showBlockingDialog(&ynd); break; } } +} +CreatureID CGCreature::getCreature() const +{ + return CreatureID(getObjTypeIndex().getNum()); +} + +void CGCreature::pickRandomObject(CRandomGenerator & rand) +{ + switch(ID.toEnum()) + { + case MapObjectID::RANDOM_MONSTER: + subID = VLC->creh->pickRandomMonster(rand); + break; + case MapObjectID::RANDOM_MONSTER_L1: + subID = VLC->creh->pickRandomMonster(rand, 1); + break; + case MapObjectID::RANDOM_MONSTER_L2: + subID = VLC->creh->pickRandomMonster(rand, 2); + break; + case MapObjectID::RANDOM_MONSTER_L3: + subID = VLC->creh->pickRandomMonster(rand, 3); + break; + case MapObjectID::RANDOM_MONSTER_L4: + subID = VLC->creh->pickRandomMonster(rand, 4); + break; + case MapObjectID::RANDOM_MONSTER_L5: + subID = VLC->creh->pickRandomMonster(rand, 5); + break; + case MapObjectID::RANDOM_MONSTER_L6: + subID = VLC->creh->pickRandomMonster(rand, 6); + break; + case MapObjectID::RANDOM_MONSTER_L7: + subID = VLC->creh->pickRandomMonster(rand, 7); + break; + } + ID = MapObjectID::MONSTER; + setType(ID, subID); } void CGCreature::initObj(CRandomGenerator & rand) @@ -177,16 +242,16 @@ void CGCreature::initObj(CRandomGenerator & rand) break; } - stacks[SlotID(0)]->setType(CreatureID(subID)); + stacks[SlotID(0)]->setType(getCreature()); TQuantity &amount = stacks[SlotID(0)]->count; - CCreature &c = *VLC->creh->objects[subID]; + const Creature * c = VLC->creatures()->getById(getCreature()); if(amount == 0) { - amount = rand.nextInt(c.ammMin, c.ammMax); + amount = rand.nextInt(c->getAdvMapAmountMin(), c->getAdvMapAmountMax()); if(amount == 0) //armies with 0 creatures are illegal { - logGlobal->warn("Stack %s cannot have 0 creatures. Check properties of %s", nodeName(), c.nodeName()); + logGlobal->warn("Stack cannot have 0 creatures. Check properties of %s", c->getJsonKey()); amount = 1; } } @@ -202,31 +267,28 @@ void CGCreature::newTurn(CRandomGenerator & rand) const if (stacks.begin()->second->count < VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1) { ui32 power = static_cast(temppower * (100 + VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100); - cb->setObjProperty(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount - cb->setObjProperty(id, ObjProperty::MONSTER_POWER, power); //increase temppower + cb->setObjPropertyValue(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount + cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, power); //increase temppower } } if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - cb->setObjProperty(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose + cb->setObjPropertyValue(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose } -void CGCreature::setPropertyDer(ui8 what, ui32 val) +void CGCreature::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::MONSTER_COUNT: - stacks[SlotID(0)]->count = val; + stacks[SlotID(0)]->count = identifier.getNum(); break; case ObjProperty::MONSTER_POWER: - temppower = val; + temppower = identifier.getNum(); break; case ObjProperty::MONSTER_EXP: - giveStackExp(val); - break; - case ObjProperty::MONSTER_RESTORE_TYPE: - formation.basicType = val; + giveStackExp(identifier.getNum()); break; case ObjProperty::MONSTER_REFUSED_JOIN: - refusedJoining = val; + refusedJoining = identifier.getNum(); break; } } @@ -252,11 +314,11 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const powerFactor = -3; std::set myKindCres; //what creatures are the same kind as we - const CCreature * myCreature = VLC->creh->objects[subID]; + const CCreature * myCreature = getCreature().toCreature(); myKindCres.insert(myCreature->getId()); //we myKindCres.insert(myCreature->upgrades.begin(), myCreature->upgrades.end()); //our upgrades - for(ConstTransitivePtr &crea : VLC->creh->objects) + for(auto const & crea : VLC->creh->objects) { if(vstd::contains(crea->upgrades, myCreature->getId())) //it's our base creatures myKindCres.insert(crea->getId()); @@ -290,7 +352,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const return JOIN_FOR_FREE; else if(diplomacy * 2 + sympathy + 1 >= character) - return VLC->creatures()->getByIndex(subID)->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold + return VLC->creatures()->getById(getCreature())->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold } //we are still here - creatures have not joined hero, flee or fight @@ -304,7 +366,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const void CGCreature::fleeDecision(const CGHeroInstance *h, ui32 pursue) const { if(refusedJoining) - cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, false); + cb->setObjPropertyValue(id, ObjProperty::MONSTER_REFUSED_JOIN, false); if(pursue) { @@ -322,7 +384,7 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co { if(takenAction(h,false) == FLEE) { - cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, true); + cb->setObjPropertyValue(id, ObjProperty::MONSTER_REFUSED_JOIN, true); flee(h); } else //they fight @@ -357,10 +419,6 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co void CGCreature::fight( const CGHeroInstance *h ) const { //split stacks - //TODO: multiple creature types in a stack? - int basicType = stacks.begin()->second->type->getId(); - cb->setObjProperty(id, ObjProperty::MONSTER_RESTORE_TYPE, basicType); //store info about creature stack - int stacksCount = getNumberOfStacks(h); //source: http://heroescommunity.com/viewthread.php3?TID=27539&PID=1266335#focus @@ -390,7 +448,7 @@ void CGCreature::fight( const CGHeroInstance *h ) const if(!upgrades.empty()) { auto it = RandomGeneratorUtil::nextItem(upgrades, CRandomGenerator::getDefault()); - cb->changeStackType(StackLocation(this, slotID), VLC->creh->objects[*it]); + cb->changeStackType(StackLocation(this, slotID), it->toCreature()); } } } @@ -404,7 +462,7 @@ void CGCreature::flee( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT,91); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID); + ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); cb->showBlockingDialog(&ynd); } @@ -424,7 +482,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & { //merge stacks into one TSlots::const_iterator i; - CCreature * cre = VLC->creh->objects[formation.basicType]; + const CCreature * cre = getCreature().toCreature(); for(i = stacks.begin(); i != stacks.end(); i++) { if(cre->isMyUpgrade(i->second->type)) @@ -449,7 +507,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & cb->moveStack(StackLocation(this, i->first), StackLocation(this, slot), i->second->count); } - cb->setObjProperty(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties + cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties } } @@ -526,17 +584,17 @@ void CGCreature::giveReward(const CGHeroInstance * h) const if(!resources.empty()) { cb->giveResources(h->tempOwner, resources); - for(int i = 0; i < resources.size(); i++) + for(auto const & res : GameResID::ALL_RESOURCES()) { - if(resources[i] > 0) - iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0); + if(resources[res] > 0) + iw.components.emplace_back(ComponentType::RESOURCE, res, resources[res]); } } if(gainedArtifact != ArtifactID::NONE) { - cb->giveHeroNewArtifact(h, VLC->arth->objects[gainedArtifact], ArtifactPosition::FIRST_AVAILABLE); - iw.components.emplace_back(Component::EComponentType::ARTIFACT, gainedArtifact, 0, 0); + cb->giveHeroNewArtifact(h, gainedArtifact.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + iw.components.emplace_back(ComponentType::ARTIFACT, gainedArtifact); } if(!iw.components.empty()) diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 385b96cb5..d81e1bd8b 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -18,6 +18,8 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CGCreature : public CArmedInstance //creatures on map { public: + using CArmedInstance::CArmedInstance; + enum Action { FIGHT = -2, FLEE = -1, JOIN_FOR_FREE = 0 //values > 0 mean gold price }; @@ -40,27 +42,21 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void newTurn(CRandomGenerator & rand) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + CreatureID getCreature() const; //stack formation depends on position, bool containsUpgradedStack() const; int getNumberOfStacks(const CGHeroInstance *hero) const; - struct DLL_LINKAGE formationInfo // info about merging stacks after battle back into one - { - si32 basicType; - ui8 upgrade; //random seed used to determine number of stacks and is there's upgraded stack - template void serialize(Handler &h, const int version) - { - h & basicType; - h & upgrade; - } - } formation; - - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & identifier; @@ -75,7 +71,7 @@ public: h & formation; } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; private: diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 65da8db66..dafcc8819 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -11,9 +11,12 @@ #include "StdInc.h" #include "CGDwelling.h" #include "../serializer/JsonSerializeFormat.h" +#include "../mapping/CMap.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" #include "../networkPacks/StackLocation.h" #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" @@ -26,41 +29,10 @@ VCMI_LIB_NAMESPACE_BEGIN -CSpecObjInfo::CSpecObjInfo(): - owner(nullptr) -{ - -} - -void CCreGenAsCastleInfo::serializeJson(JsonSerializeFormat & handler) +void CGDwellingRandomizationInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("sameAsTown", instanceId); - - if(!handler.saving) - { - asCastle = !instanceId.empty(); - allowedFactions.clear(); - } - - if(!asCastle) - { - std::vector standard; - standard.resize(VLC->townh->size(), true); - - JsonSerializeFormat::LIC allowedLIC(standard, &FactionID::decode, &FactionID::encode); - allowedLIC.any = allowedFactions; - - handler.serializeLIC("allowedFactions", allowedLIC); - - if(!handler.saving) - { - allowedFactions = allowedLIC.any; - } - } -} - -void CCreGenLeveledInfo::serializeJson(JsonSerializeFormat & handler) -{ + handler.serializeIdArray("allowedFactions", allowedFactions); handler.serializeInt("minLevel", minLevel, static_cast(1)); handler.serializeInt("maxLevel", maxLevel, static_cast(7)); @@ -72,30 +44,142 @@ void CCreGenLeveledInfo::serializeJson(JsonSerializeFormat & handler) } } -void CCreGenLeveledCastleInfo::serializeJson(JsonSerializeFormat & handler) +CGDwelling::CGDwelling(IGameCallback *cb): + CArmedInstance(cb) +{} + +CGDwelling::~CGDwelling() = default; + +FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) { - CCreGenAsCastleInfo::serializeJson(handler); - CCreGenLeveledInfo::serializeJson(handler); + if (ID == Obj::RANDOM_DWELLING_FACTION) + return FactionID(subID.getNum()); + + assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL); + assert(randomizationInfo.has_value()); + if (!randomizationInfo) + return FactionID::CASTLE; + + CGTownInstance * linkedTown = nullptr; + + if (!randomizationInfo->instanceId.empty()) + { + auto iter = cb->gameState()->map->instanceNames.find(randomizationInfo->instanceId); + + if(iter == cb->gameState()->map->instanceNames.end()) + logGlobal->error("Map object not found: %s", randomizationInfo->instanceId); + linkedTown = dynamic_cast(iter->second.get()); + } + + if (randomizationInfo->identifier != 0) + { + for(auto & elem : cb->gameState()->map->objects) + { + auto town = dynamic_cast(elem.get()); + if(town && town->identifier == randomizationInfo->identifier) + { + linkedTown = town; + break; + } + } + } + + if (linkedTown) + { + if(linkedTown->ID==Obj::RANDOM_TOWN) + linkedTown->pickRandomObject(rand); //we have to randomize the castle first + + assert(linkedTown->ID == Obj::TOWN); + if(linkedTown->ID==Obj::TOWN) + return linkedTown->getFaction(); + } + + if(!randomizationInfo->allowedFactions.empty()) + return *RandomGeneratorUtil::nextItem(randomizationInfo->allowedFactions, rand); + + + std::vector potentialPicks; + + for (FactionID faction(0); faction < FactionID(VLC->townh->size()); ++faction) + if (VLC->factions()->getById(faction)->hasTown()) + potentialPicks.push_back(faction); + + assert(!potentialPicks.empty()); + return *RandomGeneratorUtil::nextItem(potentialPicks, rand); } -CGDwelling::CGDwelling() - : info(nullptr) +int CGDwelling::randomizeLevel(CRandomGenerator & rand) { + if (ID == Obj::RANDOM_DWELLING_LVL) + return subID.getNum(); + + assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_FACTION); + assert(randomizationInfo.has_value()); + + if (!randomizationInfo) + return rand.nextInt(1, 7) - 1; + + if(randomizationInfo->minLevel == randomizationInfo->maxLevel) + return randomizationInfo->minLevel - 1; + + return rand.nextInt(randomizationInfo->minLevel, randomizationInfo->maxLevel) - 1; } -CGDwelling::~CGDwelling() +void CGDwelling::pickRandomObject(CRandomGenerator & rand) { - vstd::clear_pointer(info); + if (ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL || ID == Obj::RANDOM_DWELLING_FACTION) + { + FactionID faction = randomizeFaction(rand); + int level = randomizeLevel(rand); + assert(faction != FactionID::NONE && faction != FactionID::NEUTRAL); + assert(level >= 0 && level <= 6); + randomizationInfo.reset(); + + CreatureID cid = (*VLC->townh)[faction]->town->creatures[level][0]; + + //NOTE: this will pick last dwelling with this creature (Mantis #900) + //check for block map equality is better but more complex solution + auto testID = [&](const Obj & primaryID) -> MapObjectSubID + { + auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID); + for (MapObjectSubID entry : dwellingIDs) + { + const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); + + if (handler->producesCreature(cid.toCreature())) + return MapObjectSubID(entry); + } + return MapObjectSubID(); + }; + + ID = Obj::CREATURE_GENERATOR1; + subID = testID(Obj::CREATURE_GENERATOR1); + + if (subID == MapObjectSubID()) + { + ID = Obj::CREATURE_GENERATOR4; + subID = testID(Obj::CREATURE_GENERATOR4); + } + + if (subID == MapObjectSubID()) + { + logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level)); + ID = Obj::CREATURE_GENERATOR1; + subID = *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), rand); + } + + setType(ID, subID); + } } void CGDwelling::initObj(CRandomGenerator & rand) { - switch(ID) + switch(ID.toEnum()) { case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR4: { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); if (getOwner() != PlayerColor::NEUTRAL) cb->gameState()->players[getOwner()].dwellings.emplace_back(this); @@ -121,24 +205,7 @@ void CGDwelling::initObj(CRandomGenerator & rand) } } -void CGDwelling::initRandomObjectInfo() -{ - vstd::clear_pointer(info); - switch(ID) - { - case Obj::RANDOM_DWELLING: info = new CCreGenLeveledCastleInfo(); - break; - case Obj::RANDOM_DWELLING_LVL: info = new CCreGenAsCastleInfo(); - break; - case Obj::RANDOM_DWELLING_FACTION: info = new CCreGenLeveledInfo(); - break; - } - - if(info) - info->owner = this; -} - -void CGDwelling::setPropertyDer(ui8 what, ui32 val) +void CGDwelling::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { @@ -151,14 +218,14 @@ void CGDwelling::setPropertyDer(ui8 what, ui32 val) std::vector >* dwellings = &cb->gameState()->players[tempOwner].dwellings; dwellings->erase (std::find(dwellings->begin(), dwellings->end(), this)); } - if (PlayerColor(val) != PlayerColor::NEUTRAL) //can new owner be neutral? - cb->gameState()->players[PlayerColor(val)].dwellings.emplace_back(this); + if (identifier.as().isValidPlayer()) + cb->gameState()->players[identifier.as()].dwellings.emplace_back(this); } break; case ObjProperty::AVAILABLE_CREATURE: creatures.resize(1); creatures[0].second.resize(1); - creatures[0].second[0] = CreatureID(val); + creatures[0].second[0] = identifier.as(); break; } } @@ -171,7 +238,7 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::ADVOB_TXT, 44); //{%s} \n\n The camp is deserted. Perhaps you should try next week. - iw.text.replaceLocalString(EMetaText::OBJ_NAMES, ID); + iw.text.replaceName(ID); cb->sendAndApply(&iw); return; } @@ -186,12 +253,12 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog bd(true,false); bd.player = h->tempOwner; bd.text.appendLocalString(EMetaText::GENERAL_TXT, 421); //Much to your dismay, the %s is guarded by %s %s. Do you wish to fight the guards? - bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) bd.text.replaceRawString(CCreature::getQuantityRangeStringForId(Slots().begin()->second->getQuantityID())); else bd.text.replaceLocalString(EMetaText::ARRAY_TXT, 173 + (int)Slots().begin()->second->getQuantityID()*3); - bd.text.replaceCreatureName(*Slots().begin()->second); + bd.text.replaceName(*Slots().begin()->second); cb->showBlockingDialog(&bd); return; } @@ -207,16 +274,16 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const if(ID == Obj::CREATURE_GENERATOR1 || ID == Obj::CREATURE_GENERATOR4) { bd.text.appendLocalString(EMetaText::ADVOB_TXT, ID == Obj::CREATURE_GENERATOR1 ? 35 : 36); //{%s} Would you like to recruit %s? / {%s} Would you like to recruit %s, %s, %s, or %s? - bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); for(const auto & elem : creatures) - bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]); + bd.text.replaceNamePlural(elem.second[0]); } else if(ID == Obj::REFUGEE_CAMP) { bd.text.appendLocalString(EMetaText::ADVOB_TXT, 35); //{%s} Would you like to recruit %s? - bd.text.replaceLocalString(EMetaText::OBJ_NAMES, ID); + bd.text.replaceName(ID); for(const auto & elem : creatures) - bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]); + bd.text.replaceNamePlural(elem.second[0]); } else if(ID == Obj::WAR_MACHINE_FACTORY) bd.text.appendLocalString(EMetaText::ADVOB_TXT, 157); //{War Machine Factory} Would you like to purchase War Machines? @@ -237,7 +304,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const if(ID == Obj::REFUGEE_CAMP) //if it's a refugee camp, we need to pick an available creature { - cb->setObjProperty(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster(rand)); + cb->setObjPropertyID(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster(rand)); } bool change = false; @@ -256,8 +323,8 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const else creaturesAccumulate = VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL); - CCreature *cre = VLC->creh->objects[creatures[i].second[0]]; - TQuantity amount = cre->getGrowth() * (1 + cre->valOfBonuses(BonusType::CREATURE_GROWTH_PERCENT)/100) + cre->valOfBonuses(BonusType::CREATURE_GROWTH); + const CCreature * cre =creatures[i].second[0].toCreature(); + TQuantity amount = cre->getGrowth() * (1 + cre->valOfBonuses(BonusType::CREATURE_GROWTH_PERCENT)/100) + cre->valOfBonuses(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(cre->getLevel())); if (creaturesAccumulate && ID != Obj::REFUGEE_CAMP) //camp should not try to accumulate different kinds of creatures sac.creatures[i].first += amount; else @@ -272,6 +339,30 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const updateGuards(); } +std::vector CGDwelling::getPopupComponents(PlayerColor player) const +{ + if (getOwner() != player) + return {}; + + std::vector result; + + if (ID == Obj::CREATURE_GENERATOR1 && !creatures.empty()) + { + for (auto const & creature : creatures.front().second) + result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first); + } + + if (ID == Obj::CREATURE_GENERATOR4) + { + for (auto const & creatureLevel : creatures) + { + if (!creatureLevel.second.empty()) + result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first); + } + } + return result; +} + void CGDwelling::updateGuards() const { //TODO: store custom guard config and use it @@ -281,7 +372,7 @@ void CGDwelling::updateGuards() const //default condition - creatures are of level 5 or higher for (auto creatureEntry : creatures) { - if (VLC->creatures()->getByIndex(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP) + if (VLC->creatures()->getById(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP) { guarded = true; break; @@ -292,7 +383,7 @@ void CGDwelling::updateGuards() const { for (auto creatureEntry : creatures) { - const CCreature * crea = VLC->creh->objects[creatureEntry.second.at(0)]; + const CCreature * crea = creatureEntry.second.at(0).toCreature(); SlotID slot = getSlotFor(crea->getId()); if (hasStackAtSlot(slot)) //stack already exists, overwrite it @@ -349,7 +440,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 425);//The %s would join your hero, but there aren't enough provisions to support them. - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); cb->showInfoDialog(&iw); } else //give creatures @@ -365,7 +456,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 423); //%d %s join your army. iw.text.replaceNumber(count); - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); cb->showInfoDialog(&iw); cb->sendAndApply(&sac); @@ -377,7 +468,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 422); //There are no %s here to recruit. - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); iw.player = h->tempOwner; cb->sendAndApply(&iw); } @@ -425,10 +516,7 @@ void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) { - if(!handler.saving) - initRandomObjectInfo(); - - switch (ID) + switch (ID.toEnum()) { case Obj::WAR_MACHINE_FACTORY: case Obj::REFUGEE_CAMP: @@ -437,8 +525,10 @@ void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) case Obj::RANDOM_DWELLING: case Obj::RANDOM_DWELLING_LVL: case Obj::RANDOM_DWELLING_FACTION: - info->serializeJson(handler); - //fall through + if (!handler.saving) + randomizationInfo = CGDwellingRandomizationInfo(); + randomizationInfo->serializeJson(handler); + [[fallthrough]]; default: serializeJsonOwner(handler); break; diff --git a/lib/mapObjects/CGDwelling.h b/lib/mapObjects/CGDwelling.h index f27cf44e2..08d592227 100644 --- a/lib/mapObjects/CGDwelling.h +++ b/lib/mapObjects/CGDwelling.h @@ -16,74 +16,52 @@ VCMI_LIB_NAMESPACE_BEGIN class CGDwelling; -class DLL_LINKAGE CSpecObjInfo +class DLL_LINKAGE CGDwellingRandomizationInfo { public: - CSpecObjInfo(); - virtual ~CSpecObjInfo() = default; - - virtual void serializeJson(JsonSerializeFormat & handler) = 0; - - const CGDwelling * owner; -}; - -class DLL_LINKAGE CCreGenAsCastleInfo : public virtual CSpecObjInfo -{ -public: - bool asCastle = false; - ui32 identifier = 0;//h3m internal identifier - - std::vector allowedFactions; + std::set allowedFactions; std::string instanceId;//vcmi map instance identifier - void serializeJson(JsonSerializeFormat & handler) override; + int32_t identifier = 0;//h3m internal identifier + + uint8_t minLevel = 1; + uint8_t maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7> + + void serializeJson(JsonSerializeFormat & handler); }; -class DLL_LINKAGE CCreGenLeveledInfo : public virtual CSpecObjInfo -{ -public: - ui8 minLevel = 1; - ui8 maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7> - - void serializeJson(JsonSerializeFormat & handler) override; -}; - -class DLL_LINKAGE CCreGenLeveledCastleInfo : public CCreGenAsCastleInfo, public CCreGenLeveledInfo -{ -public: - CCreGenLeveledCastleInfo() = default; - void serializeJson(JsonSerializeFormat & handler) override; -}; - - class DLL_LINKAGE CGDwelling : public CArmedInstance { public: typedef std::vector > > TCreaturesSet; - CSpecObjInfo * info; //random dwelling options; not serialized + std::optional randomizationInfo; //random dwelling options; not serialized TCreaturesSet creatures; //creatures[level] -> - CGDwelling(); + CGDwelling(IGameCallback *cb); ~CGDwelling() override; - void initRandomObjectInfo(); protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; private: + FactionID randomizeFaction(CRandomGenerator & rand); + int randomizeLevel(CRandomGenerator & rand); + + void pickRandomObject(CRandomGenerator & rand) override; void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void newTurn(CRandomGenerator & rand) const override; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + std::vector getPopupComponents(PlayerColor player) const override; void updateGuards() const; void heroAcceptsCreatures(const CGHeroInstance *h) const; public: - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & creatures; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 116d16017..be24c0258 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "../CGeneralTextHandler.h" #include "../ArtifactUtils.h" @@ -28,11 +29,15 @@ #include "../CCreatureHandler.h" #include "../CTownHandler.h" #include "../mapping/CMap.h" +#include "../StartInfo.h" #include "CGTownInstance.h" +#include "../campaign/CampaignState.h" +#include "../json/JsonBonus.h" #include "../pathfinder/TurnInfo.h" #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/MiscObjects.h" #include "../modding/ModScope.h" #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" @@ -150,6 +155,11 @@ bool CGHeroInstance::isCoastVisitable() const return true; } +bool CGHeroInstance::isBlockedVisitable() const +{ + return true; +} + BattleField CGHeroInstance::getBattlefield() const { return BattleField::NONE; @@ -212,13 +222,16 @@ bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const if ( !canLearnSkill()) return false; - if (!cb->isAllowed(2, which)) + if (!cb->isAllowed(which)) return false; if (getSecSkillLevel(which) > 0) return false; - if (type->heroClass->secSkillProbability[which] == 0) + if (type->heroClass->secSkillProbability.count(which) == 0) + return false; + + if (type->heroClass->secSkillProbability.at(which) == 0) return false; return true; @@ -231,7 +244,10 @@ int CGHeroInstance::movementPointsRemaining() const void CGHeroInstance::setMovementPoints(int points) { - movement = std::max(0, points); + if(getBonusBearer()->hasBonusOfType(BonusType::UNLIMITED_MOVEMENT)) + movement = 1000000; + else + movement = std::max(0, points); } int CGHeroInstance::movementPointsLimit(bool onLand) const @@ -263,7 +279,9 @@ int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea); } -CGHeroInstance::CGHeroInstance(): +CGHeroInstance::CGHeroInstance(IGameCallback * cb) + : CArmedInstance(cb), + type(nullptr), tacticFormationEnabled(false), inTownGarrison(false), moveDir(4), @@ -277,7 +295,6 @@ CGHeroInstance::CGHeroInstance(): setNodeType(HERO); ID = Obj::HERO; secSkills.emplace_back(SecondarySkill::NONE, -1); - blockVisit = true; } PlayerColor CGHeroInstance::getOwner() const @@ -285,27 +302,28 @@ PlayerColor CGHeroInstance::getOwner() const return tempOwner; } +HeroTypeID CGHeroInstance::getHeroType() const +{ + return HeroTypeID(getObjTypeIndex().getNum()); +} + +void CGHeroInstance::setHeroType(HeroTypeID heroType) +{ + assert(type == nullptr); + subID = heroType; +} + void CGHeroInstance::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID) { subID = SUBID.getNum(); initHero(rand); } -void CGHeroInstance::setType(si32 ID, si32 subID) -{ - assert(ID == Obj::HERO); // just in case - type = VLC->heroh->objects[subID]; - - CGObjectInstance::setType(ID, type->heroClass->getIndex()); // to find object handler we must use heroClass->id - this->subID = subID; // after setType subID used to store unique hero identify id. Check issue 2277 for details - randomizeArmy(type->heroClass->faction); -} - void CGHeroInstance::initHero(CRandomGenerator & rand) { assert(validTypes(true)); if(!type) - type = VLC->heroh->objects[subID]; + type = getHeroType().toHeroType(); if (ID == Obj::HERO) appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); @@ -323,13 +341,19 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { // hero starts with default spellbook presence status if(!getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) - putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK)); + { + auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK); + putArtifact(ArtifactPosition::SPELLBOOK, artifact); + } } else spells -= SpellID::SPELLBOOK_PRESET; if(!getArt(ArtifactPosition::MACH4)) - putArtifact(ArtifactPosition::MACH4, ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT)); //everyone has a catapult + { + auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT); + putArtifact(ArtifactPosition::MACH4, artifact); //everyone has a catapult + } if(!hasBonus(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))) { @@ -344,7 +368,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) if (gender == EHeroGender::DEFAULT) gender = type->gender; - setFormation(false); + setFormation(EArmyFormation::LOOSE); if (!stacksCount()) //standard army//initial army { initArmy(rand); @@ -385,9 +409,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) commander->giveStackExp (exp); //after our exp is set } - skillsInfo.rand.setSeed(rand.nextInt()); - skillsInfo.resetMagicSchoolCounter(); - skillsInfo.resetWisdomCounter(); + skillsInfo = SecondarySkillsInfo(); //copy active (probably growing) bonuses from hero prototype to hero object for(const std::shared_ptr & b : type->specialty) @@ -421,14 +443,14 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) int count = rand.nextInt(stack.minAmount, stack.maxAmount); - const CCreature * creature = stack.creature.toCreature(); - - if(creature == nullptr) + if(stack.creature == CreatureID::NONE) { - logGlobal->error("Hero %s has invalid creature with id %d in initial army", getNameTranslated(), stack.creature.toEnum()); + logGlobal->error("Hero %s has invalid creature in initial army", getNameTranslated()); continue; } + const CCreature * creature = stack.creature.toCreature(); + if(creature->warMachine != ArtifactID::NONE) //war machine { warMachinesGiven++; @@ -444,7 +466,10 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) ArtifactPosition slot = art->getPossibleSlots().at(ArtBearer::HERO).front(); if(!getArt(slot)) - putArtifact(slot, ArtifactUtils::createNewArtifactInstance(aid)); + { + auto artifact = ArtifactUtils::createNewArtifactInstance(aid); + putArtifact(slot, artifact); + } else logGlobal->warn("Hero %s already has artifact at %d, omitting giving artifact %d", getNameTranslated(), slot.toEnum(), aid.toEnum()); } @@ -497,7 +522,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const SetMovePoints smp; smp.hid = id; - cb->setManaPoints (id, manaLimit()); + cb->setManaPoints (id, manaLimit()); ObjectInstanceID boatId; const auto boatPos = visitablePos(); @@ -516,7 +541,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const smp.val = movementPointsLimit(true); } cb->giveHero(id, h->tempOwner, boatId); //recreates def and adds hero to player - cb->setObjProperty(id, ObjProperty::ID, Obj::HERO); //set ID to 34 AFTER hero gets correct flag color + cb->setObjPropertyID(id, ObjProperty::ID, Obj(Obj::HERO)); //set ID to 34 AFTER hero gets correct flag color cb->setMovePoints (&smp); h->showInfoDialog(102); @@ -534,7 +559,7 @@ std::string CGHeroInstance::getObjectName() const { std::string hoverName = VLC->generaltexth->allTexts[15]; boost::algorithm::replace_first(hoverName,"%s",getNameTranslated()); - boost::algorithm::replace_first(hoverName,"%s", type->heroClass->getNameTranslated()); + boost::algorithm::replace_first(hoverName,"%s", getClassNameTranslated()); return hoverName; } else @@ -553,31 +578,46 @@ ui8 CGHeroInstance::maxlevelsToWisdom() const CGHeroInstance::SecondarySkillsInfo::SecondarySkillsInfo(): magicSchoolCounter(1), wisdomCounter(1) -{ - rand.setSeed(0); -} +{} void CGHeroInstance::SecondarySkillsInfo::resetMagicSchoolCounter() { - magicSchoolCounter = 1; + magicSchoolCounter = 0; } void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter() { - wisdomCounter = 1; + wisdomCounter = 0; +} + +void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == Obj::HERO || ID == Obj::PRISON || ID == Obj::RANDOM_HERO); + + if (ID == Obj::RANDOM_HERO) + { + ID = Obj::HERO; + subID = cb->gameState()->pickNextHeroType(getOwner()); + type = getHeroType().toHeroType(); + randomizeArmy(type->heroClass->faction); + } + else + type = getHeroType().toHeroType(); + + auto oldSubID = subID; + + // to find object handler we must use heroClass->id + // after setType subID used to store unique hero identify id. Check issue 2277 for details + if (ID != Obj::PRISON) + setType(ID, type->heroClass->getIndex()); + else + setType(ID, 0); + + this->subID = oldSubID; } void CGHeroInstance::initObj(CRandomGenerator & rand) { - if(!type) - initHero(rand); //TODO: set up everything for prison before specialties are configured - if (ID != Obj::PRISON) - { - auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); - auto customApp = VLC->objtypeh->getHandlerFor(ID, type->heroClass->getIndex())->getOverride(terrain, this); - if (customApp) - appearance = customApp; - } } void CGHeroInstance::recreateSecondarySkillsBonuses() @@ -599,10 +639,10 @@ void CGHeroInstance::updateSkillBonus(const SecondarySkill & which, int val) addNewBonus(std::make_shared(*b)); } -void CGHeroInstance::setPropertyDer( ui8 what, ui32 val ) +void CGHeroInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { if(what == ObjProperty::PRIMARY_STACK_COUNT) - setStackCount(SlotID(0), val); + setStackCount(SlotID(0), identifier.getNum()); } double CGHeroInstance::getFightingStrength() const @@ -636,7 +676,7 @@ int32_t CGHeroInstance::getCasterUnitId() const return id.getNum(); } -int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const +int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const { int32_t skill = -1; //skill level @@ -731,7 +771,7 @@ void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std:: text.appendLocalString(EMetaText::GENERAL_TXT, textIndex); getCasterName(text); - text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex()); + text.replaceName(spell->getId()); if(singleTarget) attacked.at(0)->addNameReplacement(text, true); } @@ -756,7 +796,7 @@ void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) con bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const { - const bool isAllowed = IObjectInterface::cb->isAllowed(0, spell->getIndex()); + const bool isAllowed = cb->isAllowed(spell->getId()); const bool inSpellBook = vstd::contains(spells, spell->getId()) && hasSpellbook(); const bool specificBonus = hasBonusOfType(BonusType::SPELL, BonusSubtypeID(spell->getId())); @@ -820,7 +860,7 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned return false;//creature abilities can not be learned } - if(!allowBanned && !IObjectInterface::cb->isAllowed(0, spell->getIndex())) + if(!allowBanned && !cb->isAllowed(spell->getId())) { logGlobal->warn("Hero %s try to learn banned spell %s", nodeName(), spell->getNameTranslated()); return false;//banned spells should not be learned @@ -846,7 +886,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b double necromancySkill = valOfBonuses(BonusType::UNDEAD_RAISE_PERCENTAGE) / 100.0; const ui8 necromancyLevel = valOfBonuses(BonusType::IMPROVED_NECROMANCY); vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all... - const std::map &casualties = battleResult.casualties[!battleResult.winner]; + const std::map &casualties = battleResult.casualties[!battleResult.winner]; // figure out what to raise - pick strongest creature meeting requirements CreatureID creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode int requiredCasualtyLevel = 1; @@ -855,7 +895,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b { int maxCasualtyLevel = 1; for(const auto & casualty : casualties) - vstd::amax(maxCasualtyLevel, VLC->creatures()->getByIndex(casualty.first)->getLevel()); + vstd::amax(maxCasualtyLevel, VLC->creatures()->getById(casualty.first)->getLevel()); // pick best bonus available std::shared_ptr topPick; for(const std::shared_ptr & newPick : *improvedNecromancy) @@ -888,7 +928,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b // raise upgraded creature (at 2/3 rate) if no space available otherwise if(getSlotFor(creatureTypeRaised) == SlotID()) { - for(const CreatureID & upgraded : VLC->creh->objects[creatureTypeRaised]->upgrades) + for(const CreatureID & upgraded : creatureTypeRaised.toCreature()->upgrades) { if(getSlotFor(upgraded) != SlotID()) { @@ -899,11 +939,11 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b } } // calculate number of creatures raised - low level units contribute at 50% rate - const double raisedUnitHealth = VLC->creh->objects[creatureTypeRaised]->getMaxHealth(); + const double raisedUnitHealth = creatureTypeRaised.toCreature()->getMaxHealth(); double raisedUnits = 0; for(const auto & casualty : casualties) { - const CCreature * c = VLC->creh->objects[casualty.first]; + const CCreature * c = casualty.first.toCreature(); double raisedFromCasualty = std::min(c->getMaxHealth() / raisedUnitHealth, 1.0) * casualty.second * necromancySkill; if(c->getLevel() < requiredCasualtyLevel) raisedFromCasualty *= 0.5; @@ -926,7 +966,7 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta iw.type = EInfoWindowMode::AUTO; iw.soundID = soundBase::pickup01 + rand.nextInt(6); iw.player = tempOwner; - iw.components.emplace_back(raisedStack); + iw.components.emplace_back(ComponentType::CREATURE, raisedStack.getId(), raisedStack.count); if (raisedStack.count > 1) // Practicing the dark arts of necromancy, ... (plural) { @@ -937,7 +977,7 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta { iw.text.appendLocalString(EMetaText::GENERAL_TXT, 146); } - iw.text.replaceCreatureName(raisedStack); + iw.text.replaceName(raisedStack); cb->showInfoDialog(&iw); } @@ -1050,7 +1090,7 @@ HeroTypeID CGHeroInstance::getPortraitSource() const if (customPortraitSource.isValid()) return customPortraitSource; else - return HeroTypeID(subID); + return getHeroType(); } int32_t CGHeroInstance::getIconIndex() const @@ -1063,6 +1103,18 @@ std::string CGHeroInstance::getNameTranslated() const return VLC->generaltexth->translate(getNameTextID()); } +std::string CGHeroInstance::getClassNameTranslated() const +{ + return VLC->generaltexth->translate(getClassNameTextID()); +} + +std::string CGHeroInstance::getClassNameTextID() const +{ + if (isCampaignGem()) + return "core.genrltxt.735"; + return type->heroClass->getNameTextID(); +} + std::string CGHeroInstance::getNameTextID() const { if (!nameCustomTextId.empty()) @@ -1092,7 +1144,7 @@ std::string CGHeroInstance::getBiographyTextID() const CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) { - assert(art->artType->canBePutAt(this, pos)); + assert(art->canBePutAt(this, pos)); if(ArtifactUtils::isSlotEquipment(pos)) attachTo(*art); @@ -1135,7 +1187,7 @@ void CGHeroInstance::removeSpellbook() if(hasSpellbook()) { - ArtifactLocation(this, ArtifactPosition(ArtifactPosition::SPELLBOOK)).removeArtifact(); + getArt(ArtifactPosition::SPELLBOOK)->removeFrom(*this, ArtifactPosition::SPELLBOOK); } } @@ -1225,7 +1277,7 @@ EDiggingStatus CGHeroInstance::diggingStatus() const { if(static_cast(movement) < movementPointsLimit(true)) return EDiggingStatus::LACK_OF_MOVEMENT; - if(!VLC->arth->objects[ArtifactID::GRAIL]->canBePutAt(this)) + if(!ArtifactID(ArtifactID::GRAIL).toArtifact()->canBePutAt(this)) return EDiggingStatus::BACKPACK_IS_FULL; return cb->getTileDigStatus(visitablePos()); } @@ -1235,49 +1287,31 @@ ArtBearer::ArtBearer CGHeroInstance::bearerType() const return ArtBearer::HERO; } -std::vector CGHeroInstance::getLevelUpProposedSecondarySkills() const +std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const { - std::vector obligatorySkills; //hero is offered magic school or wisdom if possible - auto getObligatorySkills = [](CSkill::Obligatory obl){ - std::vector obligatory = {}; + std::set obligatory; for(auto i = 0; i < VLC->skillh->size(); i++) if((*VLC->skillh)[SecondarySkill(i)]->obligatory(obl)) - obligatory.emplace_back(i); //Always return all obligatory skills + obligatory.insert(i); //Always return all obligatory skills return obligatory; }; - auto selectObligatorySkill = [&](std::vector& ss) -> void + auto intersect = [](const std::set & left, const std::set & right) { - std::shuffle(ss.begin(), ss.end(), skillsInfo.rand.getStdGenerator()); - - for(const auto & skill : ss) - { - if (canLearnSkill(skill)) //only skills hero doesn't know yet - { - obligatorySkills.push_back(skill); - break; //only one - } - } + std::set intersect; + std::set_intersection(left.begin(), left.end(), right.begin(), right.end(), + std::inserter(intersect, intersect.begin())); + return intersect; }; - if (!skillsInfo.wisdomCounter) - { - auto obligatory = getObligatorySkills(CSkill::Obligatory::MAJOR); - selectObligatorySkill(obligatory); - } - if (!skillsInfo.magicSchoolCounter) - { - auto obligatory = getObligatorySkills(CSkill::Obligatory::MINOR); - selectObligatorySkill(obligatory); - } + std::set wisdomList = getObligatorySkills(CSkill::Obligatory::MAJOR); + std::set schoolList = getObligatorySkills(CSkill::Obligatory::MINOR); - std::vector skills; - //picking sec. skills for choice std::set basicAndAdv; - std::set expert; std::set none; + for(int i = 0; i < VLC->skillh->size(); i++) if (canLearnSkill(SecondarySkill(i))) none.insert(SecondarySkill(i)); @@ -1286,86 +1320,66 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills() { if(elem.second < MasteryLevel::EXPERT) basicAndAdv.insert(elem.first); - else - expert.insert(elem.first); none.erase(elem.first); } - for(const auto & s : obligatorySkills) //don't duplicate them - { - none.erase (s); - basicAndAdv.erase (s); - expert.erase (s); - } - //first offered skill: - // 1) give obligatory skill - // 2) give any other new skill - // 3) upgrade existing - if(canLearnSkill() && !obligatorySkills.empty()) - { - skills.push_back (obligatorySkills[0]); - } - else if(!none.empty() && canLearnSkill()) //hero have free skill slot - { - skills.push_back(type->heroClass->chooseSecSkill(none, skillsInfo.rand)); //new skill - none.erase(skills.back()); - } - else if(!basicAndAdv.empty()) - { - skills.push_back(type->heroClass->chooseSecSkill(basicAndAdv, skillsInfo.rand)); //upgrade existing - basicAndAdv.erase(skills.back()); - } + bool wantsWisdom = skillsInfo.wisdomCounter + 1 >= maxlevelsToWisdom(); + bool wantsSchool = skillsInfo.magicSchoolCounter + 1 >= maxlevelsToMagicSchool(); - //second offered skill: - //1) upgrade existing - //2) give obligatory skill - //3) give any other new skill - if(!basicAndAdv.empty()) - { - SecondarySkill s = type->heroClass->chooseSecSkill(basicAndAdv, skillsInfo.rand);//upgrade existing - skills.push_back(s); - basicAndAdv.erase(s); - } - else if (canLearnSkill() && obligatorySkills.size() > 1) - { - skills.push_back (obligatorySkills[1]); - } - else if(!none.empty() && canLearnSkill()) - { - skills.push_back(type->heroClass->chooseSecSkill(none, skillsInfo.rand)); //give new skill - none.erase(skills.back()); - } + std::vector skills; + + auto chooseSkill = [&](std::set & options) + { + bool selectWisdom = wantsWisdom && !intersect(options, wisdomList).empty(); + bool selectSchool = wantsSchool && !intersect(options, schoolList).empty(); + SecondarySkill selection; + + if (selectWisdom) + selection = type->heroClass->chooseSecSkill(intersect(options, wisdomList), rand); + else if (selectSchool) + selection = type->heroClass->chooseSecSkill(intersect(options, schoolList), rand); + else + selection = type->heroClass->chooseSecSkill(options, rand); + + skills.push_back(selection); + options.erase(selection); + + if (wisdomList.count(selection)) + wisdomList.clear(); + + if (schoolList.count(selection)) + schoolList.clear(); + }; + + if (!basicAndAdv.empty()) + chooseSkill(basicAndAdv); + + if (canLearnSkill() && !none.empty()) + chooseSkill(none); + + if (!basicAndAdv.empty() && skills.size() < 2) + chooseSkill(basicAndAdv); + + if (canLearnSkill() && !none.empty() && skills.size() < 2) + chooseSkill(none); - if (skills.size() == 2) // Fix for #1868 to avoid changing logic (possibly causing bugs in process) - std::swap(skills[0], skills[1]); return skills; } PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const { assert(gainsLevel()); - int randomValue = rand.nextInt(99); - int pom = 0; - int primarySkill = 0; const auto isLowLevelHero = level < GameConstants::HERO_HIGH_LEVEL; const auto & skillChances = isLowLevelHero ? type->heroClass->primarySkillLowLevel : type->heroClass->primarySkillHighLevel; - for(; primarySkill < GameConstants::PRIMARY_SKILLS; ++primarySkill) + if (isCampaignYog()) { - pom += skillChances[primarySkill]; - if(randomValue < pom) - { - break; - } + // Yog can only receive Attack or Defence on level-up + std::vector yogChances = { skillChances[0], skillChances[1]}; + return static_cast(RandomGeneratorUtil::nextItemWeighted(yogChances, rand)); } - if(primarySkill >= GameConstants::PRIMARY_SKILLS) - { - primarySkill = rand.nextInt(GameConstants::PRIMARY_SKILLS - 1); - logGlobal->error("Wrong values in primarySkill%sLevel for hero class %s", isLowLevelHero ? "Low" : "High", type->heroClass->getNameTranslated()); - randomValue = 100 / GameConstants::PRIMARY_SKILLS; - } - logGlobal->trace("The hero gets the primary skill %d with a probability of %d %%.", primarySkill, randomValue); - return static_cast(primarySkill); + + return static_cast(RandomGeneratorUtil::nextItemWeighted(skillChances, rand)); } std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerator & rand) const @@ -1373,7 +1387,7 @@ std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerato assert(gainsLevel()); std::optional chosenSecondarySkill; - const auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(); + const auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand); if(!proposedSecondarySkills.empty()) { std::vector learnedSecondarySkills; @@ -1403,7 +1417,7 @@ void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 { if(primarySkill < PrimarySkill::EXPERIENCE) { - auto skill = getBonusLocalFirst(Selector::type()(BonusType::PRIMARY_SKILL) + auto skill = getLocalBonus(Selector::type()(BonusType::PRIMARY_SKILL) .And(Selector::subtype()(BonusSubtypeID(primarySkill))) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); assert(skill); @@ -1433,7 +1447,7 @@ void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 bool CGHeroInstance::gainsLevel() const { - return exp >= static_cast(VLC->heroh->reqExp(level+1)); + return level < VLC->heroh->maxSupportedLevel() && exp >= static_cast(VLC->heroh->reqExp(level+1)); } void CGHeroInstance::levelUp(const std::vector & skills) @@ -1441,8 +1455,9 @@ void CGHeroInstance::levelUp(const std::vector & skills) ++level; //deterministic secondary skills - skillsInfo.magicSchoolCounter = (skillsInfo.magicSchoolCounter + 1) % maxlevelsToMagicSchool(); - skillsInfo.wisdomCounter = (skillsInfo.wisdomCounter + 1) % maxlevelsToWisdom(); + ++skillsInfo.magicSchoolCounter; + ++skillsInfo.wisdomCounter; + for(const auto & skill : skills) { if((*VLC->skillh)[skill]->obligatory(CSkill::Obligatory::MAJOR)) @@ -1462,7 +1477,7 @@ void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) const auto primarySkill = nextPrimarySkill(rand); setPrimarySkill(primarySkill, 1, false); - auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(); + auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand); const auto secondarySkill = nextSecondarySkill(rand); if(secondarySkill) @@ -1502,7 +1517,7 @@ std::string CGHeroInstance::getHeroTypeName() const } else { - return VLC->heroh->objects[subID]->getJsonKey(); + return getHeroType().toEntity(VLC)->getJsonKey(); } } return ""; @@ -1510,33 +1525,14 @@ std::string CGHeroInstance::getHeroTypeName() const void CGHeroInstance::afterAddToMap(CMap * map) { - auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool - { - return (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; - }); - - if(existingHero != map->objects.end()) - { - if(settings["session"]["editor"].Bool()) - { - logGlobal->warn("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); - } - else - { - logGlobal->error("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); - - throw std::runtime_error("Hero is already on the map"); - } - } - - if(ID == Obj::HERO) + if(ID != Obj::PRISON) { map->heroesOnMap.emplace_back(this); } } void CGHeroInstance::afterRemoveFromMap(CMap* map) { - if (ID == Obj::HERO) + if (ID == Obj::PRISON) vstd::erase_if_present(map->heroesOnMap, this); } @@ -1636,15 +1632,11 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) for(size_t skillIndex = 0; skillIndex < secondarySkills.size(); ++skillIndex) { JsonArraySerializer inner = secondarySkills.enterArray(skillIndex); - const si32 rawId = secSkills.at(skillIndex).first; + SecondarySkill skillId = secSkills.at(skillIndex).first; - if(rawId < 0 || rawId >= VLC->skillh->size()) - logGlobal->error("Invalid secondary skill %d", rawId); - - auto value = (*VLC->skillh)[SecondarySkill(rawId)]->getJsonKey(); - handler.serializeString("skill", value); - value = NSecondarySkill::levels.at(secSkills.at(skillIndex).second); - handler.serializeString("level", value); + handler.serializeId("skill", skillId); + std::string skillLevel = NSecondarySkill::levels.at(secSkills.at(skillIndex).second); + handler.serializeString("level", skillLevel); } } } @@ -1661,7 +1653,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) { auto addSkill = [this](const std::string & skillId, const std::string & levelId) { - const int rawId = CSkillHandler::decodeSkill(skillId); + const int rawId = SecondarySkill::decode(skillId); if(rawId < 0) { logGlobal->error("Invalid secondary skill %s", skillId); @@ -1736,7 +1728,7 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!appearance) { // crossoverDeserialize - type = VLC->heroh->objects[subID]; + type = getHeroType().toHeroType(); appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); } @@ -1754,17 +1746,20 @@ void CGHeroInstance::serializeJsonDefinition(JsonSerializeFormat & handler) bool CGHeroInstance::isMissionCritical() const { - for(const TriggeredEvent & event : IObjectInterface::cb->getMapHeader()->triggeredEvents) + for(const TriggeredEvent & event : cb->getMapHeader()->triggeredEvents) { if (event.effect.type != EventEffect::DEFEAT) continue; auto const & testFunctor = [&](const EventCondition & condition) { - if ((condition.condition == EventCondition::CONTROL || condition.condition == EventCondition::HAVE_0) && condition.object) + if ((condition.condition == EventCondition::CONTROL) && condition.objectID != ObjectInstanceID::NONE) + return (id != condition.objectID); + + if (condition.condition == EventCondition::HAVE_ARTIFACT) { - const auto * hero = dynamic_cast(condition.object); - return (hero != this); + if(hasArt(condition.objectType.as())) + return true; } if(condition.condition == EventCondition::IS_HUMAN) @@ -1793,4 +1788,40 @@ void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &s } } +bool CGHeroInstance::isCampaignYog() const +{ + const StartInfo *si = cb->getStartInfo(); + + // it would be nice to find a way to move this hack to config/mapOverrides.json + if(!si || !si->campState) + return false; + + std::string campaign = si->campState->getFilename(); + if (!boost::starts_with(campaign, "DATA/YOG")) // "Birth of a Barbarian" + return false; + + if (getHeroType() != HeroTypeID::SOLMYR) // Yog (based on Solmyr) + return false; + + return true; +} + +bool CGHeroInstance::isCampaignGem() const +{ + const StartInfo *si = cb->getStartInfo(); + + // it would be nice to find a way to move this hack to config/mapOverrides.json + if(!si || !si->campState) + return false; + + std::string campaign = si->campState->getFilename(); + if (!boost::starts_with(campaign, "DATA/GEM") && !boost::starts_with(campaign, "DATA/FINAL")) // "New Beginning" and "Unholy Alliance" + return false; + + if (getHeroType() != HeroTypeID::GEM) // Yog (based on Solmyr) + return false; + + return true; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 2035a488b..5f6683a7f 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -23,18 +23,20 @@ class CGTownInstance; class CMap; struct TerrainTile; struct TurnInfo; -enum class EHeroGender : uint8_t; +enum class EHeroGender : int8_t; class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance { public: + using CGObjectInstance::CGObjectInstance; + /// if this is placeholder by power, then power rank of desired hero std::optional powerRank; /// if this is placeholder by type, then hero type of desired hero std::optional heroType; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & powerRank; @@ -69,7 +71,7 @@ public: ////////////////////////////////////////////////////////////////////////// - ConstTransitivePtr type; + const CHero * type; TExpType exp; //experience points ui32 level; //current level of hero @@ -89,7 +91,7 @@ public: static constexpr si32 UNINITIALIZED_MANA = -1; static constexpr ui32 UNINITIALIZED_MOVEMENT = -1; - static constexpr TExpType UNINITIALIZED_EXPERIENCE = std::numeric_limits::max(); + static constexpr auto UNINITIALIZED_EXPERIENCE = std::numeric_limits::max(); //std::vector artifacts; //hero's artifacts from bag //std::map artifWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 @@ -101,7 +103,7 @@ public: bool patrolling; int3 initialPos; ui32 patrolRadius; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & patrolling; h & initialPos; @@ -111,9 +113,6 @@ public: struct DLL_LINKAGE SecondarySkillsInfo { - //skills are determined, initialized at map start - //FIXME remove mutable - mutable CRandomGenerator rand; ui8 magicSchoolCounter; ui8 wisdomCounter; @@ -122,11 +121,10 @@ public: void resetMagicSchoolCounter(); void resetWisdomCounter(); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & magicSchoolCounter; h & wisdomCounter; - h & rand; } } skillsInfo; @@ -151,6 +149,9 @@ public: HeroTypeID getPortraitSource() const; int32_t getIconIndex() const; + std::string getClassNameTranslated() const; + std::string getClassNameTextID() const; + private: std::string getNameTextID() const; std::string getBiographyTextID() const; @@ -194,7 +195,7 @@ public: std::optional nextSecondarySkill(CRandomGenerator & rand) const; /// Gets 0, 1 or 2 secondary skills which are proposed on hero level up. - std::vector getLevelUpProposedSecondarySkills() const; + std::vector getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const; ui8 getSecSkillLevel(const SecondarySkill & skill) const; //0 - no skill @@ -231,7 +232,8 @@ public: ////////////////////////////////////////////////////////////////////////// - void setType(si32 ID, si32 subID) override; + HeroTypeID getHeroType() const; + void setHeroType(HeroTypeID type); void initHero(CRandomGenerator & rand); void initHero(CRandomGenerator & rand, const HeroTypeID & SUBID); @@ -252,7 +254,7 @@ public: /// If this hero perishes, the scenario is failed bool isMissionCritical() const; - CGHeroInstance(); + CGHeroInstance(IGameCallback *cb); virtual ~CGHeroInstance(); PlayerColor getOwner() const override; @@ -273,7 +275,7 @@ public: ///spells::Caster int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int64_t getSpellBonus(const spells::Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int64_t getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const override; @@ -294,6 +296,7 @@ public: void deserializationFix(); void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; std::string getObjectName() const override; @@ -303,9 +306,14 @@ public: void updateFrom(const JsonNode & data) override; bool isCoastVisitable() const override; + bool isBlockedVisitable() const override; BattleField getBattlefield() const override; + + bool isCampaignYog() const; + bool isCampaignGem() const; + protected: - void setPropertyDer(ui8 what, ui32 val) override;//synchr + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;//synchr ///common part of hero instance and hero definition void serializeCommonOptions(JsonSerializeFormat & handler); @@ -320,7 +328,7 @@ public: void serializeJsonDefinition(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index ef20b1024..dfc072366 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -25,7 +25,7 @@ VCMI_LIB_NAMESPACE_BEGIN void CGMarket::initObj(CRandomGenerator & rand) { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } void CGMarket::onHeroVisit(const CGHeroInstance * h) const @@ -48,18 +48,18 @@ int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const return -1; } -std::vector CGMarket::availableItemsIds(EMarketMode mode) const +std::vector CGMarket::availableItemsIds(EMarketMode mode) const { if(allowsTrade(mode)) return IMarket::availableItemsIds(mode); - return std::vector(); + return std::vector(); } -CGMarket::CGMarket() -{ -} +CGMarket::CGMarket(IGameCallback *cb): + CGObjectInstance(cb) +{} -std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const +std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const { switch(mode) { @@ -67,16 +67,16 @@ std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const return IMarket::availableItemsIds(mode); case EMarketMode::RESOURCE_ARTIFACT: { - std::vector ret; + std::vector ret; for(const CArtifact *a : artifacts) if(a) ret.push_back(a->getId()); else - ret.push_back(-1); + ret.push_back(ArtifactID{}); return ret; } default: - return std::vector(); + return std::vector(); } } @@ -91,26 +91,12 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const return; SetAvailableArtifacts saa; - saa.id = id.getNum(); + saa.id = id; cb->pickAllowedArtsSet(saa.arts, rand); cb->sendAndApply(&saa); } -void CGUniversity::initObj(CRandomGenerator & rand) -{ - CGMarket::initObj(rand); - - std::vector toChoose; - for(int i = 0; i < VLC->skillh->size(); ++i) - { - if(!vstd::contains(skills, i) && cb->isAllowed(2, i)) - { - toChoose.push_back(i); - } - } -} - -std::vector CGUniversity::availableItemsIds(EMarketMode mode) const +std::vector CGUniversity::availableItemsIds(EMarketMode mode) const { switch (mode) { @@ -118,7 +104,7 @@ std::vector CGUniversity::availableItemsIds(EMarketMode mode) const return skills; default: - return std::vector(); + return std::vector(); } } @@ -127,4 +113,9 @@ void CGUniversity::onHeroVisit(const CGHeroInstance * h) const cb->showObjectWindow(this, EOpenWindowMode::UNIVERSITY_WINDOW, h, true); } +ArtBearer::ArtBearer CGArtifactsAltar::bearerType() const +{ + return ArtBearer::ALTAR; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index aff11ce56..c0a589f3b 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -11,6 +11,7 @@ #include "CGObjectInstance.h" #include "IMarket.h" +#include "../CArtHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,7 +26,7 @@ public: std::string title; std::string speech; //currently shown only in university - CGMarket(); + CGMarket(IGameCallback *cb); ///IObjectInterface void onHeroVisit(const CGHeroInstance * h) const override; //open trading window void initObj(CRandomGenerator & rand) override;//set skills for trade @@ -34,9 +35,9 @@ public: int getMarketEfficiency() const override; bool allowsTrade(EMarketMode mode) const override; int availableUnits(EMarketMode mode, int marketItemSerial) const override; //-1 if unlimited - std::vector availableItemsIds(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & marketModes; @@ -49,12 +50,14 @@ public: class DLL_LINKAGE CGBlackMarket : public CGMarket { public: + using CGMarket::CGMarket; + std::vector artifacts; //available artifacts void newTurn(CRandomGenerator & rand) const override; //reset artifacts for black market every month - std::vector availableItemsIds(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & artifacts; @@ -64,17 +67,32 @@ public: class DLL_LINKAGE CGUniversity : public CGMarket { public: - std::vector skills; //available skills + using CGMarket::CGMarket; - std::vector availableItemsIds(EMarketMode mode) const override; - void initObj(CRandomGenerator & rand) override;//set skills for trade + std::vector skills; //available skills + + std::vector availableItemsIds(EMarketMode mode) const override; void onHeroVisit(const CGHeroInstance * h) const override; //open window - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & skills; } }; +class DLL_LINKAGE CGArtifactsAltar : public CGMarket, public CArtifactSet +{ +public: + using CGMarket::CGMarket; + + ArtBearer::ArtBearer bearerType() const override; + + template void serialize(Handler & h) + { + h & static_cast(*this); + h & static_cast(*this); + } +}; + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 7d5cacbe2..a3ca5643a 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -28,24 +28,26 @@ VCMI_LIB_NAMESPACE_BEGIN //TODO: remove constructor -CGObjectInstance::CGObjectInstance(): +CGObjectInstance::CGObjectInstance(IGameCallback *cb): + IObjectInterface(cb), pos(-1,-1,-1), ID(Obj::NO_OBJ), subID(-1), tempOwner(PlayerColor::UNFLAGGABLE), - blockVisit(false) + blockVisit(false), + removable(false) { } //must be instantiated in .cpp file for access to complete types of all member fields CGObjectInstance::~CGObjectInstance() = default; -int32_t CGObjectInstance::getObjGroupIndex() const +MapObjectID CGObjectInstance::getObjGroupIndex() const { - return ID.num; + return ID; } -int32_t CGObjectInstance::getObjTypeIndex() const +MapObjectSubID CGObjectInstance::getObjTypeIndex() const { return subID; } @@ -117,12 +119,12 @@ std::set CGObjectInstance::getBlockedPos() const return ret; } -std::set CGObjectInstance::getBlockedOffsets() const +const std::set & CGObjectInstance::getBlockedOffsets() const { return appearance->getBlockedOffsets(); } -void CGObjectInstance::setType(si32 newID, si32 newSubID) +void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID) { auto position = visitablePos(); auto oldOffset = getVisitableOffset(); @@ -166,38 +168,41 @@ void CGObjectInstance::setType(si32 newID, si32 newSubID) cb->gameState()->map->addBlockVisTiles(this); } -void CGObjectInstance::initObj(CRandomGenerator & rand) +void CGObjectInstance::pickRandomObject(CRandomGenerator & rand) { - switch(ID) - { - case Obj::TAVERN: - blockVisit = true; - break; - } + // no-op } -void CGObjectInstance::setProperty( ui8 what, ui32 val ) +void CGObjectInstance::initObj(CRandomGenerator & rand) { - setPropertyDer(what, val); // call this before any actual changes (needed at least for dwellings) + // no-op +} + +void CGObjectInstance::setProperty( ObjProperty what, ObjPropertyID identifier ) +{ + setPropertyDer(what, identifier); // call this before any actual changes (needed at least for dwellings) switch(what) { case ObjProperty::OWNER: - tempOwner = PlayerColor(val); + tempOwner = identifier.as(); break; case ObjProperty::BLOCKVIS: - blockVisit = val; + // Never actually used in code, but possible in ERM + blockVisit = identifier.getNum(); break; case ObjProperty::ID: - ID = Obj(val); - break; - case ObjProperty::SUBID: - subID = val; + ID = identifier.as(); break; } } -void CGObjectInstance::setPropertyDer( ui8 what, ui32 val ) +TObjectTypeHandler CGObjectInstance::getObjectHandler() const +{ + return VLC->objtypeh->getHandlerFor(ID, subID); +} + +void CGObjectInstance::setPropertyDer( ObjProperty what, ObjPropertyID identifier ) {} int3 CGObjectInstance::getSightCenter() const @@ -219,7 +224,7 @@ void CGObjectInstance::giveDummyBonus(const ObjectInstanceID & heroID, BonusDura { GiveBonus gbonus; gbonus.bonus.type = BonusType::NONE; - gbonus.id = heroID.getNum(); + gbonus.id = heroID; gbonus.bonus.duration = duration; gbonus.bonus.source = BonusSource::OBJECT_TYPE; gbonus.bonus.sid = BonusSourceID(ID); @@ -292,7 +297,7 @@ std::vector CGObjectInstance::getPopupComponents(const CGHeroInstance void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const { - switch(ID) + switch(ID.toEnum()) { case Obj::SANCTUARY: { @@ -320,9 +325,16 @@ bool CGObjectInstance::isVisitable() const bool CGObjectInstance::isBlockedVisitable() const { + // TODO: Read from json return blockVisit; } +bool CGObjectInstance::isRemovable() const +{ + // TODO: Read from json + return removable; +} + bool CGObjectInstance::isCoastVisitable() const { return false; diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 1e57c2344..b1e4f144e 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -21,6 +21,8 @@ struct Component; class JsonSerializeFormat; class ObjectTemplate; class CMap; +class AObjectTypeHandler; +using TObjectTypeHandler = std::shared_ptr; class DLL_LINKAGE CGObjectInstance : public IObjectInterface { @@ -28,9 +30,9 @@ public: /// Position of bottom-right corner of object on map int3 pos; /// Type of object, e.g. town, hero, creature. - Obj ID; + MapObjectID ID; /// Subtype of object, depends on type - si32 subID; + MapObjectSubID subID; /// Current owner of an object (when below PLAYER_LIMIT) PlayerColor tempOwner; /// Index of object in map's list of objects @@ -42,16 +44,17 @@ public: std::string typeName; std::string subTypeName; - CGObjectInstance(); //TODO: remove constructor + CGObjectInstance(IGameCallback *cb); ~CGObjectInstance() override; - int32_t getObjGroupIndex() const override; - int32_t getObjTypeIndex() const override; + MapObjectID getObjGroupIndex() const override; + MapObjectSubID getObjTypeIndex() const override; /// "center" tile from which the sight distance is calculated int3 getSightCenter() const; /// If true hero can visit this object only from neighbouring tiles and can't stand on this object bool blockVisit; + bool removable; PlayerColor getOwner() const override { @@ -75,7 +78,7 @@ public: bool coveringAt (const int3 & pos) const; //returns true if object covers with picture location (x, y) (h3m pos) std::set getBlockedPos() const; //returns set of positions blocked by this object - std::set getBlockedOffsets() const; //returns set of relative positions blocked by this object + const std::set & getBlockedOffsets() const; //returns set of relative positions blocked by this object /// returns true if object is visitable bool isVisitable() const; @@ -83,6 +86,9 @@ public: /// If true hero can visit this object only from neighbouring tiles and can't stand on this object virtual bool isBlockedVisitable() const; + // If true, can be possibly removed from the map + virtual bool isRemovable() const; + /// If true this object can be visited by hero standing on the coast virtual bool isCoastVisitable() const; @@ -94,6 +100,8 @@ public: std::optional getVisitSound() const; std::optional getRemovalSound() const; + TObjectTypeHandler getObjectHandler() const; + /** VIRTUAL METHODS **/ /// Returns true if player can pass through visitable tiles of this object @@ -102,10 +110,6 @@ public: virtual int getSightRadius() const; /// returns (x,y,0) offset to a visitable tile of object virtual int3 getVisitableOffset() const; - /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") - virtual void setType(si32 ID, si32 subID); - - /// returns text visible in status bar with specific hero/player active. /// Returns generic name of object, without any player-specific info virtual std::string getObjectName() const; @@ -124,25 +128,27 @@ public: /** OVERRIDES OF IObjectInterface **/ void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; /// method for synchronous update. Note: For new properties classes should override setPropertyDer instead - void setProperty(ui8 what, ui32 val) final; + void setProperty(ObjProperty what, ObjPropertyID identifier) final; virtual void afterAddToMap(CMap * map); virtual void afterRemoveFromMap(CMap * map); ///Entry point of binary (de-)serialization - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & instanceName; h & typeName; h & subTypeName; h & pos; h & ID; - h & subID; + subID.serializeIdentifier(h, ID); h & id; h & tempOwner; h & blockVisit; + h & removable; h & appearance; //definfo is handled by map serializer } @@ -153,7 +159,10 @@ public: protected: /// virtual method that allows synchronously update object state on server and all clients - virtual void setPropertyDer(ui8 what, ui32 val); + virtual void setPropertyDer(ObjProperty what, ObjPropertyID identifier); + + /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") + void setType(MapObjectID ID, MapObjectSubID subID); /// Gives dummy bonus from this object to hero. Can be used to track visited state void giveDummyBonus(const ObjectInstanceID & heroID, BonusDuration::Type duration = BonusDuration::ONE_DAY) const; diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 22d5aa6e6..b79077948 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -66,7 +66,7 @@ void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, b return text; }; - auto sendInfoWindow = [h](const MetaString & text, const Rewardable::Reward & reward) + auto sendInfoWindow = [&](const MetaString & text, const Rewardable::Reward & reward) { InfoWindow iw; iw.player = h->tempOwner; @@ -135,7 +135,7 @@ void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, b for(auto c : vi.reward.creatures) { loot.appendRawString("%s"); - loot.replaceCreatureName(c); + loot.replaceName(c); } if(vi.reward.creatures.size() == 1 && vi.reward.creatures[0].count == 1) @@ -226,7 +226,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeInt("experience", vinfo.reward.heroExperience, 0); handler.serializeInt("mana", vinfo.reward.manaDiff, 0); - int val; + int val = 0; handler.serializeInt("morale", val, 0); if(val) vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); @@ -257,7 +257,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) const std::string skillName = p.first; const std::string levelId = p.second.String(); - const int rawId = CSkillHandler::decodeSkill(skillName); + const int rawId = SecondarySkill::decode(skillName); if(rawId < 0) { logGlobal->error("Invalid secondary skill %s", skillName); diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index 4bbd5b2af..f739f1fb9 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -19,6 +19,8 @@ struct InfoWindow; class DLL_LINKAGE CGPandoraBox : public CRewardableObject { public: + using CRewardableObject::CRewardableObject; + MetaString message; void initObj(CRandomGenerator & rand) override; @@ -26,7 +28,7 @@ public: void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & message; @@ -41,12 +43,14 @@ protected: class DLL_LINKAGE CGEvent : public CGPandoraBox //event objects { public: + using CGPandoraBox::CGPandoraBox; + bool removeAfterVisit = false; //true if event is removed after occurring std::set availableFor; //players whom this event is available for bool computerActivate = false; //true if computer player can activate this event bool humanActivate = false; //true if human player can activate this event - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & removeAfterVisit; diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 4921b085a..94c855448 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -19,17 +19,27 @@ VCMI_LIB_NAMESPACE_BEGIN +CGTownBuilding::CGTownBuilding(IGameCallback * cb) + : IObjectInterface(cb) + , town(nullptr) +{} + +CGTownBuilding::CGTownBuilding(CGTownInstance * town) + : IObjectInterface(town->cb) + , town(town) +{} + PlayerColor CGTownBuilding::getOwner() const { return town->getOwner(); } -int32_t CGTownBuilding::getObjGroupIndex() const +MapObjectID CGTownBuilding::getObjGroupIndex() const { return -1; } -int32_t CGTownBuilding::getObjTypeIndex() const +MapObjectSubID CGTownBuilding::getObjTypeIndex() const { return 0; } @@ -111,21 +121,24 @@ std::string CGTownBuilding::getCustomBonusGreeting(const Bonus & bonus) const return greeting; } +COPWBonus::COPWBonus(IGameCallback *cb) + : CGTownBuilding(cb) +{} COPWBonus::COPWBonus(const BuildingID & bid, BuildingSubID::EBuildingSubID subId, CGTownInstance * cgTown) + : CGTownBuilding(cgTown) { bID = bid; bType = subId; - town = cgTown; indexOnTV = static_cast(town->bonusingBuildings.size()); } -void COPWBonus::setProperty(ui8 what, ui32 val) +void COPWBonus::setProperty(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::VISITORS: - visitors.insert(val); + visitors.insert(identifier.as()); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: visitors.clear(); @@ -148,14 +161,10 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const { GiveBonus gb; gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT_TYPE, 600, BonusSourceID(Obj(Obj::STABLES)), BonusCustomSubtype::heroMovementLand, VLC->generaltexth->arraytxt[100]); - gb.id = heroID.getNum(); + gb.id = heroID; cb->giveHeroBonus(&gb); - SetMovePoints mp; - mp.val = 600; - mp.absolute = false; - mp.hid = heroID; - cb->setMovePoints(&mp); + cb->setMovePoints(heroID, 600, false); iw.text.appendRawString(VLC->generaltexth->allTexts[580]); cb->showInfoDialog(&iw); @@ -179,18 +188,22 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const } } +CTownBonus::CTownBonus(IGameCallback *cb) + : CGTownBuilding(cb) +{} + CTownBonus::CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * cgTown) + : CGTownBuilding(cgTown) { bID = index; bType = subId; - town = cgTown; indexOnTV = static_cast(town->bonusingBuildings.size()); } -void CTownBonus::setProperty (ui8 what, ui32 val) +void CTownBonus::setProperty(ObjProperty what, ObjPropertyID identifier) { if(what == ObjProperty::VISITORS) - visitors.insert(ObjectInstanceID(val)); + visitors.insert(identifier.as()); } void CTownBonus::onHeroVisit (const CGHeroInstance * h) const @@ -207,31 +220,31 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const case BuildingSubID::KNOWLEDGE_VISITING_BONUS: //wall of knowledge what = PrimarySkill::KNOWLEDGE; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 3, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::KNOWLEDGE, 1); break; case BuildingSubID::SPELL_POWER_VISITING_BONUS: //order of fire what = PrimarySkill::SPELL_POWER; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 2, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::SPELL_POWER, 1); break; case BuildingSubID::ATTACK_VISITING_BONUS: //hall of Valhalla what = PrimarySkill::ATTACK; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 0, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::ATTACK, 1); break; case BuildingSubID::EXPERIENCE_VISITING_BONUS: //academy of battle scholars what = PrimarySkill::EXPERIENCE; val = static_cast(h->calculateXp(1000)); - iw.components.emplace_back(Component::EComponentType::EXPERIENCE, 0, val, 0); + iw.components.emplace_back(ComponentType::EXPERIENCE, val); break; case BuildingSubID::DEFENSE_VISITING_BONUS: //cage of warlords what = PrimarySkill::DEFENSE; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 1, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::DEFENSE, 1); break; case BuildingSubID::CUSTOM_VISITING_BONUS: @@ -249,8 +262,12 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const iw.player = cb->getOwner(heroID); iw.text.appendRawString(getVisitingBonusGreeting()); cb->showInfoDialog(&iw); - cb->changePrimSkill (cb->getHero(heroID), what, val); - town->addHeroToStructureVisitors(h, indexOnTV); + if (what == PrimarySkill::EXPERIENCE) + cb->giveExperience(cb->getHero(heroID), val); + else + cb->changePrimSkill(cb->getHero(heroID), what, val); + + town->addHeroToStructureVisitors(h, indexOnTV); } } } @@ -272,7 +289,7 @@ void CTownBonus::applyBonuses(CGHeroInstance * h, const BonusList & bonuses) con bonus->duration = BonusDuration::ONE_DAY; } gb.bonus = * bonus; - gb.id = h->id.getNum(); + gb.id = h->id; cb->giveHeroBonus(&gb); if(bonus->duration == BonusDuration::PERMANENT) @@ -286,11 +303,15 @@ void CTownBonus::applyBonuses(CGHeroInstance * h, const BonusList & bonuses) con town->addHeroToStructureVisitors(h, indexOnTV); } +CTownRewardableBuilding::CTownRewardableBuilding(IGameCallback *cb) + : CGTownBuilding(cb) +{} + CTownRewardableBuilding::CTownRewardableBuilding(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * cgTown, CRandomGenerator & rand) + : CGTownBuilding(cgTown) { bID = index; bType = subId; - town = cgTown; indexOnTV = static_cast(town->bonusingBuildings.size()); initObj(rand); } @@ -301,7 +322,7 @@ void CTownRewardableBuilding::initObj(CRandomGenerator & rand) auto building = town->town->buildings.at(bID); - building->rewardableObjectInfo.configureObject(configuration, rand); + building->rewardableObjectInfo.configureObject(configuration, rand, cb); for(auto & rewardInfo : configuration.info) { for (auto & bonus : rewardInfo.reward.bonuses) @@ -318,21 +339,21 @@ void CTownRewardableBuilding::newTurn(CRandomGenerator & rand) const { if(configuration.resetParameters.rewards) { - cb->setObjProperty(town->id, ObjProperty::REWARD_RANDOMIZE, indexOnTV); + cb->setObjPropertyValue(town->id, ObjProperty::REWARD_RANDOMIZE, indexOnTV); } if(configuration.resetParameters.visitors) { - cb->setObjProperty(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, indexOnTV); + cb->setObjPropertyValue(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, indexOnTV); } } } -void CTownRewardableBuilding::setProperty(ui8 what, ui32 val) +void CTownRewardableBuilding::setProperty(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::VISITORS: - visitors.insert(ObjectInstanceID(val)); + visitors.insert(identifier.as()); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: visitors.clear(); @@ -341,7 +362,7 @@ void CTownRewardableBuilding::setProperty(ui8 what, ui32 val) initObj(cb->gameState()->getRandomGenerator()); break; case ObjProperty::REWARD_SELECT: - selectedReward = val; + selectedReward = identifier.getNum(); break; } } diff --git a/lib/mapObjects/CGTownBuilding.h b/lib/mapObjects/CGTownBuilding.h index 1fffb3710..9b620a6ac 100644 --- a/lib/mapObjects/CGTownBuilding.h +++ b/lib/mapObjects/CGTownBuilding.h @@ -22,9 +22,12 @@ class DLL_LINKAGE CGTownBuilding : public IObjectInterface { ///basic class for town structures handled as map objects public: + CGTownBuilding(CGTownInstance * town); + CGTownBuilding(IGameCallback *cb); + si32 indexOnTV = 0; //identifies its index on towns vector - CGTownInstance * town = nullptr; + CGTownInstance * town; STRONG_INLINE BuildingSubID::EBuildingSubID getBuildingSubtype() const @@ -45,13 +48,13 @@ public: } PlayerColor getOwner() const override; - int32_t getObjGroupIndex() const override; - int32_t getObjTypeIndex() const override; + MapObjectID getObjGroupIndex() const override; + MapObjectSubID getObjTypeIndex() const override; int3 visitablePos() const override; int3 getPosition() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & bID; h & indexOnTV; @@ -69,14 +72,14 @@ protected: class DLL_LINKAGE COPWBonus : public CGTownBuilding {///used for OPW bonusing structures public: - std::set visitors; - void setProperty(ui8 what, ui32 val) override; + std::set visitors; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit (const CGHeroInstance * h) const override; COPWBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * TOWN); - COPWBonus() = default; + COPWBonus(IGameCallback *cb); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & visitors; @@ -89,13 +92,13 @@ class DLL_LINKAGE CTownBonus : public CGTownBuilding ///feel free to merge inheritance tree public: std::set visitors; - void setProperty(ui8 what, ui32 val) override; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit (const CGHeroInstance * h) const override; CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * TOWN); - CTownBonus() = default; + CTownBonus(IGameCallback *cb); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & visitors; @@ -117,7 +120,7 @@ class DLL_LINKAGE CTownRewardableBuilding : public CGTownBuilding, public Reward void grantReward(ui32 rewardID, const CGHeroInstance * hero) const; public: - void setProperty(ui8 what, ui32 val) override; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit(const CGHeroInstance * h) const override; void newTurn(CRandomGenerator & rand) const override; @@ -131,9 +134,9 @@ public: void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; CTownRewardableBuilding(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * town, CRandomGenerator & rand); - CTownRewardableBuilding() = default; + CTownRewardableBuilding(IGameCallback *cb); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9e7f842b2..ecb233ff5 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -20,6 +20,7 @@ #include "../gameState/CGameState.h" #include "../mapping/CMap.h" #include "../CPlayerState.h" +#include "../StartInfo.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -32,10 +33,6 @@ VCMI_LIB_NAMESPACE_BEGIN -std::vector CGTownInstance::merchantArtifacts; -std::vector CGTownInstance::universitySkills; - - int CGTownInstance::getSightRadius() const //returns sight distance { auto ret = CBuilding::HEIGHT_NO_TOWER; @@ -52,28 +49,28 @@ int CGTownInstance::getSightRadius() const //returns sight distance return ret; } -void CGTownInstance::setPropertyDer(ui8 what, ui32 val) +void CGTownInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { ///this is freakin' overcomplicated solution switch (what) { case ObjProperty::STRUCTURE_ADD_VISITING_HERO: - bonusingBuildings[val]->setProperty(ObjProperty::VISITORS, visitingHero->id.getNum()); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, visitingHero->id); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: - bonusingBuildings[val]->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, 0); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, NumericID(0)); break; case ObjProperty::STRUCTURE_ADD_GARRISONED_HERO: //add garrisoned hero to visitors - bonusingBuildings[val]->setProperty(ObjProperty::VISITORS, garrisonHero->id.getNum()); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, garrisonHero->id); break; case ObjProperty::BONUS_VALUE_FIRST: - bonusValue.first = val; + bonusValue.first = identifier.getNum(); break; case ObjProperty::BONUS_VALUE_SECOND: - bonusValue.second = val; + bonusValue.second = identifier.getNum(); break; case ObjProperty::REWARD_RANDOMIZE: - bonusingBuildings[val]->setProperty(ObjProperty::REWARD_RANDOMIZE, 0); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::REWARD_RANDOMIZE, NumericID(0)); break; } } @@ -135,7 +132,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const if (creatures[level].second.empty()) return ret; //no dwelling - const CCreature *creature = VLC->creh->objects[creatures[level].second.back()]; + const Creature *creature = creatures[level].second.back().toEntity(VLC); const int base = creature->getGrowth(); int castleBonus = 0; @@ -163,7 +160,8 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const } //other *-of-legion-like bonuses (%d to growth cumulative with grail) - TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level))); + // Note: bonus uses 1-based levels (Pikeman is level 1), town list uses 0-based (Pikeman in 0-th creatures entry) + TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level+1))); for(const auto & b : *bonuses) ret.entries.emplace_back(b->val, b->Description()); @@ -226,7 +224,8 @@ bool CGTownInstance::hasCapitol() const return hasBuilt(BuildingID::CAPITOL); } -CGTownInstance::CGTownInstance(): +CGTownInstance::CGTownInstance(IGameCallback *cb): + CGDwelling(cb), town(nullptr), builded(0), destroyed(0), @@ -324,7 +323,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const InfoWindow iw; iw.player = h->tempOwner; iw.text.appendRawString(h->commander->getName()); - iw.components.emplace_back(*h->commander); + iw.components.emplace_back(ComponentType::CREATURE, h->commander->getId(), h->commander->getCount()); cb->showInfoDialog(&iw); } } @@ -371,7 +370,7 @@ bool CGTownInstance::isBonusingBuildingAdded(BuildingID bid) const { auto present = std::find_if(bonusingBuildings.begin(), bonusingBuildings.end(), [&](CGTownBuilding* building) { - return building->getBuildingType().num == bid; + return building->getBuildingType() == bid; }); return present != bonusingBuildings.end(); @@ -459,6 +458,40 @@ void CGTownInstance::deleteTownBonus(BuildingID bid) delete freeIt; } +FactionID CGTownInstance::randomizeFaction(CRandomGenerator & rand) +{ + if(getOwner().isValidPlayer()) + return cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner()).castle; + + if(alignmentToPlayer.isValidPlayer()) + return cb->gameState()->scenarioOps->getIthPlayersSettings(alignmentToPlayer).castle; + + std::vector potentialPicks; + + for (FactionID faction(0); faction < FactionID(VLC->townh->size()); ++faction) + if (VLC->factions()->getById(faction)->hasTown()) + potentialPicks.push_back(faction); + + assert(!potentialPicks.empty()); + return *RandomGeneratorUtil::nextItem(potentialPicks, rand); +} + +void CGTownInstance::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == MapObjectID::TOWN || ID == MapObjectID::RANDOM_TOWN); + if (ID == MapObjectID::RANDOM_TOWN) + { + ID = MapObjectID::TOWN; + subID = randomizeFaction(rand); + } + + assert(ID == Obj::TOWN); // just in case + setType(ID, subID); + town = (*VLC->townh)[getFaction()]->town; + randomizeArmy(getFaction()); + updateAppearance(); +} + void CGTownInstance::initObj(CRandomGenerator & rand) ///initialize town structures { blockVisit = true; @@ -499,12 +532,12 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const resID = (resID==2)?1:resID; int resVal = rand.nextInt(1, 4);//with size 1..4 cb->giveResource(tempOwner, static_cast(resID), resVal); - cb->setObjProperty (id, ObjProperty::BONUS_VALUE_FIRST, resID); - cb->setObjProperty (id, ObjProperty::BONUS_VALUE_SECOND, resVal); + cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_FIRST, resID); + cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_SECOND, resVal); } for(const auto * manaVortex : getBonusingBuildings(BuildingSubID::MANA_VORTEX)) - cb->setObjProperty(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex //get Mana Vortex or Stables bonuses //same code is in the CGameHandler::buildStructure method @@ -536,7 +569,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const } else //upgrade { - cb->changeStackType(sl, VLC->creh->objects[*c->upgrades.begin()]); + cb->changeStackType(sl, c->upgrades.begin()->toCreature()); } } if ((stacksCount() < GameConstants::ARMY_SIZE && rand.nextInt(99) < 25) || Slots().empty()) //add new stack @@ -549,7 +582,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const TQuantity count = creatureGrowth(i); if (!count) // no dwelling - count = VLC->creatures()->getByIndex(c)->getGrowth(); + count = VLC->creatures()->getById(c)->getGrowth(); {//no lower tiers or above current month @@ -557,7 +590,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const { StackLocation sl(this, n); if (slotEmpty(n)) - cb->insertNewStack(sl, VLC->creh->objects[c], count); + cb->insertNewStack(sl, c.toCreature(), count); else //add to existing cb->changeStackCount(sl, count); } @@ -730,40 +763,31 @@ bool CGTownInstance::allowsTrade(EMarketMode mode) const } } -std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const +std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const { if(mode == EMarketMode::RESOURCE_ARTIFACT) { - std::vector ret; - for(const CArtifact *a : merchantArtifacts) + std::vector ret; + for(const CArtifact *a : cb->gameState()->map->townMerchantArtifacts) if(a) ret.push_back(a->getId()); else - ret.push_back(-1); + ret.push_back(ArtifactID{}); return ret; } else if ( mode == EMarketMode::RESOURCE_SKILL ) { - return universitySkills; + return cb->gameState()->map->townUniversitySkills; } else return IMarket::availableItemsIds(mode); } -void CGTownInstance::setType(si32 ID, si32 subID) -{ - assert(ID == Obj::TOWN); // just in case - CGObjectInstance::setType(ID, subID); - town = (*VLC->townh)[subID]->town; - randomizeArmy(subID); - updateAppearance(); -} - void CGTownInstance::updateAppearance() { auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); //FIXME: not the best way to do this - auto app = VLC->objtypeh->getHandlerFor(ID, subID)->getOverride(terrain, this); + auto app = getObjectHandler()->getOverride(terrain, this); if (app) appearance = app; } @@ -891,7 +915,7 @@ const CTown * CGTownInstance::getTown() const { if(nullptr == town) { - return (*VLC->townh)[subID]->town; + return (*VLC->townh)[getFaction()]->town; } else return town; @@ -921,6 +945,11 @@ std::string CGTownInstance::getNameTranslated() const return VLC->generaltexth->translate(nameTextId); } +std::string CGTownInstance::getNameTextID() const +{ + return nameTextId; +} + void CGTownInstance::setNameTextId( const std::string & newName ) { nameTextId = newName; @@ -970,9 +999,9 @@ bool CGTownInstance::hasBuilt(const BuildingID & buildingID) const return vstd::contains(builtBuildings, buildingID); } -bool CGTownInstance::hasBuilt(const BuildingID & buildingID, int townID) const +bool CGTownInstance::hasBuilt(const BuildingID & buildingID, FactionID townID) const { - if (townID == town->faction->getIndex() || townID == ETownType::ANY) + if (townID == town->faction->getId() || townID == FactionID::ANY) return hasBuilt(buildingID); return false; } @@ -1046,14 +1075,14 @@ CBuilding::TRequired CGTownInstance::genBuildingRequirements(const BuildingID & void CGTownInstance::addHeroToStructureVisitors(const CGHeroInstance *h, si64 structureInstanceID ) const { if(visitingHero == h) - cb->setObjProperty(id, ObjProperty::STRUCTURE_ADD_VISITING_HERO, structureInstanceID); //add to visitors + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_ADD_VISITING_HERO, structureInstanceID); //add to visitors else if(garrisonHero == h) - cb->setObjProperty(id, ObjProperty::STRUCTURE_ADD_GARRISONED_HERO, structureInstanceID); //then it must be garrisoned hero + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_ADD_GARRISONED_HERO, structureInstanceID); //then it must be garrisoned hero else { //should never ever happen logGlobal->error("Cannot add hero %s to visitors of structure # %d", h->getNameTranslated(), structureInstanceID); - throw std::runtime_error("internal error"); + throw std::runtime_error("unexpected hero in CGTownInstance::addHeroToStructureVisitors"); } } @@ -1074,20 +1103,12 @@ void CGTownInstance::onTownCaptured(const PlayerColor & winner) const void CGTownInstance::afterAddToMap(CMap * map) { - if(ID == Obj::TOWN) - map->towns.emplace_back(this); + map->towns.emplace_back(this); } void CGTownInstance::afterRemoveFromMap(CMap * map) { - if (ID == Obj::TOWN) - vstd::erase_if_present(map->towns, this); -} - -void CGTownInstance::reset() -{ - CGTownInstance::merchantArtifacts.clear(); - CGTownInstance::universitySkills.clear(); + vstd::erase_if_present(map->towns, this); } void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) @@ -1125,7 +1146,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) for(const BuildingID & id : forbiddenBuildings) { - buildingsLIC.none.insert(id); + buildingsLIC.none.insert(id.getNum()); customBuildings = true; } @@ -1142,7 +1163,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(id == BuildingID::FORT) hasFort = true; - buildingsLIC.all.insert(id); + buildingsLIC.all.insert(id.getNum()); customBuildings = true; } @@ -1177,42 +1198,14 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) } { - std::vector standard = VLC->spellh->getDefaultAllowed(); - JsonSerializeFormat::LIC spellsLIC(standard, SpellID::decode, SpellID::encode); - - if(handler.saving) - { - for(const SpellID & id : possibleSpells) - spellsLIC.any[id.num] = true; - - for(const SpellID & id : obligatorySpells) - spellsLIC.all[id.num] = true; - } - - handler.serializeLIC("spells", spellsLIC); - - if(!handler.saving) - { - possibleSpells.clear(); - for(si32 idx = 0; idx < spellsLIC.any.size(); idx++) - { - if(spellsLIC.any[idx]) - possibleSpells.emplace_back(idx); - } - - obligatorySpells.clear(); - for(si32 idx = 0; idx < spellsLIC.all.size(); idx++) - { - if(spellsLIC.all[idx]) - obligatorySpells.emplace_back(idx); - } - } + handler.serializeIdArray( "possibleSpells", possibleSpells); + handler.serializeIdArray( "obligatorySpells", obligatorySpells); } } FactionID CGTownInstance::getFaction() const { - return town->faction->getId(); + return FactionID(subID.getNum()); } TerrainId CGTownInstance::getNativeTerrain() const @@ -1223,12 +1216,21 @@ TerrainId CGTownInstance::getNativeTerrain() const GrowthInfo::Entry::Entry(const std::string &format, int _count) : count(_count) { - description = boost::str(boost::format(format) % count); + MetaString formatter; + formatter.appendRawString(format); + formatter.replacePositiveNumber(count); + + description = formatter.toString(); } GrowthInfo::Entry::Entry(int subID, const BuildingID & building, int _count): count(_count) { - description = boost::str(boost::format("%s %+d") % (*VLC->townh)[subID]->town->buildings.at(building)->getNameTranslated() % count); + MetaString formatter; + formatter.appendRawString("%s %+d"); + formatter.replaceRawString((*VLC->townh)[subID]->town->buildings.at(building)->getNameTranslated()); + formatter.replacePositiveNumber(count); + + description = formatter.toString(); } GrowthInfo::Entry::Entry(int _count, std::string fullDescription): diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 07da57644..f8fd3f8e3 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -67,10 +67,7 @@ public: std::pair bonusValue;//var to store town bonuses (rampart = resources from mystic pond); ////////////////////////////////////////////////////////////////////////// - static std::vector merchantArtifacts; //vector of artifacts available at Artifact merchant, NULLs possible (for making empty space when artifact is bought) - static std::vector universitySkills;//skills for university of magic - - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & nameTextId; @@ -92,7 +89,18 @@ public: for(auto * bonusingBuilding : bonusingBuildings) bonusingBuilding->town = this; - h & town; + if (h.saving) + { + CFaction * faction = town ? town->faction : nullptr; + h & faction; + } + else + { + CFaction * faction = nullptr; + h & faction; + town = faction ? faction->town : nullptr; + } + h & townAndVis; BONUS_TREE_DESERIALIZATION_FIX @@ -126,6 +134,7 @@ public: const CArmedInstance *getUpperArmy() const; //garrisoned hero if present or the town itself std::string getNameTranslated() const; + std::string getNameTextID() const; void setNameTextId(const std::string & newName); ////////////////////////////////////////////////////////////////////////// @@ -139,9 +148,8 @@ public: const IObjectInterface * getObject() const override; int getMarketEfficiency() const override; //=market count bool allowsTrade(EMarketMode mode) const override; - std::vector availableItemsIds(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; - void setType(si32 ID, si32 subID) override; void updateAppearance(); ////////////////////////////////////////////////////////////////////////// @@ -161,7 +169,7 @@ public: bool hasBuilt(BuildingSubID::EBuildingSubID buildingID) const; //checks if building is constructed and town has same subID bool hasBuilt(const BuildingID & buildingID) const; - bool hasBuilt(const BuildingID & buildingID, int townID) const; + bool hasBuilt(const BuildingID & buildingID, FactionID townID) const; TResources getBuildingCost(const BuildingID & buildingID) const; TResources dailyIncome() const; //calculates daily income of this town @@ -189,7 +197,7 @@ public: FactionID getFaction() const override; TerrainId getNativeTerrain() const override; - CGTownInstance(); + CGTownInstance(IGameCallback *cb); virtual ~CGTownInstance(); ///IObjectInterface overrides @@ -197,6 +205,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void onHeroLeave(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void battleFinished(const CGHeroInstance * hero, const BattleResult & result) const override; std::string getObjectName() const override; @@ -204,18 +213,18 @@ public: void afterAddToMap(CMap * map) override; void afterRemoveFromMap(CMap * map) override; - static void reset(); inline bool isBattleOutsideTown(const CGHeroInstance * defendingHero) const { return defendingHero && garrisonHero && defendingHero != garrisonHero; } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; private: + FactionID randomizeFaction(CRandomGenerator & rand); void setOwner(const PlayerColor & owner) const; void onTownCaptured(const PlayerColor & winner) const; int getDwellingBonus(const std::vector& creatureIds, const std::vector >& dwellings) const; diff --git a/lib/mapObjects/CObjectHandler.cpp b/lib/mapObjects/CObjectHandler.cpp index 34162c4c3..8226d01ef 100644 --- a/lib/mapObjects/CObjectHandler.cpp +++ b/lib/mapObjects/CObjectHandler.cpp @@ -13,7 +13,7 @@ #include "CGObjectInstance.h" #include "../filesystem/ResourcePath.h" -#include "../JsonNode.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -28,14 +28,4 @@ CObjectHandler::CObjectHandler() logGlobal->trace("\t\tDone loading resource prices!"); } -CGObjectInstanceBySubIdFinder::CGObjectInstanceBySubIdFinder(CGObjectInstance * obj) : obj(obj) -{ - -} - -bool CGObjectInstanceBySubIdFinder::operator()(CGObjectInstance * obj) const -{ - return this->obj->subID == obj->subID; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CObjectHandler.h b/lib/mapObjects/CObjectHandler.h index 128d579b4..7413918b2 100644 --- a/lib/mapObjects/CObjectHandler.h +++ b/lib/mapObjects/CObjectHandler.h @@ -16,28 +16,12 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; class int3; -/// function object which can be used to find an object with an specific sub ID -class CGObjectInstanceBySubIdFinder -{ -public: - CGObjectInstanceBySubIdFinder(CGObjectInstance * obj); - bool operator()(CGObjectInstance * obj) const; - -private: - CGObjectInstance * obj; -}; - class DLL_LINKAGE CObjectHandler { public: std::vector resVals; //default values of resources in gold CObjectHandler(); - - template void serialize(Handler &h, const int version) - { - h & resVals; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 5431a12a1..150170676 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -23,6 +23,7 @@ #include "../serializer/JsonSerializeFormat.h" #include "../GameConstants.h" #include "../constants/StringConstants.h" +#include "../CPlayerState.h" #include "../CSkillHandler.h" #include "../mapping/CMap.h" #include "../mapObjects/CGHeroInstance.h" @@ -30,12 +31,11 @@ #include "../modding/ModUtility.h" #include "../networkPacks/PacksForClient.h" #include "../spells/CSpellHandler.h" +#include "../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN -std::map > CGKeys::playerKeyMap; - //TODO: Remove constructor CQuest::CQuest(): qid(-1), @@ -129,12 +129,12 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const if(!mission.heroAllowed(h)) return false; - if(killTarget != ObjectInstanceID::NONE) + if(killTarget.hasValue()) { - if(CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) + PlayerColor owner = h->getOwner(); + if (!h->cb->getPlayerState(owner)->destroyedObjects.count(killTarget)) return false; } - return true; } @@ -144,7 +144,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const { if(h->hasArt(elem)) { - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); + cb->removeArtifact(ArtifactLocation(h->id, h->getArtPos(elem, false))); } else { @@ -153,7 +153,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const auto parts = assembly->getPartsInfo(); // Remove the assembly - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); + cb->removeArtifact(ArtifactLocation(h->id, h->getArtPos(assembly))); // Disassemble this backpack artifact for(const auto & ci : parts) @@ -168,7 +168,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const cb->giveResources(h->getOwner(), mission.resources); } -void CQuest::addTextReplacements(MetaString & text, std::vector & components) const +void CQuest::addTextReplacements(IGameCallback * cb, MetaString & text, std::vector & components) const { if(mission.heroLevel > 0) text.replaceNumber(mission.heroLevel); @@ -204,13 +204,13 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) { - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); + components.emplace_back(ComponentType::HERO_PORTRAIT, heroPortrait); addKillTargetReplacements(text); } - if(killTarget != ObjectInstanceID::NONE && stackToKill.type) + if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) { - components.emplace_back(stackToKill); + components.emplace_back(ComponentType::CREATURE, stackToKill); addKillTargetReplacements(text); } @@ -223,7 +223,7 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com for(const auto & elem : mission.artifacts) { loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); + loot.replaceName(elem); } text.replaceRawString(loot.buildList()); } @@ -234,7 +234,7 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com for(const auto & elem : mission.creatures) { loot.appendRawString("%s"); - loot.replaceCreatureName(elem); + loot.replaceName(elem); } text.replaceRawString(loot.buildList()); } @@ -242,13 +242,13 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com if(mission.resources.nonZero()) { MetaString loot; - for(int i = 0; i < 7; ++i) + for(auto i : GameResID::ALL_RESOURCES()) { if(mission.resources[i]) { loot.appendRawString("%d %s"); loot.replaceNumber(mission.resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); + loot.replaceName(i); } } text.replaceRawString(loot.buildList()); @@ -258,16 +258,16 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com { MetaString loot; for(auto & p : mission.players) - loot.appendLocalString(EMetaText::COLOR, p); + loot.appendName(p); text.replaceRawString(loot.buildList()); } if(lastDay >= 0) - text.replaceNumber(lastDay - IObjectInterface::cb->getDate(Date::DAY)); + text.replaceNumber(lastDay - cb->getDate(Date::DAY)); } -void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const +void CQuest::getVisitText(IGameCallback * cb, MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const { bool failRequirements = (h ? !checkQuest(h) : true); mission.loadComponents(components, h); @@ -280,10 +280,10 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components if(lastDay >= 0) iwText.appendTextID(TextIdentifier("core", "seerhut", "time", textOption).get()); - addTextReplacements(iwText, components); + addTextReplacements(cb, iwText, components); } -void CQuest::getRolloverText(MetaString &ms, bool onHover) const +void CQuest::getRolloverText(IGameCallback * cb, MetaString &ms, bool onHover) const { if(onHover) ms.appendRawString("\n\n"); @@ -293,15 +293,15 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const ms.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, questState, textOption).get()); std::vector components; - addTextReplacements(ms, components); + addTextReplacements(cb, ms, components); } -void CQuest::getCompletionText(MetaString &iwText) const +void CQuest::getCompletionText(IGameCallback * cb, MetaString &iwText) const { iwText.appendRawString(completedText.toString()); std::vector components; - addTextReplacements(iwText, components); + addTextReplacements(cb, iwText, components); } void CQuest::defineQuestName() @@ -314,7 +314,7 @@ void CQuest::defineQuestName() if(!mission.spells.empty()) questName = CQuest::missionName(2); if(!mission.secondary.empty()) questName = CQuest::missionName(2); if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) questName = CQuest::missionName(3); - if(killTarget != ObjectInstanceID::NONE && stackToKill.getType()) questName = CQuest::missionName(4); + if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) questName = CQuest::missionName(4); if(!mission.artifacts.empty()) questName = CQuest::missionName(5); if(!mission.creatures.empty()) questName = CQuest::missionName(6); if(mission.resources.nonZero()) questName = CQuest::missionName(7); @@ -326,10 +326,10 @@ void CQuest::defineQuestName() void CQuest::addKillTargetReplacements(MetaString &out) const { if(!heroName.empty()) - out.replaceTextID(heroName); - if(stackToKill.type) + out.replaceRawString(heroName); + if(stackToKill != CreatureID::NONE) { - out.replaceCreatureName(stackToKill); + out.replaceNamePlural(stackToKill); out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); } } @@ -392,15 +392,15 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi if(missionType == "Hero") { - ui32 temp; - handler.serializeId("hero", temp, 0); + HeroTypeID temp; + handler.serializeId("hero", temp, HeroTypeID::NONE); mission.heroes.emplace_back(temp); } if(missionType == "Player") { - ui32 temp; - handler.serializeId("player", temp, PlayerColor::NEUTRAL); + PlayerColor temp; + handler.serializeId("player", temp, PlayerColor::NEUTRAL); mission.players.emplace_back(temp); } } @@ -412,9 +412,9 @@ bool IQuestObject::checkQuest(const CGHeroInstance* h) const return quest->checkQuest(h); } -void IQuestObject::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const +void CGSeerHut::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const { - quest->getVisitText(text, components, FirstVisit, h); + quest->getVisitText(cb, text, components, FirstVisit, h); } void IQuestObject::afterAddToMapCommon(CMap * map) const @@ -429,9 +429,8 @@ void CGSeerHut::setObjToKill() if(getCreatureToKill(true)) { - quest->stackToKill = getCreatureToKill(false)->getStack(SlotID(0)); //FIXME: stacks tend to disappear (desync?) on server :? - assert(quest->stackToKill.type); - quest->stackToKill.count = 0; //no count in info window + quest->stackToKill = getCreatureToKill(false)->getCreature(); + assert(quest->stackToKill != CreatureID::NONE); quest->stackDirection = checkDirection(); } else if(getHeroToKill(true)) @@ -481,14 +480,14 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest-> questName, quest->missionState(2), quest->textOption).get()); } - quest->getCompletionText(configuration.onSelect); + quest->getCompletionText(cb, configuration.onSelect); for(auto & i : configuration.info) - quest->getCompletionText(i.message); + quest->getCompletionText(cb, i.message); } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const { - quest->getRolloverText(text, onHover);//TODO: simplify? + quest->getRolloverText(cb, text, onHover);//TODO: simplify? if(!onHover) text.replaceRawString(seerName); } @@ -513,18 +512,49 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const return hoverName; } -void CGSeerHut::setPropertyDer(ui8 what, ui32 val) +std::string CGSeerHut::getHoverText(const CGHeroInstance * hero) const +{ + return getHoverText(hero->getOwner()); +} + +std::string CGSeerHut::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} + +std::string CGSeerHut::getPopupText(const CGHeroInstance * hero) const +{ + return getHoverText(hero->getOwner()); +} + +std::vector CGSeerHut::getPopupComponents(PlayerColor player) const +{ + std::vector result; + if (quest->activeForPlayers.count(player)) + quest->mission.loadComponents(result, nullptr); + return result; +} + +std::vector CGSeerHut::getPopupComponents(const CGHeroInstance * hero) const +{ + std::vector result; + if (quest->activeForPlayers.count(hero->getOwner())) + quest->mission.loadComponents(result, hero); + return result; +} + +void CGSeerHut::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch(what) { - case CGSeerHut::SEERHUT_VISITED: + case ObjProperty::SEERHUT_VISITED: { - quest->activeForPlayers.emplace(val); + quest->activeForPlayers.emplace(identifier.as()); break; } - case CGSeerHut::SEERHUT_COMPLETE: + case ObjProperty::SEERHUT_COMPLETE: { - quest->isCompleted = val; + quest->isCompleted = identifier.getNum(); quest->activeForPlayers.clear(); break; } @@ -536,7 +566,7 @@ void CGSeerHut::newTurn(CRandomGenerator & rand) const CRewardableObject::newTurn(rand); if(quest->lastDay >= 0 && quest->lastDay <= cb->getDate() - 1) //time is up { - cb->setObjProperty (id, CGSeerHut::SEERHUT_COMPLETE, true); + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, true); } } @@ -551,7 +581,7 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const if(firstVisit) { - cb->setObjProperty(id, CGSeerHut::SEERHUT_VISITED, h->getOwner()); + cb->setObjPropertyID(id, ObjProperty::SEERHUT_VISITED, h->getOwner()); AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); @@ -582,7 +612,7 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const int CGSeerHut::checkDirection() const { - int3 cord = getCreatureToKill()->pos; + int3 cord = getCreatureToKill(false)->pos; if(static_cast(cord.x) / static_cast(cb->getMapSize().x) < 0.34) //north { if(static_cast(cord.y) / static_cast(cb->getMapSize().y) < 0.34) //northwest @@ -614,7 +644,7 @@ int CGSeerHut::checkDirection() const const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); + const CGObjectInstance *o = cb->getObj(quest->killTarget); if(allowNull && !o) return nullptr; return dynamic_cast(o); @@ -622,7 +652,7 @@ const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); + const CGObjectInstance *o = cb->getObj(quest->killTarget); if(allowNull && !o) return nullptr; return dynamic_cast(o); @@ -634,7 +664,7 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) if(answer) { quest->completeQuest(cb, hero); - cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, !quest->repeatedQuest); //mission complete + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, !quest->repeatedQuest); //mission complete } } @@ -724,7 +754,7 @@ void CGQuestGuard::init(CRandomGenerator & rand) configuration.info.push_back({}); configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - configuration.info.back().reward.removeObject = subID == 0 ? true : false; + configuration.info.back().reward.removeObject = subID.getNum() == 0 ? true : false; configuration.canRefuse = true; } @@ -733,7 +763,7 @@ void CGQuestGuard::onHeroVisit(const CGHeroInstance * h) const if(!quest->isCompleted) CGSeerHut::onHeroVisit(h); else - cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, false); + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, false); } bool CGQuestGuard::passableFor(PlayerColor color) const @@ -747,25 +777,9 @@ void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) quest->serializeJson(handler, "quest"); } -void CGKeys::reset() -{ - playerKeyMap.clear(); -} - -void CGKeys::setPropertyDer (ui8 what, ui32 val) //101-108 - enable key for player 1-8 -{ - if (what >= 101 && what <= (100 + PlayerColor::PLAYER_LIMIT_I)) - { - PlayerColor player(what-101); - playerKeyMap[player].insert(static_cast(val)); - } - else - logGlobal->error("Unexpected properties requested to set: what=%d, val=%d", static_cast(what), val); -} - bool CGKeys::wasMyColorVisited(const PlayerColor & player) const { - return playerKeyMap.count(player) && vstd::contains(playerKeyMap[player], subID); + return cb->getPlayerState(player)->visitedObjectsGlobal.count({Obj::KEYMASTER, subID}) != 0; } std::string CGKeys::getHoverText(PlayerColor player) const @@ -775,7 +789,7 @@ std::string CGKeys::getHoverText(PlayerColor player) const std::string CGKeys::getObjectName() const { - return VLC->generaltexth->tentColors[subID] + " " + CGObjectInstance::getObjectName(); + return VLC->generaltexth->tentColors[subID.getNum()] + " " + CGObjectInstance::getObjectName(); } bool CGKeymasterTent::wasVisited (PlayerColor player) const @@ -788,7 +802,11 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const int txt_id; if (!wasMyColorVisited (h->getOwner()) ) { - cb->setObjProperty(id, h->tempOwner.getNum()+101, subID); + ChangeObjectVisitors cow; + cow.mode = ChangeObjectVisitors::VISITOR_GLOBAL; + cow.hero = h->id; + cow.object = id; + cb->sendAndApply(&cow); txt_id=19; } else @@ -810,7 +828,7 @@ void CGBorderGuard::getRolloverText(MetaString &text, bool onHover) const { if (!onHover) { - text.appendRawString(VLC->generaltexth->tentColors[subID]); + text.appendRawString(VLC->generaltexth->tentColors[subID.getNum()]); text.appendRawString(" "); text.appendRawString(VLC->objtypeh->getObjectName(Obj::KEYMASTER, subID)); } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 3f76d3550..5b9fb2220 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -40,12 +40,14 @@ public: // needed for messages / hover text ui8 textOption; ui8 completedOption; - CStackBasicDescriptor stackToKill; + CreatureID stackToKill; ui8 stackDirection; std::string heroName; //backup of hero name HeroTypeID heroPortrait; - MetaString firstVisitText, nextVisitText, completedText; + MetaString firstVisitText; + MetaString nextVisitText; + MetaString completedText; bool isCustomFirst; bool isCustomNext; bool isCustomComplete; @@ -54,11 +56,11 @@ public: static bool checkMissionArmy(const CQuest * q, const CCreatureSet * army); virtual bool checkQuest(const CGHeroInstance * h) const; //determines whether the quest is complete or not - virtual void getVisitText(MetaString &text, std::vector & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; - virtual void getCompletionText(MetaString &text) const; - virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry + virtual void getVisitText(IGameCallback * cb, MetaString &text, std::vector & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getCompletionText(IGameCallback * cb, MetaString &text) const; + virtual void getRolloverText (IGameCallback * cb, MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; - virtual void addTextReplacements(MetaString &out, std::vector & components) const; + virtual void addTextReplacements(IGameCallback * cb, MetaString &out, std::vector & components) const; virtual void addKillTargetReplacements(MetaString &out) const; void defineQuestName(); @@ -67,7 +69,7 @@ public: return (quest.qid == qid); } - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & qid; h & isCompleted; @@ -101,10 +103,10 @@ public: ///Information about quest should remain accessible even if IQuestObject removed from map ///All CQuest objects are freed in CMap destructor virtual ~IQuestObject() = default; - virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const = 0; virtual bool checkQuest (const CGHeroInstance * h) const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & quest; } @@ -115,34 +117,39 @@ protected: class DLL_LINKAGE CGSeerHut : public CRewardableObject, public IQuestObject { public: + using CRewardableObject::CRewardableObject; + std::string seerName; void initObj(CRandomGenerator & rand) override; std::string getHoverText(PlayerColor player) const override; + std::string getHoverText(const CGHeroInstance * hero) const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; + std::vector getPopupComponents(const CGHeroInstance * hero) const override; void newTurn(CRandomGenerator & rand) const override; void onHeroVisit(const CGHeroInstance * h) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; virtual void init(CRandomGenerator & rand); int checkDirection() const; //calculates the region of map where monster is placed void setObjToKill(); //remember creatures / heroes to kill after they are initialized - const CGHeroInstance *getHeroToKill(bool allowNull = false) const; - const CGCreature *getCreatureToKill(bool allowNull = false) const; + const CGHeroInstance *getHeroToKill(bool allowNull) const; + const CGCreature *getCreatureToKill(bool allowNull) const; void getRolloverText (MetaString &text, bool onHover) const; void afterAddToMap(CMap * map) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); h & seerName; } protected: - static constexpr int SEERHUT_VISITED = 10; - static constexpr int SEERHUT_COMPLETE = 11; - - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; }; @@ -150,12 +157,14 @@ protected: class DLL_LINKAGE CGQuestGuard : public CGSeerHut { public: + using CGSeerHut::CGSeerHut; + void init(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; bool passableFor(PlayerColor color) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); } @@ -166,31 +175,28 @@ protected: class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards { public: - static std::map > playerKeyMap; //[players][keysowned] - //SubID 0 - lightblue, 1 - green, 2 - red, 3 - darkblue, 4 - brown, 5 - purple, 6 - white, 7 - black - - static void reset(); + using CGObjectInstance::CGObjectInstance; bool wasMyColorVisited(const PlayerColor & player) const; std::string getObjectName() const override; //depending on color std::string getHoverText(PlayerColor player) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); } -protected: - void setPropertyDer(ui8 what, ui32 val) override; }; class DLL_LINKAGE CGKeymasterTent : public CGKeys { public: + using CGKeys::CGKeys; + bool wasVisited (PlayerColor player) const override; void onHeroVisit(const CGHeroInstance * h) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); } @@ -199,6 +205,8 @@ public: class DLL_LINKAGE CGBorderGuard : public CGKeys, public IQuestObject { public: + using CGKeys::CGKeys; + void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; @@ -209,7 +217,7 @@ public: void afterAddToMap(CMap * map) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); @@ -219,11 +227,13 @@ public: class DLL_LINKAGE CGBorderGate : public CGBorderGuard { public: + using CGBorderGuard::CGBorderGuard; + void onHeroVisit(const CGHeroInstance * h) const override; bool passableFor(PlayerColor color) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); //need to serialize or object will be empty } diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 7b03c8093..314a9bd7a 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -91,7 +91,7 @@ std::vector CRewardableObject::loadComponents(const CGHeroInstance * if (rewardIndices.empty()) return result; - if (configuration.selectMode != Rewardable::SELECT_FIRST) + if (configuration.selectMode != Rewardable::SELECT_FIRST && rewardIndices.size() > 1) { for (auto index : rewardIndices) result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); @@ -143,15 +143,20 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const selectRewardWthMessage(h, rewards, configuration.onSelect); break; case Rewardable::SELECT_FIRST: // give first available - grantRewardWithMessage(h, rewards.front(), true); + if (configuration.canRefuse) + selectRewardWthMessage(h, { rewards.front() }, configuration.info.at(rewards.front()).message); + else + grantRewardWithMessage(h, rewards.front(), true); break; case Rewardable::SELECT_RANDOM: // give random - grantRewardWithMessage(h, *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()), true); - break; - case Rewardable::SELECT_ALL: // grant all possible - auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT); - grantAllRewardsWthMessage(h, rewards, true); + { + ui32 rewardIndex = *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()); + if (configuration.canRefuse) + selectRewardWthMessage(h, { rewardIndex }, configuration.info.at(rewardIndex).message); + else + grantRewardWithMessage(h, rewardIndex, true); break; + } } break; } @@ -205,7 +210,7 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const { - cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, true); + cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id); cb->sendAndApply(&cov); @@ -213,7 +218,7 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const { - cb->setObjProperty(id, ObjProperty::REWARD_SELECT, rewardID); + cb->setObjPropertyValue(id, ObjProperty::REWARD_SELECT, rewardID); grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero); // hero is not blocked by levelup dialog - grant remainer immediately @@ -337,7 +342,7 @@ std::string CRewardableObject::getDescriptionMessage(PlayerColor player, const C return configuration.description.toString(); auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty() && !configuration.info[0].description.empty()) + if (rewardIndices.empty() || !configuration.info[0].description.empty()) return configuration.info[0].description.toString(); if (!configuration.info[rewardIndices.front()].description.empty()) @@ -374,7 +379,7 @@ std::vector CRewardableObject::getPopupComponents(const CGHeroInstanc return getPopupComponentsImpl(hero->getOwner(), hero); } -void CRewardableObject::setPropertyDer(ui8 what, ui32 val) +void CRewardableObject::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { @@ -382,10 +387,10 @@ void CRewardableObject::setPropertyDer(ui8 what, ui32 val) initObj(cb->gameState()->getRandomGenerator()); break; case ObjProperty::REWARD_SELECT: - selectedReward = val; + selectedReward = identifier.getNum(); break; case ObjProperty::REWARD_CLEARED: - onceVisitableObjectCleared = val; + onceVisitableObjectCleared = identifier.getNum(); break; } } @@ -396,11 +401,11 @@ void CRewardableObject::newTurn(CRandomGenerator & rand) const { if (configuration.resetParameters.rewards) { - cb->setObjProperty(id, ObjProperty::REWARD_RANDOMIZE, 0); + cb->setObjPropertyValue(id, ObjProperty::REWARD_RANDOMIZE, 0); } if (configuration.resetParameters.visitors) { - cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, false); + cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, false); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_CLEAR, id); cb->sendAndApply(&cov); } @@ -409,10 +414,11 @@ void CRewardableObject::newTurn(CRandomGenerator & rand) const void CRewardableObject::initObj(CRandomGenerator & rand) { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } -CRewardableObject::CRewardableObject() +CRewardableObject::CRewardableObject(IGameCallback *cb) + :CArmedInstance(cb) {} void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler) diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index ed5673e49..1a7cea319 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -67,9 +67,9 @@ public: void initObj(CRandomGenerator & rand) override; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; - CRewardableObject(); + CRewardableObject(IGameCallback *cb); std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; @@ -80,7 +80,7 @@ public: std::vector getPopupComponents(PlayerColor player) const override; std::vector getPopupComponents(const CGHeroInstance * hero) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); diff --git a/lib/mapObjects/IMarket.cpp b/lib/mapObjects/IMarket.cpp index d37662028..06fb94f1e 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -96,14 +96,14 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) case EMarketMode::CREATURE_EXP: { val1 = 1; - val2 = (VLC->creh->objects[id1]->getAIValue() / 40) * 5; + val2 = (CreatureID(id1).toEntity(VLC)->getAIValue() / 40) * 5; } break; case EMarketMode::ARTIFACT_EXP: { val1 = 1; - int givenClass = VLC->arth->objects[id1]->getArtClassSerial(); + int givenClass = ArtifactID(id1).toArtifact()->getArtClassSerial(); if(givenClass < 0 || givenClass > 3) { val2 = 0; @@ -140,34 +140,20 @@ int IMarket::availableUnits(EMarketMode mode, int marketItemSerial) const } } -std::vector IMarket::availableItemsIds(EMarketMode mode) const +std::vector IMarket::availableItemsIds(EMarketMode mode) const { - std::vector ret; + std::vector ret; switch(mode) { case EMarketMode::RESOURCE_RESOURCE: case EMarketMode::ARTIFACT_RESOURCE: case EMarketMode::CREATURE_RESOURCE: - for (int i = 0; i < 7; i++) - ret.push_back(i); + for (auto res : GameResID::ALL_RESOURCES()) + ret.push_back(res); } return ret; } -const IMarket * IMarket::castFrom(const CGObjectInstance *obj, bool verbose) -{ - auto * imarket = dynamic_cast(obj); - if(verbose && !imarket) - { - logGlobal->error("Cannot cast to IMarket"); - if(obj) - { - logGlobal->error("Object type %s", obj->typeName); - } - } - return imarket; -} - IMarket::IMarket() { } diff --git a/lib/mapObjects/IMarket.h b/lib/mapObjects/IMarket.h index f279ffe6d..caf3e4cb4 100644 --- a/lib/mapObjects/IMarket.h +++ b/lib/mapObjects/IMarket.h @@ -9,7 +9,8 @@ */ #pragma once -#include "../GameConstants.h" +#include "../networkPacks/TradeItem.h" +#include "../constants/Enumerations.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,12 +25,10 @@ public: virtual int getMarketEfficiency() const = 0; virtual bool allowsTrade(EMarketMode mode) const; virtual int availableUnits(EMarketMode mode, int marketItemSerial) const; //-1 if unlimited - virtual std::vector availableItemsIds(EMarketMode mode) const; + virtual std::vector availableItemsIds(EMarketMode mode) const; bool getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const; //val1 - how many units of id1 player has to give to receive val2 units std::vector availableModes() const; - - static const IMarket *castFrom(const CGObjectInstance *obj, bool verbose = true); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index 268c99c4c..e7dac1ca0 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -21,8 +21,6 @@ VCMI_LIB_NAMESPACE_BEGIN -IGameCallback * IObjectInterface::cb = nullptr; - void IObjectInterface::showInfoDialog(const ui32 txtID, const ui16 soundID, EInfoWindowMode mode) const { InfoWindow iw; @@ -30,7 +28,7 @@ void IObjectInterface::showInfoDialog(const ui32 txtID, const ui16 soundID, EInf iw.player = getOwner(); iw.type = mode; iw.text.appendLocalString(EMetaText::ADVOB_TXT,txtID); - IObjectInterface::cb->sendAndApply(&iw); + cb->sendAndApply(&iw); } ///IObjectInterface @@ -46,7 +44,10 @@ void IObjectInterface::newTurn(CRandomGenerator & rand) const void IObjectInterface::initObj(CRandomGenerator & rand) {} -void IObjectInterface::setProperty( ui8 what, ui32 val ) +void IObjectInterface::pickRandomObject(CRandomGenerator & rand) +{} + +void IObjectInterface::setProperty(ObjProperty what, ObjPropertyID identifier) {} bool IObjectInterface::wasVisited (PlayerColor player) const @@ -86,11 +87,23 @@ int3 IBoatGenerator::bestLocation() const int3 targetTile = getObject()->visitablePos() + offset; const TerrainTile *tile = getObject()->cb->getTile(targetTile, false); - if(tile) //tile is in the map + if(!tile) + continue; // tile not visible / outside the map + + if(!tile->terType->isWater()) + continue; + + if (tile->blocked) { - if(tile->terType->isWater() && (!tile->blocked || tile->blockingObjects.front()->ID == Obj::BOAT)) //and is water and is not blocked or is blocked by boat - return targetTile; + bool hasBoat = false; + for (auto const * object : tile->blockingObjects) + if (object->ID == Obj::BOAT || object->ID == Obj::HERO) + hasBoat = true; + + if (!hasBoat) + continue; // tile is blocked, but not by boat -> check next potential position } + return targetTile; } return int3 (-1,-1,-1); } @@ -102,14 +115,14 @@ IBoatGenerator::EGeneratorState IBoatGenerator::shipyardStatus() const if(!tile.valid()) return TILE_BLOCKED; //no available water - const TerrainTile *t = IObjectInterface::cb->getTile(tile); + const TerrainTile *t = getObject()->cb->getTile(tile); if(!t) return TILE_BLOCKED; //no available water if(t->blockingObjects.empty()) return GOOD; //OK - if(t->blockingObjects.front()->ID == Obj::BOAT) + if(t->blockingObjects.front()->ID == Obj::BOAT || t->blockingObjects.front()->ID == Obj::HERO) return BOAT_ALREADY_BUILT; //blocked with boat return TILE_BLOCKED; //blocked @@ -143,9 +156,4 @@ void IShipyard::getBoatCost(TResources & cost) const cost[EGameResID::GOLD] = 1000; } -const IShipyard * IShipyard::castFrom( const CGObjectInstance *obj ) -{ - return dynamic_cast(obj); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index 1ed826903..12d2baeb1 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -10,6 +10,9 @@ #pragma once #include "../networkPacks/EInfoWindowMode.h" +#include "../networkPacks/ObjProperty.h" +#include "../constants/EntityIdentifiers.h" +#include "../GameCallbackHolder.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,15 +29,15 @@ class int3; class MetaString; class PlayerColor; -class DLL_LINKAGE IObjectInterface +class DLL_LINKAGE IObjectInterface : public GameCallbackHolder { public: - static IGameCallback *cb; + using GameCallbackHolder::GameCallbackHolder; virtual ~IObjectInterface() = default; - virtual int32_t getObjGroupIndex() const = 0; - virtual int32_t getObjTypeIndex() const = 0; + virtual MapObjectID getObjGroupIndex() const = 0; + virtual MapObjectSubID getObjTypeIndex() const = 0; virtual PlayerColor getOwner() const = 0; virtual int3 visitablePos() const = 0; @@ -44,7 +47,8 @@ public: virtual void onHeroLeave(const CGHeroInstance * h) const; virtual void newTurn(CRandomGenerator & rand) const; virtual void initObj(CRandomGenerator & rand); //synchr - virtual void setProperty(ui8 what, ui32 val);//synchr + virtual void pickRandomObject(CRandomGenerator & rand); + virtual void setProperty(ObjProperty what, ObjPropertyID identifier);//synchr //Called when queries created DURING HERO VISIT are resolved //First parameter is always hero that visited object and triggered the query @@ -63,7 +67,7 @@ public: static void preInit(); //called before objs receive their initObj static void postInit();//called after objs receive their initObj - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { logGlobal->error("IObjectInterface serialized, unexpected, should not happen!"); } @@ -97,8 +101,6 @@ class DLL_LINKAGE IShipyard : public IBoatGenerator { public: virtual void getBoatCost(ResourceSet & cost) const; - - static const IShipyard *castFrom(const CGObjectInstance *obj); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 0bfb191a7..943ba9356 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -11,7 +11,10 @@ #include "StdInc.h" #include "MiscObjects.h" +#include "../ArtifactUtils.h" +#include "../bonuses/Propagators.h" #include "../constants/StringConstants.h" +#include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../CSkillHandler.h" @@ -31,10 +34,6 @@ VCMI_LIB_NAMESPACE_BEGIN -std::map > CGMagi::eyelist; -ui8 CGObelisk::obeliskCount = 0; //how many obelisks are on map -std::map CGObelisk::visited; //map: team_id => how many obelisks has been visited - ///helpers static std::string visitedTxt(const bool visited) { @@ -42,10 +41,10 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } -void CTeamVisited::setPropertyDer(ui8 what, ui32 val) +void CTeamVisited::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { - if(what == CTeamVisited::OBJPROP_VISITED) - players.insert(PlayerColor(val)); + if(what == ObjProperty::VISITED) + players.insert(identifier.as()); } bool CTeamVisited::wasVisited(PlayerColor player) const @@ -85,7 +84,7 @@ void CGMine::onHeroVisit( const CGHeroInstance * h ) const { BlockingDialog ynd(true,false); ynd.player = h->tempOwner; - ynd.text.appendLocalString(EMetaText::ADVOB_TXT, subID == 7 ? 84 : 187); + ynd.text.appendLocalString(EMetaText::ADVOB_TXT, isAbandoned() ? 84 : 187); cb->showBlockingDialog(&ynd); return; } @@ -115,23 +114,39 @@ void CGMine::initObj(CRandomGenerator & rand) putStack(SlotID(0), troglodytes); assert(!abandonedMineResources.empty()); - producedResource = *RandomGeneratorUtil::nextItem(abandonedMineResources, rand); + if (!abandonedMineResources.empty()) + { + producedResource = *RandomGeneratorUtil::nextItem(abandonedMineResources, rand); + } + else + { + logGlobal->error("Abandoned mine at (%s) has no valid resource candidates!", pos.toString()); + producedResource = GameResID::GOLD; + } } else { - producedResource = GameResID(subID); + producedResource = GameResID(getObjTypeIndex().getNum()); } producedQuantity = defaultResProduction(); } bool CGMine::isAbandoned() const { - return (subID >= 7); + return subID.getNum() >= 7; +} + +ResourceSet CGMine::dailyIncome() const +{ + ResourceSet result; + result[producedResource] += defaultResProduction(); + + return result; } std::string CGMine::getObjectName() const { - return VLC->generaltexth->translate("core.minename", subID); + return VLC->generaltexth->translate("core.minename", getObjTypeIndex()); } std::string CGMine::getHoverText(PlayerColor player) const @@ -139,7 +154,7 @@ std::string CGMine::getHoverText(PlayerColor player) const std::string hoverName = CArmedInstance::getHoverText(player); if (tempOwner != PlayerColor::NEUTRAL) - hoverName += "\n(" + VLC->generaltexth->restypes[producedResource] + ")"; + hoverName += "\n(" + VLC->generaltexth->restypes[producedResource.getNum()] + ")"; if(stacksCount()) { @@ -159,9 +174,9 @@ void CGMine::flagMine(const PlayerColor & player) const InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.soundID = soundBase::FLAGMINE; - iw.text.appendLocalString(EMetaText::MINE_EVNTS, producedResource); //not use subID, abandoned mines uses default mine texts + iw.text.appendTextID(TextIdentifier("core.mineevnt", producedResource.getNum()).get()); //not use subID, abandoned mines uses default mine texts iw.player = player; - iw.components.emplace_back(Component::EComponentType::RESOURCE, producedResource, producedQuantity, -1); + iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, producedQuantity); cb->showInfoDialog(&iw); } @@ -200,18 +215,15 @@ void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) con void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) { CArmedInstance::serializeJsonOptions(handler); - + serializeJsonOwner(handler); if(isAbandoned()) { if(handler.saving) { - JsonNode node(JsonNode::JsonType::DATA_VECTOR); - for(auto const & resID : abandonedMineResources) - { - JsonNode one(JsonNode::JsonType::DATA_STRING); - one.String() = GameConstants::RESOURCE_NAMES[resID]; - node.Vector().push_back(one); - } + JsonNode node; + for(const auto & resID : abandonedMineResources) + node.Vector().emplace_back(GameConstants::RESOURCE_NAMES[resID.getNum()]); + handler.serializeRaw("possibleResources", node, std::nullopt); } else @@ -231,15 +243,28 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) } } } - else - { - serializeJsonOwner(handler); - } +} + +GameResID CGResource::resourceID() const +{ + return getObjTypeIndex().getNum(); } std::string CGResource::getHoverText(PlayerColor player) const { - return VLC->generaltexth->restypes[subID]; + return VLC->generaltexth->restypes[resourceID().getNum()]; +} + +void CGResource::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == Obj::RESOURCE || ID == Obj::RANDOM_RESOURCE); + + if (ID == Obj::RANDOM_RESOURCE) + { + ID = Obj::RESOURCE; + subID = rand.nextInt(EGameResID::WOOD, EGameResID::GOLD); + setType(ID, subID); + } } void CGResource::initObj(CRandomGenerator & rand) @@ -248,7 +273,7 @@ void CGResource::initObj(CRandomGenerator & rand) if(amount == CGResource::RANDOM_AMOUNT) { - switch(static_cast(subID)) + switch(resourceID().toEnum()) { case EGameResID::GOLD: amount = rand.nextInt(5, 10) * 100; @@ -285,7 +310,7 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const void CGResource::collectRes(const PlayerColor & player) const { - cb->giveResource(player, static_cast(subID), amount); + cb->giveResource(player, resourceID(), amount); InfoWindow sii; sii.player = player; if(!message.empty()) @@ -297,9 +322,9 @@ void CGResource::collectRes(const PlayerColor & player) const { sii.type = EInfoWindowMode::INFO; sii.text.appendLocalString(EMetaText::ADVOB_TXT,113); - sii.text.replaceLocalString(EMetaText::RES_NAMES, subID); + sii.text.replaceName(resourceID()); } - sii.components.emplace_back(Component::EComponentType::RESOURCE,subID,amount,0); + sii.components.emplace_back(ComponentType::RESOURCE, resourceID(), amount); sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6); cb->showInfoDialog(&sii); cb->removeObject(this, player); @@ -442,14 +467,14 @@ void CGTeleport::addToChannel(std::map & IDs, int SubID) const +TeleportChannelID CGMonolith::findMeChannel(const std::vector & IDs, MapObjectSubID SubID) const { for(auto obj : cb->gameState()->map->objects) { if(!obj) continue; - const auto * teleportObj = dynamic_cast(cb->getObj(obj->id)); + const auto * teleportObj = dynamic_cast(cb->getObj(obj->id)); if(teleportObj && vstd::contains(IDs, teleportObj->ID) && teleportObj->subID == SubID) return teleportObj->channel; } @@ -507,7 +532,7 @@ void CGMonolith::initObj(CRandomGenerator & rand) { std::vector IDs; IDs.push_back(ID); - switch(ID) + switch(ID.toEnum()) { case Obj::MONOLITH_ONE_WAY_ENTRANCE: type = ENTRANCE; @@ -553,13 +578,13 @@ void CGSubterraneanGate::initObj(CRandomGenerator & rand) type = BOTH; } -void CGSubterraneanGate::postInit() //matches subterranean gates into pairs +void CGSubterraneanGate::postInit(IGameCallback * cb) //matches subterranean gates into pairs { //split on underground and surface gates std::vector gatesSplit[2]; //surface and underground gates for(auto & obj : cb->gameState()->map->objects) { - if(!obj) // FIXME: Find out why there are nullptr objects right after initialization + if(!obj) continue; auto * hlp = dynamic_cast(cb->gameState()->getObjInstance(obj->id)); @@ -641,7 +666,7 @@ void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::ADVOB_TXT, 168); - iw.components.emplace_back(CStackBasicDescriptor(h->getCreature(targetstack), -countToTake)); + iw.components.emplace_back(ComponentType::CREATURE, h->getCreature(targetstack)->getId(), -countToTake); cb->showInfoDialog(&iw); cb->changeStackCount(StackLocation(h, targetstack), -countToTake); } @@ -688,6 +713,44 @@ bool CGWhirlpool::isProtected(const CGHeroInstance * h) || (h->stacksCount() == 1 && h->Slots().begin()->second->count == 1); } +ArtifactID CGArtifact::getArtifact() const +{ + if(ID == Obj::SPELL_SCROLL) + return ArtifactID::SPELL_SCROLL; + else + return getObjTypeIndex().getNum(); +} + +void CGArtifact::pickRandomObject(CRandomGenerator & rand) +{ + switch(ID.toEnum()) + { + case MapObjectID::RANDOM_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC); + break; + case MapObjectID::RANDOM_TREASURE_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE); + break; + case MapObjectID::RANDOM_MINOR_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_MINOR); + break; + case MapObjectID::RANDOM_MAJOR_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_MAJOR); + break; + case MapObjectID::RANDOM_RELIC_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_RELIC); + break; + } + + if (ID != MapObjectID::SPELL_SCROLL && ID != MapObjectID::ARTIFACT) + { + ID = MapObjectID::ARTIFACT; + setType(ID, subID); + } + else if (ID != MapObjectID::SPELL_SCROLL) + ID = MapObjectID::ARTIFACT; +} + void CGArtifact::initObj(CRandomGenerator & rand) { blockVisit = true; @@ -700,20 +763,45 @@ void CGArtifact::initObj(CRandomGenerator & rand) storedArtifact = a; } if(!storedArtifact->artType) - storedArtifact->setType(VLC->arth->objects[subID]); + storedArtifact->setType(getArtifact().toArtifact()); } if(ID == Obj::SPELL_SCROLL) subID = 1; assert(storedArtifact->artType); - assert(storedArtifact->getParentNodes().size()); + assert(!storedArtifact->getParentNodes().empty()); //assert(storedArtifact->artType->id == subID); //this does not stop desync } std::string CGArtifact::getObjectName() const { - return VLC->artifacts()->getByIndex(subID)->getNameTranslated(); + return VLC->artifacts()->getById(getArtifact())->getNameTranslated(); +} + +std::string CGArtifact::getPopupText(PlayerColor player) const +{ + if (settings["general"]["enableUiEnhancements"].Bool()) + { + std::string description = VLC->artifacts()->getById(getArtifact())->getDescriptionTranslated(); + if (getArtifact() == ArtifactID::SPELL_SCROLL) + ArtifactUtils::insertScrrollSpellName(description, SpellID::NONE); // erase text placeholder + return description; + } + else + return getObjectName(); +} + +std::string CGArtifact::getPopupText(const CGHeroInstance * hero) const +{ + return getPopupText(hero->getOwner()); +} + +std::vector CGArtifact::getPopupComponents(PlayerColor player) const +{ + return { + Component(ComponentType::ARTIFACT, getArtifact()) + }; } void CGArtifact::onHeroVisit(const CGHeroInstance * h) const @@ -726,27 +814,27 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const if(storedArtifact->artType->canBePutAt(h)) { - switch (ID) + switch (ID.toEnum()) { case Obj::ARTIFACT: { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, subID, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, getArtifact()); if(!message.empty()) iw.text = message; else - iw.text.appendLocalString(EMetaText::ART_EVNTS, subID); + iw.text.appendTextID(getArtifact().toArtifact()->getEventTextID()); } break; case Obj::SPELL_SCROLL: { - int spellID = storedArtifact->getScrollSpellID(); - iw.components.emplace_back(Component::EComponentType::SPELL, spellID, 0, 0); + SpellID spell = storedArtifact->getScrollSpellID(); + iw.components.emplace_back(ComponentType::SPELL, spell); if(!message.empty()) iw.text = message; else { iw.text.appendLocalString(EMetaText::ADVOB_TXT,135); - iw.text.replaceLocalString(EMetaText::SPELL_NAME, spellID); + iw.text.replaceName(spell); } } break; @@ -761,7 +849,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const } else { - switch(ID) + switch(ID.toEnum()) { case Obj::ARTIFACT: { @@ -799,7 +887,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const void CGArtifact::pick(const CGHeroInstance * h) const { - if(cb->giveHeroArtifact(h, storedArtifact, ArtifactPosition::FIRST_AVAILABLE)) + if(cb->putArtifact(ArtifactLocation(h->id, ArtifactPosition::FIRST_AVAILABLE), storedArtifact)) cb->removeObject(this, h->getOwner()); } @@ -838,7 +926,7 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) if(handler.saving && ID == Obj::SPELL_SCROLL) { - const std::shared_ptr b = storedArtifact->getBonusLocalFirst(Selector::type()(BonusType::SPELL)); + const auto & b = storedArtifact->getFirstBonus(Selector::type()(BonusType::SPELL)); SpellID spellId(b->subtype.as()); handler.serializeId("spell", spellId, SpellID::NONE); @@ -852,7 +940,7 @@ void CGSignBottle::initObj(CRandomGenerator & rand) { auto vector = VLC->generaltexth->findStringsWithPrefix("core.randsign"); std::string messageIdentifier = *RandomGeneratorUtil::nextItem(vector, rand); - message.appendTextID(TextIdentifier("core", "randsign", messageIdentifier).get()); + message.appendTextID(messageIdentifier); } if(ID == Obj::OCEAN_BOTTLE) @@ -920,26 +1008,44 @@ void CGGarrison::serializeJsonOptions(JsonSerializeFormat& handler) CArmedInstance::serializeJsonOptions(handler); } -void CGMagi::reset() +void CGGarrison::initObj(CRandomGenerator &rand) { - eyelist.clear(); + if(this->subID == MapObjectSubID::decode(this->ID, "antiMagic")) + addAntimagicGarrisonBonus(); +} + +void CGGarrison::addAntimagicGarrisonBonus() +{ + auto bonus = std::make_shared(); + bonus->type = BonusType::BLOCK_ALL_MAGIC; + bonus->source = BonusSource::OBJECT_TYPE; + bonus->sid = BonusSourceID(this->ID); + bonus->propagator = std::make_shared(CBonusSystemNode::BATTLE); + bonus->duration = BonusDuration::PERMANENT; + this->addNewBonus(bonus); } void CGMagi::initObj(CRandomGenerator & rand) { if (ID == Obj::EYE_OF_MAGI) - { blockVisit = true; - eyelist[subID].push_back(id); - } } + void CGMagi::onHeroVisit(const CGHeroInstance * h) const { if (ID == Obj::HUT_OF_MAGI) { h->showInfoDialog(61); - if (!eyelist[subID].empty()) + std::vector eyes; + + for (auto object : cb->gameState()->map->objects) + { + if (object && object->ID == Obj::EYE_OF_MAGI && object->subID == this->subID) + eyes.push_back(object); + } + + if (!eyes.empty()) { CenterView cv; cv.player = h->tempOwner; @@ -950,10 +1056,8 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const fw.mode = ETileVisibility::REVEALED; fw.waitForDialogs = true; - for(const auto & it : eyelist[subID]) + for(const auto & eye : eyes) { - const CGObjectInstance *eye = cb->getObj(it); - cb->getTilesInRange (fw.tiles, eye->pos, 10, ETileVisibility::HIDDEN, h->tempOwner); cb->sendAndApply(&fw); cv.pos = eye->pos; @@ -971,7 +1075,8 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const } } -CGBoat::CGBoat() +CGBoat::CGBoat(IGameCallback * cb) + : CGObjectInstance(cb) { hero = nullptr; direction = 4; @@ -1028,7 +1133,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const xp = h->calculateXp(static_cast(xp)); iw.text.appendLocalString(EMetaText::ADVOB_TXT,132); iw.text.replaceNumber(static_cast(xp)); - cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, xp, false); + cb->giveExperience(h, xp); } else { @@ -1113,13 +1218,13 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const cb->sendAndApply(&iw); // increment general visited obelisks counter - cb->setObjProperty(id, CGObelisk::OBJPROP_INC, team.getNum()); + cb->setObjPropertyID(id, ObjProperty::OBELISK_VISITED, team); cb->showObjectWindow(this, EOpenWindowMode::PUZZLE_MAP, h, false); // mark that particular obelisk as visited for all players in the team for(const auto & color : ts->players) { - cb->setObjProperty(id, CGObelisk::OBJPROP_VISITED, color.getNum()); + cb->setObjPropertyID(id, ObjProperty::VISITED, color); } } else @@ -1132,13 +1237,7 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const void CGObelisk::initObj(CRandomGenerator & rand) { - obeliskCount++; -} - -void CGObelisk::reset() -{ - obeliskCount = 0; - visited.clear(); + cb->gameState()->map->obeliskCount++; } std::string CGObelisk::getHoverText(PlayerColor player) const @@ -1146,25 +1245,25 @@ std::string CGObelisk::getHoverText(PlayerColor player) const return getObjectName() + " " + visitedTxt(wasVisited(player)); } -void CGObelisk::setPropertyDer( ui8 what, ui32 val ) +void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch(what) { - case CGObelisk::OBJPROP_INC: + case ObjProperty::OBELISK_VISITED: { - auto progress = ++visited[TeamID(val)]; - logGlobal->debug("Player %d: obelisk progress %d / %d", val, static_cast(progress) , static_cast(obeliskCount)); + auto progress = ++cb->gameState()->map->obelisksVisited[identifier.as()]; + logGlobal->debug("Player %d: obelisk progress %d / %d", identifier.getNum(), static_cast(progress) , static_cast(cb->gameState()->map->obeliskCount)); - if(progress > obeliskCount) + if(progress > cb->gameState()->map->obeliskCount) { - logGlobal->error("Visited %d of %d", static_cast(progress), obeliskCount); - throw std::runtime_error("internal error"); + logGlobal->error("Visited %d of %d", static_cast(progress), cb->gameState()->map->obeliskCount); + throw std::runtime_error("Player visited more obelisks than present on map!"); } break; } default: - CTeamVisited::setPropertyDer(what, val); + CTeamVisited::setPropertyDer(what, identifier); break; } } @@ -1181,7 +1280,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const if(oldOwner.isValidPlayer()) //remove bonus from old owner { RemoveBonus rb(GiveBonus::ETarget::PLAYER); - rb.whoID = oldOwner.getNum(); + rb.whoID = oldOwner; rb.source = BonusSource::OBJECT_INSTANCE; rb.id = BonusSourceID(id); cb->sendAndApply(&rb); @@ -1203,7 +1302,7 @@ void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const GiveBonus gb(GiveBonus::ETarget::PLAYER); gb.bonus.type = BonusType::MOVEMENT; gb.bonus.val = 500; - gb.id = player.getNum(); + gb.id = player; gb.bonus.duration = BonusDuration::PERMANENT; gb.bonus.source = BonusSource::OBJECT_INSTANCE; gb.bonus.sid = BonusSourceID(id); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 53021a4a9..6b74fc402 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -24,31 +24,33 @@ using TTeleportExitsList = std::vector>; class DLL_LINKAGE CTeamVisited: public CGObjectInstance { public: + using CGObjectInstance::CGObjectInstance; + std::set players; //players that visited this object bool wasVisited (const CGHeroInstance * h) const override; bool wasVisited(PlayerColor player) const override; bool wasVisited(const TeamID & team) const; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & players; } - - static constexpr int OBJPROP_VISITED = 10; }; class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles { public: + using CGObjectInstance::CGObjectInstance; + MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & message; @@ -60,24 +62,30 @@ protected: class DLL_LINKAGE CGGarrison : public CArmedInstance { public: + using CArmedInstance::CArmedInstance; + bool removableUnits; + void initObj(CRandomGenerator &rand) override; bool passableFor(PlayerColor color) const override; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & removableUnits; } protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; + void addAntimagicGarrisonBonus(); }; class DLL_LINKAGE CGArtifact : public CArmedInstance { public: + using CArmedInstance::CArmedInstance; + CArtifactInstance * storedArtifact = nullptr; MetaString message; @@ -86,14 +94,20 @@ public: void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; std::string getObjectName() const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; void pick( const CGHeroInstance * h ) const; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void afterAddToMap(CMap * map) override; BattleField getBattlefield() const override; - template void serialize(Handler &h, const int version) + ArtifactID getArtifact() const; + + template void serialize(Handler &h) { h & static_cast(*this); h & message; @@ -106,6 +120,8 @@ protected: class DLL_LINKAGE CGResource : public CArmedInstance { public: + using CArmedInstance::CArmedInstance; + static constexpr ui32 RANDOM_AMOUNT = 0; ui32 amount = RANDOM_AMOUNT; //0 if random @@ -113,13 +129,15 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; std::string getHoverText(PlayerColor player) const override; void collectRes(const PlayerColor & player) const; + GameResID resourceID() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & amount; @@ -137,8 +155,11 @@ public: std::set abandonedMineResources; bool isAbandoned() const; + ResourceSet dailyIncome() const; private: + using CArmedInstance::CArmedInstance; + void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; @@ -151,7 +172,7 @@ private: std::string getHoverText(PlayerColor player) const override; public: - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & producedResource; @@ -172,7 +193,7 @@ struct DLL_LINKAGE TeleportChannel std::vector exits; EPassability passability = EPassability::UNKNOWN; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & entrances; h & exits; @@ -195,6 +216,8 @@ protected: std::vector getAllExits(bool excludeCurrent = false) const; public: + using CGObjectInstance::CGObjectInstance; + TeleportChannelID channel; bool isEntrance() const; @@ -209,7 +232,7 @@ public: static std::vector getPassableExits(CGameState * gs, const CGHeroInstance * h, std::vector exits); static bool isExitPassable(CGameState * gs, const CGHeroInstance * h, const CGObjectInstance * obj); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & type; h & channel; @@ -219,7 +242,7 @@ public: class DLL_LINKAGE CGMonolith : public CGTeleport { - TeleportChannelID findMeChannel(const std::vector & IDs, int SubID) const; + TeleportChannelID findMeChannel(const std::vector & IDs, MapObjectSubID SubID) const; protected: void onHeroVisit(const CGHeroInstance * h) const override; @@ -227,7 +250,9 @@ protected: void initObj(CRandomGenerator & rand) override; public: - template void serialize(Handler &h, const int version) + using CGTeleport::CGTeleport; + + template void serialize(Handler &h) { h & static_cast(*this); } @@ -239,9 +264,11 @@ class DLL_LINKAGE CGSubterraneanGate : public CGMonolith void initObj(CRandomGenerator & rand) override; public: - static void postInit(); + using CGMonolith::CGMonolith; - template void serialize(Handler &h, const int version) + static void postInit(IGameCallback * cb); + + template void serialize(Handler &h) { h & static_cast(*this); } @@ -254,7 +281,9 @@ class DLL_LINKAGE CGWhirlpool : public CGMonolith static bool isProtected( const CGHeroInstance * h ); public: - template void serialize(Handler &h, const int version) + using CGMonolith::CGMonolith; + + template void serialize(Handler &h) { h & static_cast(*this); } @@ -263,11 +292,13 @@ public: class DLL_LINKAGE CGSirens : public CGObjectInstance { public: + using CGObjectInstance::CGObjectInstance; + void onHeroVisit(const CGHeroInstance * h) const override; std::string getHoverText(const CGHeroInstance * hero) const override; void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); } @@ -276,6 +307,8 @@ public: class DLL_LINKAGE CGBoat : public CGObjectInstance, public CBonusSystemNode { public: + using CGObjectInstance::CGObjectInstance; + ui8 direction; const CGHeroInstance *hero; //hero on board bool onboardAssaultAllowed; //if true, hero can attack units from transport @@ -287,10 +320,10 @@ public: AnimationPath overlayAnimation; //waves animations std::array flagAnimations; - CGBoat(); + CGBoat(IGameCallback * cb); bool isCoastVisitable() const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); h & static_cast(*this); @@ -318,7 +351,9 @@ protected: BoatId getBoatType() const override; public: - template void serialize(Handler & h, const int version) + using CGObjectInstance::CGObjectInstance; + + template void serialize(Handler & h) { h & static_cast(*this); h & createdBoat; @@ -331,14 +366,12 @@ protected: class DLL_LINKAGE CGMagi : public CGObjectInstance { public: - static std::map > eyelist; //[subID][id], supports multiple sets as in H5 - - static void reset(); + using CGObjectInstance::CGObjectInstance; void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); } @@ -347,35 +380,36 @@ public: class DLL_LINKAGE CGDenOfthieves : public CGObjectInstance { void onHeroVisit(const CGHeroInstance * h) const override; +public: + using CGObjectInstance::CGObjectInstance; }; class DLL_LINKAGE CGObelisk : public CTeamVisited { public: - static constexpr int OBJPROP_INC = 20; - static ui8 obeliskCount; //how many obelisks are on map - static std::map visited; //map: team_id => how many obelisks has been visited + using CTeamVisited::CTeamVisited; void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; std::string getHoverText(PlayerColor player) const override; - static void reset(); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; }; class DLL_LINKAGE CGLighthouse : public CGObjectInstance { public: + using CGObjectInstance::CGObjectInstance; + void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & static_cast(*this); } @@ -388,9 +422,9 @@ protected: class DLL_LINKAGE CGTerrainPatch : public CGObjectInstance { public: - CGTerrainPatch() = default; + using CGObjectInstance::CGObjectInstance; - virtual bool isTile2Terrain() const override + bool isTile2Terrain() const override { return true; } @@ -407,7 +441,9 @@ protected: void fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const override; public: - template void serialize(Handler &h, const int version) + using CGObjectInstance::CGObjectInstance; + + template void serialize(Handler &h) { h & static_cast(*this); h & upgradeCostPercentage; diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index 3b623c76c..3b0e447f1 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -16,7 +16,6 @@ #include "../GameConstants.h" #include "../constants/StringConstants.h" #include "../CGeneralTextHandler.h" -#include "../JsonNode.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/CRewardableConstructor.h" @@ -180,7 +179,7 @@ void ObjectTemplate::readTxt(CLegacyConfigParser & parser) void ObjectTemplate::readMsk() { - ResourcePath resID(animationFile.getName(), EResType::MASK); + ResourcePath resID("Sprites/" + animationFile.getName(), EResType::MASK); if (CResourceHandler::get()->existsResource(resID)) { @@ -350,16 +349,12 @@ void ObjectTemplate::writeJson(JsonNode & node, const bool withTerrain) const if(withTerrain) { //assumed that ROCK and WATER terrains are not included - if(allowedTerrains.size() < (VLC->terrainTypeHandler->objects.size() - 2)) + if(allowedTerrains.size() < (VLC->terrainTypeHandler->size() - 2)) { JsonVector & data = node["allowedTerrains"].Vector(); for(auto type : allowedTerrains) - { - JsonNode value(JsonNode::JsonType::DATA_STRING); - value.String() = VLC->terrainTypeHandler->getById(type)->getJsonKey(); - data.push_back(value); - } + data.emplace_back(VLC->terrainTypeHandler->getById(type)->getJsonKey()); } } @@ -399,13 +394,11 @@ void ObjectTemplate::writeJson(JsonNode & node, const bool withTerrain) const for(size_t i=0; i < height; i++) { - JsonNode lineNode(JsonNode::JsonType::DATA_STRING); - - std::string & line = lineNode.String(); + std::string line; line.resize(width); for(size_t j=0; j < width; j++) line[j] = tileToChar(usedTiles[height - 1 - i][width - 1 - j]); - mask.push_back(lineNode); + mask.emplace_back(line); } if(printPriority != 0) diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 3f967d76d..0ceb409a2 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -43,8 +43,8 @@ class DLL_LINKAGE ObjectTemplate public: /// H3 ID/subID of this object - Obj id; - si32 subid; + MapObjectID id; + MapObjectSubID subid; /// print priority, objects with higher priority will be print first, below everything else si32 printPriority; /// animation file that should be used to display object @@ -80,7 +80,7 @@ public: bool isVisibleAt(si32 X, si32 Y) const; bool isBlockedAt(si32 X, si32 Y) const; - inline std::set getBlockedOffsets() const + inline const std::set & getBlockedOffsets() const { return blockedOffsets; }; @@ -156,7 +156,7 @@ private: void calculateTopVisibleOffset(); public: - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & usedTiles; h & allowedTerrains; @@ -164,7 +164,7 @@ public: h & animationFile; h & stringID; h & id; - h & subid; + subid.serializeIdentifier(h, id); h & printPriority; h & visitDir; h & editorAnimationFile; diff --git a/lib/mapping/CDrawRoadsOperation.cpp b/lib/mapping/CDrawRoadsOperation.cpp index 98efe7563..9ea49db26 100644 --- a/lib/mapping/CDrawRoadsOperation.cpp +++ b/lib/mapping/CDrawRoadsOperation.cpp @@ -12,6 +12,7 @@ #include "CDrawRoadsOperation.h" #include "CMap.h" +#include "../CRandomGenerator.h" #include "../RoadHandler.h" #include "../RiverHandler.h" diff --git a/lib/mapping/CDrawRoadsOperation.h b/lib/mapping/CDrawRoadsOperation.h index c5e025432..16e2ad070 100644 --- a/lib/mapping/CDrawRoadsOperation.h +++ b/lib/mapping/CDrawRoadsOperation.h @@ -28,8 +28,10 @@ protected: struct LinePattern { std::string data[9]; - std::pair roadMapping, riverMapping; - bool hasHFlip, hasVFlip; + std::pair roadMapping; + std::pair riverMapping; + bool hasHFlip; + bool hasVFlip; }; struct ValidationResult diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 806c953c1..b6b520f13 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -19,6 +19,8 @@ #include "../RoadHandler.h" #include "../TerrainHandler.h" #include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/CQuest.h" #include "../mapObjects/ObjectTemplate.h" #include "../CGeneralTextHandler.h" #include "../spells/CSpellHandler.h" @@ -40,8 +42,12 @@ DisposedHero::DisposedHero() : heroId(0), portrait(255) } -CMapEvent::CMapEvent() : players(0), humanAffected(0), computerAffected(0), - firstOccurence(0), nextOccurence(0) +CMapEvent::CMapEvent() + : players(0) + , humanAffected(false) + , computerAffected(false) + , firstOccurence(0) + , nextOccurence(0) { } @@ -71,16 +77,20 @@ void CMapEvent::serializeJson(JsonSerializeFormat & handler) void CCastleEvent::serializeJson(JsonSerializeFormat & handler) { CMapEvent::serializeJson(handler); + + // TODO: handler.serializeIdArray("buildings", buildings); { std::vector temp(buildings.begin(), buildings.end()); auto a = handler.enterArray("buildings"); a.syncSize(temp); for(int i = 0; i < temp.size(); ++i) { - a.serializeInt(i, temp[i]); - buildings.insert(temp[i]); + int buildingID = temp[i].getNum(); + a.serializeInt(i, buildingID); + buildings.insert(buildingID); } } + { auto a = handler.enterArray("creatures"); a.syncSize(creatures); @@ -156,13 +166,14 @@ bool TerrainTile::isWater() const return terType->isWater(); } -CMap::CMap() - : checksum(0) +CMap::CMap(IGameCallback * cb) + : GameCallbackHolder(cb) + , checksum(0) , grailPos(-1, -1, -1) , grailRadius(0) , uidCounter(0) { - allHeroes.resize(allowedHeroes.size()); + allHeroes.resize(VLC->heroh->size()); allowedAbilities = VLC->skillh->getDefaultAllowed(); allowedArtifact = VLC->arth->getDefaultAllowed(); allowedSpells = VLC->spellh->getDefaultAllowed(); @@ -178,6 +189,9 @@ CMap::~CMap() for(auto quest : quests) quest.dellNull(); + for(auto artInstance : artInstances) + artInstance.dellNull(); + resetStaticData(); } @@ -250,10 +264,10 @@ void CMap::calculateGuardingGreaturePositions() } } -CGHeroInstance * CMap::getHero(int heroID) +CGHeroInstance * CMap::getHero(HeroTypeID heroID) { for(auto & elem : heroesOnMap) - if(elem->subID == heroID) + if(elem->getHeroType() == heroID) return elem; return nullptr; } @@ -341,13 +355,8 @@ int3 CMap::guardingCreaturePosition (int3 pos) const { for (CGObjectInstance* obj : posTile.visitableObjects) { - if(obj->isBlockedVisitable()) - { - if (obj->ID == Obj::MONSTER) // Monster - return pos; - else - return int3(-1, -1, -1); //blockvis objects are not guarded by neighbouring creatures - } + if (obj->ID == Obj::MONSTER) + return pos; } } @@ -393,7 +402,7 @@ const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj type // There is weird bug because of which sometimes heroes will not be found properly despite having correct position // Try to workaround that and find closest object that we can use - logGlobal->error("Failed to find object of type %d at %s", static_cast(type), pos.toString()); + logGlobal->error("Failed to find object of type %d at %s", type.getNum(), pos.toString()); logGlobal->error("Will try to find closest matching object"); CGObjectInstance * bestMatch = nullptr; @@ -426,34 +435,34 @@ void CMap::checkForObjectives() switch (cond.condition) { case EventCondition::HAVE_ARTIFACT: - event.onFulfill.replaceTextID(VLC->artifacts()->getByIndex(cond.objectType)->getNameTextID()); + event.onFulfill.replaceTextID(cond.objectType.as().toEntity(VLC)->getNameTextID()); break; case EventCondition::HAVE_CREATURES: - event.onFulfill.replaceTextID(VLC->creatures()->getByIndex(cond.objectType)->getNameSingularTextID()); + event.onFulfill.replaceTextID(cond.objectType.as().toEntity(VLC)->getNameSingularTextID()); event.onFulfill.replaceNumber(cond.value); break; case EventCondition::HAVE_RESOURCES: - event.onFulfill.replaceLocalString(EMetaText::RES_NAMES, cond.objectType); + event.onFulfill.replaceName(cond.objectType.as()); event.onFulfill.replaceNumber(cond.value); break; case EventCondition::HAVE_BUILDING: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); + cond.objectID = getObjectiveObjectFrom(cond.position, Obj::TOWN)->id; break; case EventCondition::CONTROL: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + cond.objectID = getObjectiveObjectFrom(cond.position, cond.objectType.as())->id; - if (cond.object) + if (cond.objectID != ObjectInstanceID::NONE) { - const auto * town = dynamic_cast(cond.object); + const auto * town = dynamic_cast(objects[cond.objectID].get()); if (town) event.onFulfill.replaceRawString(town->getNameTranslated()); - const auto * hero = dynamic_cast(cond.object); + const auto * hero = dynamic_cast(objects[cond.objectID].get()); if (hero) event.onFulfill.replaceRawString(hero->getNameTranslated()); } @@ -461,30 +470,22 @@ void CMap::checkForObjectives() case EventCondition::DESTROY: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + cond.objectID = getObjectiveObjectFrom(cond.position, cond.objectType.as())->id; - if (cond.object) + if (cond.objectID != ObjectInstanceID::NONE) { - const auto * hero = dynamic_cast(cond.object); + const auto * hero = dynamic_cast(objects[cond.objectID].get()); if (hero) event.onFulfill.replaceRawString(hero->getNameTranslated()); } break; case EventCondition::TRANSPORT: - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); + cond.objectID = getObjectiveObjectFrom(cond.position, Obj::TOWN)->id; break; //break; case EventCondition::DAYS_PASSED: //break; case EventCondition::IS_HUMAN: //break; case EventCondition::DAYS_WITHOUT_TOWN: //break; case EventCondition::STANDARD_WIN: - - //TODO: support new condition format - case EventCondition::HAVE_0: - break; - case EventCondition::DESTROY_0: - break; - case EventCondition::HAVE_BUILDING_0: - break; } return cond; }; @@ -618,67 +619,53 @@ void CMap::banWaterContent() void CMap::banWaterSpells() { - for (int j = 0; j < allowedSpells.size(); j++) + vstd::erase_if(allowedSpells, [&](SpellID spell) { - if (allowedSpells[j]) - { - auto* spell = dynamic_cast(VLC->spells()->getByIndex(j)); - if (spell->onlyOnWaterMap && !isWaterMap()) - { - allowedSpells[j] = false; - } - } - } + return spell.toSpell()->onlyOnWaterMap && !isWaterMap(); + }); } void CMap::banWaterArtifacts() { - for (int j = 0; j < allowedArtifact.size(); j++) + vstd::erase_if(allowedArtifact, [&](ArtifactID artifact) { - if (allowedArtifact[j]) - { - auto* art = dynamic_cast(VLC->artifacts()->getByIndex(j)); - if (art->onlyOnWaterMap && !isWaterMap()) - { - allowedArtifact[j] = false; - } - } - } + return artifact.toArtifact()->onlyOnWaterMap && !isWaterMap(); + }); } void CMap::banWaterSkills() { - for (int j = 0; j < allowedAbilities.size(); j++) + vstd::erase_if(allowedAbilities, [&](SecondarySkill skill) { - if (allowedAbilities[j]) - { - auto* skill = dynamic_cast(VLC->skills()->getByIndex(j)); - if (skill->onlyOnWaterMap && !isWaterMap()) - { - allowedAbilities[j] = false; - } - } - } + return skill.toSkill()->onlyOnWaterMap && !isWaterMap(); + }); } void CMap::banWaterHeroes() { - for (int j = 0; j < allowedHeroes.size(); j++) + vstd::erase_if(allowedHeroes, [&](HeroTypeID hero) { - if (allowedHeroes[j]) - { - auto* h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); - if ((h->onlyOnWaterMap && !isWaterMap()) || (h->onlyOnMapWithoutWater && isWaterMap())) - { - banHero(HeroTypeID(j)); - } - } - } + return hero.toHeroType()->onlyOnWaterMap && !isWaterMap(); + }); + + vstd::erase_if(allowedHeroes, [&](HeroTypeID hero) + { + return hero.toHeroType()->onlyOnMapWithoutWater && isWaterMap(); + }); } void CMap::banHero(const HeroTypeID & id) { - allowedHeroes.at(id) = false; + if (!vstd::contains(allowedHeroes, id)) + logGlobal->warn("Attempt to ban hero %s, who is already not allowed", id.encode(id)); + allowedHeroes.erase(id); +} + +void CMap::unbanHero(const HeroTypeID & id) +{ + if (vstd::contains(allowedHeroes, id)) + logGlobal->warn("Attempt to unban hero %s, who is already allowed", id.encode(id)); + allowedHeroes.insert(id); } void CMap::initTerrain() @@ -695,10 +682,21 @@ CMapEditManager * CMap::getEditManager() void CMap::resetStaticData() { - CGKeys::reset(); - CGMagi::reset(); - CGObelisk::reset(); - CGTownInstance::reset(); + obeliskCount = 0; + obelisksVisited.clear(); + townMerchantArtifacts.clear(); + townUniversitySkills.clear(); +} + +void CMap::resolveQuestIdentifiers() +{ + //FIXME: move to CMapLoaderH3M + for (auto & quest : quests) + { + if (quest->killTarget != ObjectInstanceID::NONE) + quest->killTarget = questIdentifierToId[quest->killTarget.getNum()]; + } + questIdentifierToId.clear(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index de9d5e5a2..1b31c1e08 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -10,12 +10,13 @@ #pragma once -#include "CMapHeader.h" -#include "../MetaString.h" -#include "../mapObjects/MiscObjects.h" // To serialize static props -#include "../mapObjects/CQuest.h" // To serialize static props -#include "../mapObjects/CGTownInstance.h" // To serialize static props #include "CMapDefines.h" +#include "CMapHeader.h" + +#include "../ConstTransitivePtr.h" +#include "../GameCallbackHolder.h" +#include "../MetaString.h" +#include "../networkPacks/TradeItem.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +44,7 @@ struct DLL_LINKAGE Rumor ~Rumor() = default; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & name; h & text; @@ -63,7 +64,7 @@ struct DLL_LINKAGE DisposedHero std::set players; /// Who can hire this hero (bitfield). template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & heroId; h & portrait; @@ -73,10 +74,10 @@ struct DLL_LINKAGE DisposedHero }; /// The map contains the map header, the tiles of the terrain, objects, heroes, towns, rumors... -class DLL_LINKAGE CMap : public CMapHeader +class DLL_LINKAGE CMap : public CMapHeader, public GameCallbackHolder { public: - CMap(); + explicit CMap(IGameCallback *cb); ~CMap(); void initTerrain(); @@ -112,26 +113,28 @@ public: void banWaterArtifacts(); void banWaterHeroes(); void banHero(const HeroTypeID& id); + void unbanHero(const HeroTypeID & id); void banWaterSpells(); void banWaterSkills(); void banWaterContent(); /// Gets object of specified type on requested position const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj type); - CGHeroInstance * getHero(int heroId); + CGHeroInstance * getHero(HeroTypeID heroId); /// Sets the victory/loss condition objectives ?? void checkForObjectives(); void resetStaticData(); + void resolveQuestIdentifiers(); ui32 checksum; std::vector rumors; std::vector disposedHeroes; std::vector > predefinedHeroes; - std::vector allowedSpells; - std::vector allowedArtifact; - std::vector allowedAbilities; + std::set allowedSpells; + std::set allowedArtifact; + std::set allowedAbilities; std::list events; int3 grailPos; int grailRadius; @@ -157,6 +160,12 @@ public: bool waterMap; + ui8 obeliskCount = 0; //how many obelisks are on map + std::map obelisksVisited; //map: team_id => how many obelisks has been visited + + std::vector townMerchantArtifacts; + std::vector townUniversitySkills; + private: /// a 3-dimensional array of terrain tiles, access is as follows: x, y, level. where level=1 is underground boost::multi_array terrain; @@ -165,7 +174,7 @@ private: public: template - void serialize(Handler &h, const int formatVersion) + void serialize(Handler &h) { h & static_cast(*this); h & triggeredEvents; //from CMapHeader @@ -178,7 +187,14 @@ public: h & artInstances; h & quests; h & allHeroes; - h & questIdentifierToId; + + if (h.version < Handler::Version::DESTROYED_OBJECTS) + { + // old save compatibility + //FIXME: remove this field after save-breaking change + h & questIdentifierToId; + resolveQuestIdentifiers(); + } //TODO: viccondetails h & terrain; @@ -191,12 +207,10 @@ public: h & artInstances; // static members - h & CGKeys::playerKeyMap; - h & CGMagi::eyelist; - h & CGObelisk::obeliskCount; - h & CGObelisk::visited; - h & CGTownInstance::merchantArtifacts; - h & CGTownInstance::universitySkills; + h & obeliskCount; + h & obelisksVisited; + h & townMerchantArtifacts; + h & townUniversitySkills; h & instanceNames; } diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index 85a2da1a1..e1032aff7 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -37,13 +37,13 @@ public: MetaString message; TResources resources; ui8 players; // affected players, bit field? - ui8 humanAffected; - ui8 computerAffected; + bool humanAffected; + bool computerAffected; ui32 firstOccurence; ui32 nextOccurence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & name; h & message; @@ -68,7 +68,7 @@ public: std::vector creatures; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & static_cast(*this); h & buildings; @@ -112,7 +112,7 @@ struct DLL_LINKAGE TerrainTile std::vector blockingObjects; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & terType; h & terView; diff --git a/lib/mapping/CMapEditManager.cpp b/lib/mapping/CMapEditManager.cpp index fcc4cdaf9..5a8a1c996 100644 --- a/lib/mapping/CMapEditManager.cpp +++ b/lib/mapping/CMapEditManager.cpp @@ -125,9 +125,9 @@ void CMapEditManager::clearTerrain(CRandomGenerator * gen) execute(std::make_unique(map, gen ? gen : &(this->gen))); } -void CMapEditManager::drawTerrain(TerrainId terType, CRandomGenerator * gen) +void CMapEditManager::drawTerrain(TerrainId terType, int decorationsPercentage, CRandomGenerator * gen) { - execute(std::make_unique(map, terrainSel, terType, gen ? gen : &(this->gen))); + execute(std::make_unique(map, terrainSel, terType, decorationsPercentage, gen ? gen : &(this->gen))); terrainSel.clearSelection(); } @@ -143,8 +143,6 @@ void CMapEditManager::drawRiver(RiverId riverType, CRandomGenerator* gen) terrainSel.clearSelection(); } - - void CMapEditManager::insertObject(CGObjectInstance * obj) { execute(std::make_unique(map, obj)); diff --git a/lib/mapping/CMapEditManager.h b/lib/mapping/CMapEditManager.h index 4daf3c398..e380db826 100644 --- a/lib/mapping/CMapEditManager.h +++ b/lib/mapping/CMapEditManager.h @@ -70,7 +70,7 @@ public: void clearTerrain(CRandomGenerator * gen = nullptr); /// Draws terrain at the current terrain selection. The selection will be cleared automatically. - void drawTerrain(TerrainId terType, CRandomGenerator * gen = nullptr); + void drawTerrain(TerrainId terType, int decorationsPercentage, CRandomGenerator * gen = nullptr); /// Draws roads at the current terrain selection. The selection will be cleared automatically. void drawRoad(RoadId roadType, CRandomGenerator * gen = nullptr); diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 2ca1ee8e0..74ec81358 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -15,6 +15,7 @@ #include "../VCMI_Lib.h" #include "../CTownHandler.h" #include "../CGeneralTextHandler.h" +#include "../json/JsonUtils.h" #include "../modding/CModHandler.h" #include "../CHeroHandler.h" #include "../Languages.h" @@ -68,22 +69,15 @@ bool PlayerInfo::hasCustomMainHero() const } EventCondition::EventCondition(EWinLoseType condition): - object(nullptr), - metaType(EMetaclass::INVALID), value(-1), - objectType(-1), - objectSubtype(-1), position(-1, -1, -1), condition(condition) { } -EventCondition::EventCondition(EWinLoseType condition, si32 value, si32 objectType, const int3 & position): - object(nullptr), - metaType(EMetaclass::INVALID), +EventCondition::EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position): value(value), objectType(objectType), - objectSubtype(-1), position(position), condition(condition) {} @@ -124,18 +118,14 @@ void CMapHeader::setupEvents() } CMapHeader::CMapHeader() : version(EMapFormat::VCMI), height(72), width(72), - twoLevel(true), difficulty(1), levelLimit(0), howManyTeams(0), areAnyPlayers(false) + twoLevel(true), difficulty(EMapDifficulty::NORMAL), levelLimit(0), howManyTeams(0), areAnyPlayers(false) { setupEvents(); allowedHeroes = VLC->heroh->getDefaultAllowed(); players.resize(PlayerColor::PLAYER_LIMIT_I); - VLC->generaltexth->addSubContainer(*this); } -CMapHeader::~CMapHeader() -{ - VLC->generaltexth->removeSubContainer(*this); -} +CMapHeader::~CMapHeader() = default; ui8 CMapHeader::levels() const { @@ -144,11 +134,9 @@ ui8 CMapHeader::levels() const void CMapHeader::registerMapStrings() { - VLC->generaltexth->removeSubContainer(*this); - VLC->generaltexth->addSubContainer(*this); - //get supported languages. Assuming that translation containing most strings is the base language - std::set mapLanguages, mapBaseLanguages; + std::set> mapLanguages; + std::set> mapBaseLanguages; int maxStrings = 0; for(auto & translation : translations.Struct()) { @@ -162,7 +150,7 @@ void CMapHeader::registerMapStrings() if(maxStrings == 0 || mapLanguages.empty()) { - logGlobal->info("Map %s doesn't have any supported translation", name.toString()); + logGlobal->trace("Map %s doesn't have any supported translation", name.toString()); return; } @@ -173,7 +161,8 @@ void CMapHeader::registerMapStrings() mapBaseLanguages.insert(translation.first); } - std::string baseLanguage, language; + std::string baseLanguage; + std::string language; //english is preferrable as base language if(mapBaseLanguages.count(Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier)) baseLanguage = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; @@ -200,7 +189,7 @@ void CMapHeader::registerMapStrings() JsonUtils::mergeCopy(data, translations[language]); for(auto & s : data.Struct()) - registerString("map", TextIdentifier(s.first), s.second.String(), language); + texts.registerString("map", TextIdentifier(s.first), s.second.String(), language); } std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized) @@ -210,7 +199,7 @@ std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeade std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language) { - mapHeader.registerString(modContext, UID, localized, language); + mapHeader.texts.registerString(modContext, UID, localized, language); mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized; return UID.get(); } diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index a64a08dfe..deaf52e98 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -10,6 +10,9 @@ #pragma once +#include "../constants/EntityIdentifiers.h" +#include "../constants/Enumerations.h" +#include "../constants/VariantIdentifier.h" #include "../modding/CModInfo.h" #include "../LogicalExpression.h" #include "../int3.h" @@ -20,7 +23,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; enum class EMapFormat : uint8_t; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /// The hero name struct consists of the hero id and the hero name. struct DLL_LINKAGE SHeroName @@ -31,7 +34,7 @@ struct DLL_LINKAGE SHeroName std::string heroName; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & heroId; h & heroName; @@ -75,7 +78,7 @@ struct DLL_LINKAGE PlayerInfo TeamID team; /// The default value NO_TEAM template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & hasRandomHero; h & mainCustomHeroId; @@ -99,7 +102,6 @@ struct DLL_LINKAGE PlayerInfo struct DLL_LINKAGE EventCondition { enum EWinLoseType { - //internal use, deprecated HAVE_ARTIFACT, // type - required artifact HAVE_CREATURES, // type - creatures to collect, value - amount to collect HAVE_RESOURCES, // type - resource ID, value - amount to collect @@ -108,42 +110,34 @@ struct DLL_LINKAGE EventCondition DESTROY, // position - position of object, optional, type - type of object TRANSPORT, // position - where artifact should be transported, type - type of artifact - //map format version pre 1.0 DAYS_PASSED, // value - number of days from start of the game IS_HUMAN, // value - 0 = player is AI, 1 = player is human DAYS_WITHOUT_TOWN, // value - how long player can live without town, 0=instakill STANDARD_WIN, // normal defeat all enemies condition CONST_VALUE, // condition that always evaluates to "value" (0 = false, 1 = true) - - //map format version 1.0+ - HAVE_0, - HAVE_BUILDING_0, - DESTROY_0 }; - EventCondition(EWinLoseType condition = STANDARD_WIN); - EventCondition(EWinLoseType condition, si32 value, si32 objectType, const int3 & position = int3(-1, -1, -1)); + using TargetTypeID = VariantIdentifier; - const CGObjectInstance * object; // object that was at specified position or with instance name on start - EMetaclass metaType; + EventCondition(EWinLoseType condition = STANDARD_WIN); + EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position = int3(-1, -1, -1)); + + ObjectInstanceID objectID; // object that was at specified position or with instance name on start si32 value; - si32 objectType; - si32 objectSubtype; + TargetTypeID objectType; std::string objectInstanceName; int3 position; EWinLoseType condition; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { - h & object; + h & objectID; h & value; h & objectType; h & position; h & condition; - h & objectSubtype; h & objectInstanceName; - h & metaType; } }; @@ -164,7 +158,7 @@ struct DLL_LINKAGE EventEffect MetaString toOtherMessage; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & type; h & toOtherMessage; @@ -189,7 +183,7 @@ struct DLL_LINKAGE TriggeredEvent EventEffect effect; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & identifier; h & trigger; @@ -199,8 +193,17 @@ struct DLL_LINKAGE TriggeredEvent } }; +enum class EMapDifficulty : uint8_t +{ + EASY = 0, + NORMAL = 1, + HARD = 2, + EXPERT = 3, + IMPOSSIBLE = 4 +}; + /// The map header holds information about loss/victory condition,map format, version, players, height, width,... -class DLL_LINKAGE CMapHeader: public TextLocalizationContainer +class DLL_LINKAGE CMapHeader { void setupEvents(); public: @@ -226,7 +229,7 @@ public: bool twoLevel; /// The default value is true. MetaString name; MetaString description; - ui8 difficulty; /// The default value is 1 representing a normal map difficulty. + EMapDifficulty difficulty; /// Specifies the maximum level to reach for a hero. A value of 0 states that there is no /// maximum level for heroes. This is the default value. ui8 levelLimit; @@ -238,8 +241,8 @@ public: std::vector players; /// The default size of the vector is PlayerColor::PLAYER_LIMIT. ui8 howManyTeams; - std::vector allowedHeroes; - std::vector reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserverd for campaign + std::set allowedHeroes; + std::set reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserverd for campaign bool areAnyPlayers; /// Unused. True if there are any playable players on the map. @@ -248,13 +251,14 @@ public: /// translations for map to be transferred over network JsonNode translations; + TextContainerRegistrable texts; void registerMapStrings(); template - void serialize(Handler & h, const int Version) + void serialize(Handler & h) { - h & static_cast(*this); + h & texts; h & version; h & mods; h & name; @@ -262,7 +266,12 @@ public: h & width; h & height; h & twoLevel; - h & difficulty; + // FIXME: we should serialize enum's according to their underlying type + // should be fixed when we are making breaking change to save compatiblity + static_assert(Handler::Version::MINIMAL < Handler::Version::RELEASE_143); + uint8_t difficultyInteger = static_cast(difficulty); + h & difficultyInteger; + difficulty = static_cast(difficultyInteger); h & levelLimit; h & areAnyPlayers; h & players; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index ede7a16e6..459cc4120 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" #include "CMapInfo.h" -#include - #include "../filesystem/ResourcePath.h" #include "../StartInfo.h" #include "../GameConstants.h" @@ -21,12 +19,15 @@ #include "../campaign/CampaignHandler.h" #include "../filesystem/Filesystem.h" -#include "../serializer/CMemorySerializer.h" +#include "../serializer/CLoadFile.h" #include "../CGeneralTextHandler.h" +#include "../TextOperations.h" #include "../rmg/CMapGenOptions.h" #include "../CCreatureHandler.h" #include "../GameSettings.h" #include "../CHeroHandler.h" +#include "../Languages.h" +#include "../CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -54,7 +55,7 @@ void CMapInfo::mapInit(const std::string & fname) void CMapInfo::saveInit(const ResourcePath & file) { - CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); + CLoadFile lf(*CResourceHandler::get()->getResourceName(file), ESerializationVersion::MINIMAL); lf.checkMagicBytes(SAVEGAME_MAGIC); mapHeader = std::make_unique(); @@ -63,8 +64,8 @@ void CMapInfo::saveInit(const ResourcePath & file) originalFileURI = file.getOriginalName(); fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(file)).string(); countPlayers(); - std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); - date = vstd::getFormattedDateTime(time); + lastWrite = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); + date = TextOperations::getFormattedDateTimeLocal(lastWrite); // We absolutely not need this data for lobby and server will read it from save // FIXME: actually we don't want them in CMapHeader! diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 512cff481..40e05baee 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -28,8 +28,9 @@ public: std::unique_ptr campaign; //may be nullptr if scenario StartInfo * scenarioOptionsOfSave; // Options with which scenario has been started (used only with saved games) std::string fileURI; - std::string originalFileURI; - std::string fullFileURI; + std::string originalFileURI; // no need to serialize + std::string fullFileURI; // no need to serialize + std::time_t lastWrite; // no need to serialize std::string date; int amountOfPlayersOnMap; int amountOfHumanControllablePlayers; @@ -57,7 +58,7 @@ public: int getMapSizeFormatIconId() const; std::string getMapSizeName() const; - template void serialize(Handler &h, const int Version) + template void serialize(Handler &h) { h & mapHeader; h & campaign; diff --git a/lib/mapping/CMapOperation.cpp b/lib/mapping/CMapOperation.cpp index 1865523ba..072fbf2ce 100644 --- a/lib/mapping/CMapOperation.cpp +++ b/lib/mapping/CMapOperation.cpp @@ -12,7 +12,9 @@ #include "CMapOperation.h" #include "../VCMI_Lib.h" +#include "../CRandomGenerator.h" #include "../TerrainHandler.h" +#include "../mapObjects/CGObjectInstance.h" #include "CMap.h" #include "MapEditUtils.h" @@ -83,10 +85,11 @@ void CComposedOperation::addOperation(std::unique_ptr&& operation operations.push_back(std::move(operation)); } -CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, CRandomGenerator * gen): +CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, CRandomGenerator * gen): CMapOperation(map), terrainSel(std::move(terrainSel)), terType(terType), + decorationsPercentage(decorationsPercentage), gen(gen) { @@ -286,14 +289,19 @@ void CDrawTerrainOperation::updateTerrainViews() // Get mapping const TerrainViewPattern& pattern = patterns[bestPattern][valRslt.flip]; std::pair mapping; - if(valRslt.transitionReplacement.empty()) + + mapping = pattern.mapping[0]; + + if(pattern.decoration) { - mapping = pattern.mapping[0]; + if (pattern.mapping.size() < 2 || gen->nextInt(100) > decorationsPercentage) + mapping = pattern.mapping[0]; + else + mapping = pattern.mapping[1]; } - else - { + + if (!valRslt.transitionReplacement.empty()) mapping = valRslt.transitionReplacement == TerrainViewPattern::RULE_DIRT ? pattern.mapping[0] : pattern.mapping[1]; - } // Set terrain view auto & tile = map->getTile(pos); @@ -504,9 +512,8 @@ CDrawTerrainOperation::InvalidTiles CDrawTerrainOperation::getInvalidTiles(const { if(map->isInTheMap(pos)) { - auto * ptrConfig = VLC->terviewh; const auto * terType = map->getTile(pos).terType; - auto valid = validateTerrainView(pos, ptrConfig->getTerrainTypePatternById("n1")).result; + auto valid = validateTerrainView(pos, VLC->terviewh->getTerrainTypePatternById("n1")).result; // Special validity check for rock & water if(valid && (terType->isWater() || !terType->isPassable())) @@ -514,7 +521,7 @@ CDrawTerrainOperation::InvalidTiles CDrawTerrainOperation::getInvalidTiles(const static const std::string patternIds[] = { "s1", "s2" }; for(const auto & patternId : patternIds) { - valid = !validateTerrainView(pos, ptrConfig->getTerrainTypePatternById(patternId)).result; + valid = !validateTerrainView(pos, VLC->terviewh->getTerrainTypePatternById(patternId)).result; if(!valid) break; } } @@ -524,7 +531,7 @@ CDrawTerrainOperation::InvalidTiles CDrawTerrainOperation::getInvalidTiles(const static const std::string patternIds[] = { "n2", "n3" }; for(const auto & patternId : patternIds) { - valid = validateTerrainView(pos, ptrConfig->getTerrainTypePatternById(patternId)).result; + valid = validateTerrainView(pos, VLC->terviewh->getTerrainTypePatternById(patternId)).result; if(valid) break; } } @@ -555,12 +562,12 @@ CClearTerrainOperation::CClearTerrainOperation(CMap* map, CRandomGenerator* gen) { CTerrainSelection terrainSel(map); terrainSel.selectRange(MapRect(int3(0, 0, 0), map->width, map->height)); - addOperation(std::make_unique(map, terrainSel, ETerrainId::WATER, gen)); + addOperation(std::make_unique(map, terrainSel, ETerrainId::WATER, 0, gen)); if(map->twoLevel) { terrainSel.clearSelection(); terrainSel.selectRange(MapRect(int3(0, 0, 1), map->width, map->height)); - addOperation(std::make_unique(map, terrainSel, ETerrainId::ROCK, gen)); + addOperation(std::make_unique(map, terrainSel, ETerrainId::ROCK, 0, gen)); } } diff --git a/lib/mapping/CMapOperation.h b/lib/mapping/CMapOperation.h index 688a99edb..a9c36fe27 100644 --- a/lib/mapping/CMapOperation.h +++ b/lib/mapping/CMapOperation.h @@ -63,7 +63,7 @@ private: class CDrawTerrainOperation : public CMapOperation { public: - CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, CRandomGenerator * gen); + CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, CRandomGenerator * gen); void execute() override; void undo() override; @@ -83,7 +83,8 @@ private: struct InvalidTiles { - std::set foreignTiles, nativeTiles; + std::set foreignTiles; + std::set nativeTiles; bool centerPosValid; InvalidTiles() : centerPosValid(false) { } @@ -101,6 +102,7 @@ private: CTerrainSelection terrainSel; TerrainId terType; + int decorationsPercentage; CRandomGenerator* gen; std::set invalidatedTerViews; }; diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 0d119bd5a..cf70f7b08 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "CMapService.h" +#include "../json/JsonUtils.h" #include "../filesystem/Filesystem.h" #include "../filesystem/CBinaryReader.h" #include "../filesystem/CCompressedStream.h" @@ -30,14 +31,14 @@ VCMI_LIB_NAMESPACE_BEGIN -std::unique_ptr CMapService::loadMap(const ResourcePath & name) const +std::unique_ptr CMapService::loadMap(const ResourcePath & name, IGameCallback * cb) const { std::string modName = VLC->modh->findResourceOrigin(name); std::string language = VLC->modh->getModLanguage(modName); std::string encoding = Languages::getLanguageOptions(language).encoding; auto stream = getStreamFromFS(name); - return getMapLoader(stream, name.getName(), modName, encoding)->loadMap(); + return getMapLoader(stream, name.getName(), modName, encoding)->loadMap(cb); } std::unique_ptr CMapService::loadMapHeader(const ResourcePath & name) const @@ -50,10 +51,10 @@ std::unique_ptr CMapService::loadMapHeader(const ResourcePath & name return getMapLoader(stream, name.getName(), modName, encoding)->loadMapHeader(); } -std::unique_ptr CMapService::loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const +std::unique_ptr CMapService::loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding, IGameCallback * cb) const { auto stream = getStreamFromMem(buffer, size); - std::unique_ptr map(getMapLoader(stream, name, modName, encoding)->loadMap()); + std::unique_ptr map(getMapLoader(stream, name, modName, encoding)->loadMap(cb)); std::unique_ptr header(map.get()); //might be original campaign and require patch @@ -94,7 +95,8 @@ ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) { const auto & activeMods = VLC->modh->getActiveMods(); - ModCompatibilityInfo missingMods, missingModsFiltered; + ModCompatibilityInfo missingMods; + ModCompatibilityInfo missingModsFiltered; for(const auto & mapMod : map.mods) { if(vstd::contains(activeMods, mapMod.first)) @@ -169,16 +171,13 @@ static JsonNode loadPatches(const std::string & path) for (auto & entry : node.Struct()) JsonUtils::validate(entry.second, "vcmi:mapHeader", "patch for " + entry.first); - node.setMeta(ModScope::scopeMap()); + node.setModScope(ModScope::scopeMap()); return node; } std::unique_ptr CMapService::getMapPatcher(std::string scenarioName) { - static JsonNode node; - - if (node.isNull()) - node = loadPatches("config/mapOverrides.json"); + static const JsonNode node = loadPatches("config/mapOverrides.json"); boost::to_lower(scenarioName); logGlobal->debug("Request to patch map %s", scenarioName); diff --git a/lib/mapping/CMapService.h b/lib/mapping/CMapService.h index ed791197b..abad7f8cd 100644 --- a/lib/mapping/CMapService.h +++ b/lib/mapping/CMapService.h @@ -10,7 +10,7 @@ #pragma once -#include "../modding/CModInfo.h" +#include "../modding/ModVerificationInfo.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,8 +22,9 @@ class CInputStream; class IMapLoader; class IMapPatcher; +class IGameCallback; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /** * The map service provides loading of VCMI/H3 map files. It can @@ -40,7 +41,7 @@ public: * @param name the name of the map * @return a unique ptr to the loaded map class */ - virtual std::unique_ptr loadMap(const ResourcePath & name) const = 0; + virtual std::unique_ptr loadMap(const ResourcePath & name, IGameCallback * cb) const = 0; /** * Loads the VCMI/H3 map header specified by the name. @@ -57,7 +58,7 @@ public: * @param name indicates name of file that will be used during map header patching * @return a unique ptr to the loaded map class */ - virtual std::unique_ptr loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const = 0; + virtual std::unique_ptr loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding, IGameCallback * cb) const = 0; /** * Loads the VCMI/H3 map header from a buffer. This method is temporarily @@ -82,9 +83,9 @@ public: CMapService() = default; virtual ~CMapService() = default; - std::unique_ptr loadMap(const ResourcePath & name) const override; + std::unique_ptr loadMap(const ResourcePath & name, IGameCallback * cb) const override; std::unique_ptr loadMapHeader(const ResourcePath & name) const override; - std::unique_ptr loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; + std::unique_ptr loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding, IGameCallback * cb) const override; std::unique_ptr loadMapHeader(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; void saveMap(const std::unique_ptr & map, boost::filesystem::path fullPath) const override; @@ -142,7 +143,7 @@ public: * * @return a unique ptr of the loaded map class */ - virtual std::unique_ptr loadMap() = 0; + virtual std::unique_ptr loadMap(IGameCallback * cb) = 0; /** * Loads the VCMI/H3 map header. diff --git a/lib/mapping/MapEditUtils.cpp b/lib/mapping/MapEditUtils.cpp index eef87e147..d0691e082 100644 --- a/lib/mapping/MapEditUtils.cpp +++ b/lib/mapping/MapEditUtils.cpp @@ -12,7 +12,6 @@ #include "MapEditUtils.h" #include "../filesystem/Filesystem.h" -#include "../JsonNode.h" #include "../TerrainHandler.h" #include "CMap.h" #include "CMapOperation.h" @@ -145,6 +144,7 @@ const std::string TerrainViewPattern::RULE_ANY = "?"; TerrainViewPattern::TerrainViewPattern() : diffImages(false) , rotationTypesCount(0) + , decoration(false) , minPoints(0) , maxPoints(std::numeric_limits::max()) { @@ -209,6 +209,7 @@ CTerrainViewPatternConfig::CTerrainViewPatternConfig() // Read various properties pattern.id = ptrnNode["id"].String(); assert(!pattern.id.empty()); + pattern.decoration = ptrnNode["decoration"].Bool(); pattern.minPoints = static_cast(ptrnNode["minPoints"].Float()); pattern.maxPoints = static_cast(ptrnNode["maxPoints"].Float()); if (pattern.maxPoints == 0) diff --git a/lib/mapping/MapEditUtils.h b/lib/mapping/MapEditUtils.h index d7385362b..0d2120489 100644 --- a/lib/mapping/MapEditUtils.h +++ b/lib/mapping/MapEditUtils.h @@ -23,8 +23,11 @@ struct DLL_LINKAGE MapRect { MapRect(); MapRect(const int3 & pos, si32 width, si32 height); - si32 x, y, z; - si32 width, height; + si32 x; + si32 y; + si32 z; + si32 width; + si32 height; si32 left() const; si32 right() const; @@ -199,12 +202,15 @@ struct DLL_LINKAGE TerrainViewPattern /// If diffImages is true, different images/frames are used to place a rotated terrain view. If it's false /// the same frame will be used and rotated. bool diffImages; + /// If true, then this pattern describes decoration tiles and should be used with specified probability + bool decoration; /// The rotationTypesCount is only used if diffImages is true and holds the number how many rotation types(horizontal, etc...) /// are supported. int rotationTypesCount; /// The minimum and maximum points to reach to validate the pattern successfully. - int minPoints, maxPoints; + int minPoints; + int maxPoints; }; /// The terrain view pattern config loads pattern data from the filesystem. diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index 4152e16cd..99bda2b09 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -97,7 +97,7 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() MapFormatFeaturesH3M result = getFeaturesAB(); result.levelSOD = true; - result.artifactsCount = 141; // + Combined artifacts + result.artifactsCount = 144; // + Combined artifacts + 3 unfinished artifacts (required for some maps) result.artifactsBytes = 18; result.heroesPortraitsCount = 163; // +Finneas +young Gem +young Sandro +young Yog diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 800dc4d55..3f3c64606 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -71,10 +71,10 @@ CMapLoaderH3M::CMapLoaderH3M(const std::string & mapName, const std::string & mo //must be instantiated in .cpp file for access to complete types of all member fields CMapLoaderH3M::~CMapLoaderH3M() = default; -std::unique_ptr CMapLoaderH3M::loadMap() +std::unique_ptr CMapLoaderH3M::loadMap(IGameCallback * cb) { // Init map object by parsing the input buffer - map = new CMap(); + map = new CMap(cb); mapHeader = std::unique_ptr(dynamic_cast(map)); init(); @@ -108,8 +108,6 @@ void CMapLoaderH3M::init() inputStream->seek(0); readHeader(); - map->allHeroes.resize(map->allowedHeroes.size()); - readDisposedHeroes(); readMapOptions(); readAllowedArtifacts(); @@ -173,7 +171,7 @@ void CMapLoaderH3M::readHeader() identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); if (features.levelHOTA0) identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); - + reader->setIdentifierRemapper(identifierMapper); // include basic mod @@ -186,10 +184,10 @@ void CMapLoaderH3M::readHeader() mapHeader->twoLevel = reader->readBool(); mapHeader->name.appendTextID(readLocalizedString("header.name")); mapHeader->description.appendTextID(readLocalizedString("header.description")); - mapHeader->difficulty = reader->readInt8(); + mapHeader->difficulty = static_cast(reader->readInt8Checked(0, 4)); if(features.levelAB) - mapHeader->levelLimit = reader->readUInt8(); + mapHeader->levelLimit = reader->readInt8Checked(0, std::min(100u, VLC->heroh->maxSupportedLevel())); else mapHeader->levelLimit = 0; @@ -220,7 +218,7 @@ void CMapLoaderH3M::readPlayerInfo() continue; } - playerInfo.aiTactic = static_cast(reader->readUInt8()); + playerInfo.aiTactic = static_cast(reader->readInt8Checked(-1, 3)); if(features.levelSOD) reader->skipUnused(1); //TODO: check meaning? @@ -263,12 +261,12 @@ void CMapLoaderH3M::readPlayerInfo() if(features.levelAB) { reader->skipUnused(1); //TODO: check meaning? - uint32_t heroCount = reader->readUInt32(); - for(int pp = 0; pp < heroCount; ++pp) + size_t heroCount = reader->readUInt32(); + for(size_t pp = 0; pp < heroCount; ++pp) { SHeroName vv; vv.heroId = reader->readHero(); - vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId)); + vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId.getNum())); playerInfo.heroesNames.push_back(vv); } @@ -276,39 +274,13 @@ void CMapLoaderH3M::readPlayerInfo() } } -enum class EVictoryConditionType : uint8_t -{ - ARTIFACT = 0, - GATHERTROOP = 1, - GATHERRESOURCE = 2, - BUILDCITY = 3, - BUILDGRAIL = 4, - BEATHERO = 5, - CAPTURECITY = 6, - BEATMONSTER = 7, - TAKEDWELLINGS = 8, - TAKEMINES = 9, - TRANSPORTITEM = 10, - HOTA_ELIMINATE_ALL_MONSTERS = 11, - HOTA_SURVIVE_FOR_DAYS = 12, - WINSTANDARD = 255 -}; - -enum class ELossConditionType : uint8_t -{ - LOSSCASTLE = 0, - LOSSHERO = 1, - TIMEEXPIRES = 2, - LOSSSTANDARD = 255 -}; - void CMapLoaderH3M::readVictoryLossConditions() { mapHeader->triggeredEvents.clear(); mapHeader->victoryMessage.clear(); mapHeader->defeatMessage.clear(); - auto vicCondition = static_cast(reader->readUInt8()); + auto vicCondition = static_cast(reader->readInt8Checked(-1, 12)); EventCondition victoryCondition(EventCondition::STANDARD_WIN); EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); @@ -381,7 +353,7 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::GATHERRESOURCE: { EventCondition cond(EventCondition::HAVE_RESOURCES); - cond.objectType = reader->readUInt8(); + cond.objectType = reader->readGameResID(); cond.value = reader->readInt32(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); @@ -397,9 +369,9 @@ void CMapLoaderH3M::readVictoryLossConditions() EventExpression::OperatorAll oper; EventCondition cond(EventCondition::HAVE_BUILDING); cond.position = reader->readInt3(); - cond.objectType = BuildingID::TOWN_HALL + reader->readUInt8(); + cond.objectType = BuildingID::HALL_LEVEL(reader->readInt8Checked(0,3) + 1); oper.expressions.emplace_back(cond); - cond.objectType = BuildingID::FORT + reader->readUInt8(); + cond.objectType = BuildingID::FORT_LEVEL(reader->readInt8Checked(0, 2)); oper.expressions.emplace_back(cond); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); @@ -414,7 +386,7 @@ void CMapLoaderH3M::readVictoryLossConditions() assert(allowNormalVictory == true); // not selectable in editor assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::HAVE_BUILDING); - cond.objectType = BuildingID::GRAIL; + cond.objectType = BuildingID(BuildingID::GRAIL); cond.position = reader->readInt3(); if(cond.position.z > 2) cond.position = int3(-1, -1, -1); @@ -433,7 +405,7 @@ void CMapLoaderH3M::readVictoryLossConditions() allowNormalVictory = true; // H3 behavior assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::HERO; + cond.objectType = MapObjectID(MapObjectID::HERO); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); @@ -446,7 +418,7 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::CAPTURECITY: { EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; + cond.objectType = MapObjectID(MapObjectID::TOWN); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); @@ -460,7 +432,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; + cond.objectType = MapObjectID(MapObjectID::MONSTER); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); @@ -473,8 +445,8 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::TAKEDWELLINGS: { EventExpression::OperatorAll oper; - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR1)); - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR4)); + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR1))); + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR4))); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289"); specialVictory.onFulfill.appendTextID("core.genrltxt.288"); @@ -486,7 +458,7 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::TAKEMINES: { EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::MINE; + cond.objectType = MapObjectID(MapObjectID::MINE); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291"); specialVictory.onFulfill.appendTextID("core.genrltxt.290"); @@ -499,7 +471,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { assert(allowNormalVictory == true); // not selectable in editor EventCondition cond(EventCondition::TRANSPORT); - cond.objectType = reader->readUInt8(); + cond.objectType = reader->readArtifact8(); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); @@ -513,7 +485,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; + cond.objectType = MapObjectID(MapObjectID::MONSTER); specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers"); specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf"); @@ -580,7 +552,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } // Read loss conditions - auto lossCond = static_cast(reader->readUInt8()); + auto lossCond = static_cast(reader->readInt8Checked(-1, 2)); if(lossCond == ELossConditionType::LOSSSTANDARD) { mapHeader->defeatIconIndex = 3; @@ -602,7 +574,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; + cond.objectType = Obj(Obj::TOWN); cond.position = reader->readInt3(); noneOf.expressions.emplace_back(cond); @@ -616,7 +588,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; + cond.objectType = Obj(Obj::HERO); cond.position = reader->readInt3(); noneOf.expressions.emplace_back(cond); @@ -687,12 +659,12 @@ void CMapLoaderH3M::readAllowedHeroes() if(features.levelAB) { - uint32_t placeholdersQty = reader->readUInt32(); + size_t placeholdersQty = reader->readUInt32(); - for (uint32_t i = 0; i < placeholdersQty; ++i) + for (size_t i = 0; i < placeholdersQty; ++i) { auto heroID = reader->readHero(); - mapHeader->reservedCampaignHeroes.push_back(heroID); + mapHeader->reservedCampaignHeroes.insert(heroID); } } } @@ -702,13 +674,13 @@ void CMapLoaderH3M::readDisposedHeroes() // Reading disposed heroes (20 bytes) if(features.levelSOD) { - ui8 disp = reader->readUInt8(); + size_t disp = reader->readUInt8(); map->disposedHeroes.resize(disp); - for(int g = 0; g < disp; ++g) + for(size_t g = 0; g < disp; ++g) { map->disposedHeroes[g].heroId = reader->readHero(); - map->disposedHeroes[g].portrait.setNum(reader->readHeroPortrait()); - map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); + map->disposedHeroes[g].portrait = reader->readHeroPortrait(); + map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId.getNum())); reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); } } @@ -762,15 +734,15 @@ void CMapLoaderH3M::readAllowedArtifacts() // ban combo artifacts if(!features.levelSOD) { - for(CArtifact * artifact : VLC->arth->objects) + for(auto const & artifact : VLC->arth->objects) if(artifact->isCombined()) - map->allowedArtifact[artifact->getId()] = false; + map->allowedArtifact.erase(artifact->getId()); } if(!features.levelAB) { - map->allowedArtifact[ArtifactID::VIAL_OF_DRAGON_BLOOD] = false; - map->allowedArtifact[ArtifactID::ARMAGEDDONS_BLADE] = false; + map->allowedArtifact.erase(ArtifactID::VIAL_OF_DRAGON_BLOOD); + map->allowedArtifact.erase(ArtifactID::ARMAGEDDONS_BLADE); } // Messy, but needed @@ -780,7 +752,7 @@ void CMapLoaderH3M::readAllowedArtifacts() { if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT) { - map->allowedArtifact[cond.objectType] = false; + map->allowedArtifact.erase(cond.objectType.as()); } return cond; }; @@ -803,10 +775,10 @@ void CMapLoaderH3M::readAllowedSpellsAbilities() void CMapLoaderH3M::readRumors() { - uint32_t rumorsCount = reader->readUInt32(); + size_t rumorsCount = reader->readUInt32(); assert(rumorsCount < 1000); // sanity check - for(int it = 0; it < rumorsCount; it++) + for(size_t it = 0; it < rumorsCount; it++) { Rumor ourRumor; ourRumor.name = readBasicString(); @@ -833,7 +805,7 @@ void CMapLoaderH3M::readPredefinedHeroes() if(!custom) continue; - auto * hero = new CGHeroInstance(); + auto * hero = new CGHeroInstance(map->cb); hero->ID = Obj::HERO; hero->subID = heroID; @@ -855,7 +827,7 @@ void CMapLoaderH3M::readPredefinedHeroes() for(int yy = 0; yy < howMany; ++yy) { hero->secSkills[yy].first = reader->readSkill(); - hero->secSkills[yy].second = reader->readUInt8(); + hero->secSkills[yy].second = reader->readInt8Checked(1,3); } } @@ -866,7 +838,7 @@ void CMapLoaderH3M::readPredefinedHeroes() hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); // 0xFF is default, 00 male, 01 female - hero->gender = static_cast(reader->readUInt8()); + hero->gender = static_cast(reader->readInt8Checked(-1, 1)); assert(hero->gender == EHeroGender::MALE || hero->gender == EHeroGender::FEMALE || hero->gender == EHeroGender::DEFAULT); bool hasCustomSpells = reader->readBool(); @@ -883,7 +855,7 @@ void CMapLoaderH3M::readPredefinedHeroes() } map->predefinedHeroes.emplace_back(hero); - logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getByIndex(hero->subID)->getJsonKey()); + logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getById(hero->getHeroType())->getJsonKey()); } } @@ -900,7 +872,7 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) { - logGlobal->debug("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); + logGlobal->debug("Hero %d at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getHeroType().getNum(), hero->pos.toString()); hero->artifactsInBackpack.clear(); while(!hero->artifactsWorn.empty()) @@ -912,8 +884,8 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) // bag artifacts // number of artifacts in hero's bag - int amount = reader->readUInt16(); - for(int i = 0; i < amount; ++i) + size_t amount = reader->readUInt16(); + for(size_t i = 0; i < amount; ++i) { loadArtifactToSlot(hero, ArtifactPosition::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); } @@ -926,7 +898,7 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) if(artifactID == ArtifactID::NONE) return false; - const Artifact * art = artifactID.toArtifact(VLC->artifacts()); + const Artifact * art = artifactID.toEntity(VLC); if(!art) { @@ -944,10 +916,9 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) // He has Shackles of War (normally - MISC slot artifact) in LEFT_HAND slot set in editor // Artifact seems to be missing in game, so skip artifacts that don't fit target slot auto * artifact = ArtifactUtils::createArtifact(map, artifactID); - auto dstLoc = ArtifactLocation(hero, ArtifactPosition(slot)); - if(artifact->canBePutAt(dstLoc)) + if(artifact->canBePutAt(hero, ArtifactPosition(slot))) { - artifact->putAt(dstLoc); + artifact->putAt(*hero, ArtifactPosition(slot)); } else { @@ -1008,7 +979,7 @@ void CMapLoaderH3M::readObjectTemplates() CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) { - auto * object = new CGEvent(); + auto * object = new CGEvent(map->cb); readBoxContent(object, mapPosition, idToBeGiven); @@ -1028,7 +999,7 @@ CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const Obje CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) { - auto * object = new CGPandoraBox(); + auto * object = new CGPandoraBox(map->cb); readBoxContent(object, mapPosition, idToBeGiven); return object; } @@ -1038,52 +1009,52 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi readMessageAndGuards(object->message, object, mapPosition); Rewardable::VisitInfo vinfo; auto & reward = vinfo.reward; - + reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); - if(auto val = reader->readUInt8()) + if(auto val = reader->readInt8Checked(-3, 3)) reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); - if(auto val = reader->readUInt8()) + if(auto val = reader->readInt8Checked(-3, 3)) reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) reward.primary.at(x) = reader->readUInt8(); - int gabn = reader->readUInt8(); //number of gained abilities - for(int oo = 0; oo < gabn; ++oo) + size_t gabn = reader->readUInt8(); //number of gained abilities + for(size_t oo = 0; oo < gabn; ++oo) { auto rId = reader->readSkill(); - auto rVal = reader->readUInt8(); - + auto rVal = reader->readInt8Checked(1,3); + reward.secondary[rId] = rVal; } - int gart = reader->readUInt8(); //number of gained artifacts - for(int oo = 0; oo < gart; ++oo) + size_t gart = reader->readUInt8(); //number of gained artifacts + for(size_t oo = 0; oo < gart; ++oo) reward.artifacts.push_back(reader->readArtifact()); - int gspel = reader->readUInt8(); //number of gained spells - for(int oo = 0; oo < gspel; ++oo) + size_t gspel = reader->readUInt8(); //number of gained spells + for(size_t oo = 0; oo < gspel; ++oo) reward.spells.push_back(reader->readSpell()); - int gcre = reader->readUInt8(); //number of gained creatures - for(int oo = 0; oo < gcre; ++oo) + size_t gcre = reader->readUInt8(); //number of gained creatures + for(size_t oo = 0; oo < gcre; ++oo) { auto rId = reader->readCreature(); auto rVal = reader->readUInt16(); - + reward.creatures.emplace_back(rId, rVal); } - + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; object->configuration.info.push_back(vinfo); - + reader->skipZero(8); } CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) { - auto * object = new CGCreature(); + auto * object = new CGCreature(map->cb); if(features.levelAB) { @@ -1097,7 +1068,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob //type will be set during initialization object->putStack(SlotID(0), hlp); - object->character = reader->readInt8(); + object->character = reader->readInt8Checked(0, 4); bool hasMessage = reader->readBool(); if(hasMessage) @@ -1137,7 +1108,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) { - auto * object = new CGSignBottle(); + auto * object = new CGSignBottle(map->cb); object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); reader->skipZero(4); return object; @@ -1148,95 +1119,113 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share auto * object = readGeneric(position, objectTemplate); auto * rewardable = dynamic_cast(object); - assert(rewardable); - // AB and later maps have allowed abilities defined in H3M if(features.levelAB) { std::set allowedAbilities; reader->readBitmaskSkills(allowedAbilities, false); - if(allowedAbilities.size() != 1) + if (rewardable) { - auto defaultAllowed = VLC->skillh->getDefaultAllowed(); + if(allowedAbilities.size() != 1) + { + auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID) - if(defaultAllowed[skillID]) - allowedAbilities.insert(SecondarySkill(skillID)); + for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID) + if(defaultAllowed.count(skillID)) + allowedAbilities.insert(SecondarySkill(skillID)); + } + + JsonNode variable; + if (allowedAbilities.size() == 1) + { + variable.String() = VLC->skills()->getById(*allowedAbilities.begin())->getJsonKey(); + } + else + { + JsonVector anyOfList; + for (auto const & skill : allowedAbilities) + { + JsonNode entry; + entry.String() = VLC->skills()->getById(skill)->getJsonKey(); + anyOfList.push_back(entry); + } + variable["anyOf"].Vector() = anyOfList; + } + + variable.setModScope(ModScope::scopeGame()); // list may include skills from all mods + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); } - - JsonVector anyOfList; - - for (auto const & skill : allowedAbilities) + else { - JsonNode entry; - entry.String() = VLC->skills()->getById(skill)->getJsonKey(); - anyOfList.push_back(entry); + logGlobal->warn("Failed to set allowed secondary skills to a Witch Hut! Object is not rewardable!"); } - JsonNode variable; - variable["anyOf"].Vector() = anyOfList; - variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods - - rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); } return object; } CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared_ptr objectTemplate) { - enum class ScholarBonusType : uint8_t { + enum class ScholarBonusType : int8_t { + RANDOM = -1, PRIM_SKILL = 0, SECONDARY_SKILL = 1, SPELL = 2, - RANDOM = 255 }; auto * object = readGeneric(position, objectTemplate); auto * rewardable = dynamic_cast(object); - uint8_t bonusTypeRaw = reader->readUInt8(); + uint8_t bonusTypeRaw = reader->readInt8Checked(-1, 2); auto bonusType = static_cast(bonusTypeRaw); auto bonusID = reader->readUInt8(); - switch (bonusType) + if (rewardable) { - case ScholarBonusType::PRIM_SKILL: + switch (bonusType) { - JsonNode variable; - JsonNode dice; - variable.String() = NPrimarySkill::names[bonusID]; - variable.setMeta(ModScope::scopeGame()); - dice.Integer() = 80; - rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable); - rewardable->configuration.presetVariable("dice", "0", dice); - break; + case ScholarBonusType::PRIM_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = NPrimarySkill::names[bonusID]; + variable.setModScope(ModScope::scopeGame()); + dice.Integer() = 80; + rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SECONDARY_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey(); + variable.setModScope(ModScope::scopeGame()); + dice.Integer() = 50; + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SPELL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey(); + variable.setModScope(ModScope::scopeGame()); + dice.Integer() = 20; + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::RANDOM: + break;// No-op + default: + logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName); } - case ScholarBonusType::SECONDARY_SKILL: - { - JsonNode variable; - JsonNode dice; - variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey(); - variable.setMeta(ModScope::scopeGame()); - dice.Integer() = 50; - rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); - rewardable->configuration.presetVariable("dice", "0", dice); - break; - } - case ScholarBonusType::SPELL: - { - JsonNode variable; - JsonNode dice; - variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey(); - variable.setMeta(ModScope::scopeGame()); - dice.Integer() = 20; - rewardable->configuration.presetVariable("spell", "gainedSpell", variable); - rewardable->configuration.presetVariable("dice", "0", dice); - break; - } - case ScholarBonusType::RANDOM: - break;// No-op - default: - logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName); + } + else + { + logGlobal->warn("Failed to set reward parameters for a Scholar! Object is not rewardable!"); } reader->skipZero(6); @@ -1245,7 +1234,7 @@ CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition) { - auto * object = new CGGarrison(); + auto * object = new CGGarrison(map->cb); setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); readCreatureSet(object, 7); @@ -1262,7 +1251,7 @@ CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::sh { ArtifactID artID = ArtifactID::NONE; //random, set later SpellID spellID = SpellID::NONE; - auto * object = new CGArtifact(); + auto * object = new CGArtifact(map->cb); readMessageAndGuards(object->message, object, mapPosition); @@ -1283,12 +1272,12 @@ CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::sh CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::shared_ptr objectTemplate) { - auto * object = new CGResource(); + auto * object = new CGResource(map->cb); readMessageAndGuards(object->message, object, mapPosition); object->amount = reader->readUInt32(); - if(objectTemplate->subid == GameResID(EGameResID::GOLD)) + if(GameResID(objectTemplate->subid) == GameResID(EGameResID::GOLD)) { // Gold is multiplied by 100. object->amount *= 100; @@ -1299,7 +1288,7 @@ CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::sh CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared_ptr objectTemplate) { - auto * object = new CGMine(); + auto * object = new CGMine(map->cb); if(objectTemplate->subid < 7) { setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); @@ -1314,69 +1303,43 @@ CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared CGObjectInstance * CMapLoaderH3M::readDwelling(const int3 & position) { - auto * object = new CGDwelling(); + auto * object = new CGDwelling(map->cb); setOwnerAndValidate(position, object, reader->readPlayer32()); return object; } CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, std::shared_ptr objectTemplate) { - auto * object = new CGDwelling(); - - CSpecObjInfo * spec = nullptr; - switch(objectTemplate->id) - { - case Obj::RANDOM_DWELLING: - spec = new CCreGenLeveledCastleInfo(); - break; - case Obj::RANDOM_DWELLING_LVL: - spec = new CCreGenAsCastleInfo(); - break; - case Obj::RANDOM_DWELLING_FACTION: - spec = new CCreGenLeveledInfo(); - break; - default: - throw std::runtime_error("Invalid random dwelling format"); - } - spec->owner = object; + auto * object = new CGDwelling(map->cb); setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - //216 and 217 - if(auto * castleSpec = dynamic_cast(spec)) + object->randomizationInfo = CGDwellingRandomizationInfo(); + + bool hasFactionInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_LVL; + bool hasLevelInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_FACTION; + + if (hasFactionInfo) { - castleSpec->instanceId = ""; - castleSpec->identifier = reader->readUInt32(); - if(!castleSpec->identifier) - { - castleSpec->asCastle = false; - const int MASK_SIZE = 8; - ui8 mask[2]; - mask[0] = reader->readUInt8(); - mask[1] = reader->readUInt8(); + object->randomizationInfo->identifier = reader->readUInt32(); - castleSpec->allowedFactions.clear(); - castleSpec->allowedFactions.resize(VLC->townh->size(), false); + if(object->randomizationInfo->identifier == 0) + reader->readBitmaskFactions(object->randomizationInfo->allowedFactions, false); + } + else + object->randomizationInfo->allowedFactions.insert(FactionID(objectTemplate->subid)); - for(int i = 0; i < MASK_SIZE; i++) - castleSpec->allowedFactions[i] = ((mask[0] & (1 << i)) > 0); - - for(int i = 0; i < (GameConstants::F_NUMBER - MASK_SIZE); i++) - castleSpec->allowedFactions[i + MASK_SIZE] = ((mask[1] & (1 << i)) > 0); - } - else - { - castleSpec->asCastle = true; - } + if(hasLevelInfo) + { + object->randomizationInfo->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; + object->randomizationInfo->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; + } + else + { + object->randomizationInfo->minLevel = objectTemplate->subid; + object->randomizationInfo->maxLevel = objectTemplate->subid; } - //216 and 218 - if(auto * lvlSpec = dynamic_cast(spec)) - { - lvlSpec->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; - lvlSpec->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; - } - object->info = spec; return object; } @@ -1385,23 +1348,28 @@ CGObjectInstance * CMapLoaderH3M::readShrine(const int3 & position, std::shared_ auto * object = readGeneric(position, objectTemplate); auto * rewardable = dynamic_cast(object); - assert(rewardable); - SpellID spell = reader->readSpell32(); - if(spell != SpellID::NONE) + if (rewardable) { - JsonNode variable; - variable.String() = VLC->spells()->getById(spell)->getJsonKey(); - variable.setMeta(ModScope::scopeGame()); // list may include spells from all mods - rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + if(spell != SpellID::NONE) + { + JsonNode variable; + variable.String() = VLC->spells()->getById(spell)->getJsonKey(); + variable.setModScope(ModScope::scopeGame()); // list may include spells from all mods + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + } + } + else + { + logGlobal->warn("Failed to set selected spell to a Shrine!. Object is not rewardable!"); } return object; } CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition) { - auto * object = new CGHeroPlaceholder(); + auto * object = new CGHeroPlaceholder(map->cb); setOwnerAndValidate(mapPosition, object, reader->readPlayer()); @@ -1439,23 +1407,23 @@ CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition, std::share CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::shared_ptr objectTemplate) { if(VLC->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid)) - return VLC->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(objectTemplate); + return VLC->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(map->cb, objectTemplate); logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile.getOriginalName(), mapPosition.toString()); - return new CGObjectInstance(); + return new CGObjectInstance(map->cb); } CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) { if(objectTemplate->subid == 0) - return new CBank(); + return new CBank(map->cb); - return new CGObjectInstance(); + return new CGObjectInstance(map->cb); } CGObjectInstance * CMapLoaderH3M::readQuestGuard(const int3 & mapPosition) { - auto * guard = new CGQuestGuard(); + auto * guard = new CGQuestGuard(map->cb); readQuest(guard, mapPosition); return guard; } @@ -1469,7 +1437,7 @@ CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::sh CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition) { - auto * object = new CGLighthouse(); + auto * object = new CGLighthouse(map->cb); setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); return object; } @@ -1483,7 +1451,7 @@ CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared int32_t guardsPresetIndex = reader->readInt32(); // presence of upgraded stack: -1 = random, 0 = never, 1 = always - int8_t upgradedStackPresence = reader->readInt8(); + int8_t upgradedStackPresence = reader->readInt8Checked(-1, 1); assert(vstd::iswithin(guardsPresetIndex, -1, 4)); assert(vstd::iswithin(upgradedStackPresence, -1, 1)); @@ -1515,7 +1483,7 @@ CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) { - switch(objectTemplate->id) + switch(objectTemplate->id.toEnum()) { case Obj::EVENT: return readEvent(mapPosition, objectInstanceID); @@ -1739,7 +1707,7 @@ void CMapLoaderH3M::setOwnerAndValidate(const int3 & mapPosition, CGObjectInstan CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) { - auto * object = new CGHeroInstance(); + auto * object = new CGHeroInstance(map->cb); if(features.levelAB) { @@ -1767,7 +1735,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec for(auto & elem : map->disposedHeroes) { - if(elem.heroId.getNum() == object->subID) + if(elem.heroId == object->getHeroType()) { object->nameCustomTextId = elem.name; object->customPortraitSource = elem.portrait; @@ -1777,7 +1745,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec bool hasName = reader->readBool(); if(hasName) - object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "name")); + object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->getHeroType().getNum(), "name")); if(features.levelSOD) { @@ -1813,7 +1781,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec for(int i = 0; i < skillsCount; ++i) { object->secSkills[i].first = reader->readSkill(); - object->secSkills[i].second = reader->readUInt8(); + object->secSkills[i].second = reader->readInt8Checked(1,3); } } @@ -1821,7 +1789,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec if(hasGarison) readCreatureSet(object, 7); - object->formation = static_cast(reader->readUInt8()); + object->formation = static_cast(reader->readInt8Checked(0, 1)); assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); loadArtifactsOfHero(object); @@ -1834,7 +1802,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec if(hasCustomBiography) object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); - object->gender = static_cast(reader->readUInt8()); + object->gender = static_cast(reader->readInt8Checked(-1, 1)); assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT); } else @@ -1854,9 +1822,8 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); } - object->spells.insert(SpellID::PRESET); //placeholder "preset spells" - reader->readBitmaskSpells(object->spells, false); + object->spells.insert(SpellID::PRESET); //placeholder "preset spells" } } else if(features.levelAB) @@ -1876,7 +1843,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); if(ps->size()) { - logGlobal->debug("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); + logGlobal->debug("Hero %s has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getHeroType().getNum() ); for(const auto & b : *ps) object->removeBonus(b); } @@ -1888,8 +1855,8 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec } } - if (object->subID != -1) - logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getByIndex(object->subID)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); + if (object->subID != MapObjectSubID()) + logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getById(object->getHeroType())->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); else logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); @@ -1899,7 +1866,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position, const ObjectInstanceID & idToBeGiven) { - auto * hut = new CGSeerHut(); + auto * hut = new CGSeerHut(map->cb); uint32_t questsCount = 1; @@ -1945,30 +1912,12 @@ enum class ESeerHutRewardType : uint8_t CREATURE = 10, }; -enum class EQuestMission { - NONE = 0, - LEVEL = 1, - PRIMARY_SKILL = 2, - KILL_HERO = 3, - KILL_CREATURE = 4, - ARTIFACT = 5, - ARMY = 6, - RESOURCES = 7, - HERO = 8, - PLAYER = 9, - HOTA_MULTI = 10, - // end of H3 missions - KEYMASTER = 100, - HOTA_HERO_CLASS = 101, - HOTA_REACH_DATE = 102 -}; - void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) { EQuestMission missionType = EQuestMission::NONE; if(features.levelAB) { - missionType = static_cast(readQuest(hut, position)); + missionType = readQuest(hut, position); } else { @@ -1988,7 +1937,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con if(missionType != EQuestMission::NONE) { - auto rewardType = static_cast(reader->readUInt8()); + auto rewardType = static_cast(reader->readInt8Checked(0, 10)); Rewardable::VisitInfo vinfo; auto & reward = vinfo.reward; switch(rewardType) @@ -2010,37 +1959,35 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } case ESeerHutRewardType::MORALE: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readInt8Checked(-3, 3), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::LUCK: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, reader->readInt8Checked(-3, 3), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::RESOURCES: { - auto rId = reader->readUInt8(); + auto rId = reader->readGameResID(); auto rVal = reader->readUInt32(); - assert(rId < features.resourcesCount); - reward.resources[rId] = rVal; break; } case ESeerHutRewardType::PRIMARY_SKILL: { - auto rId = reader->readUInt8(); + auto rId = reader->readPrimary(); auto rVal = reader->readUInt8(); - - reward.primary.at(rId) = rVal; + + reward.primary.at(rId.getNum()) = rVal; break; } case ESeerHutRewardType::SECONDARY_SKILL: { auto rId = reader->readSkill(); - auto rVal = reader->readUInt8(); - + auto rVal = reader->readInt8Checked(1,3); + reward.secondary[rId] = rVal; break; } @@ -2058,7 +2005,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con { auto rId = reader->readCreature(); auto rVal = reader->readUInt16(); - + reward.creatures.emplace_back(rId, rVal); break; } @@ -2067,7 +2014,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con assert(0); } } - + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; hut->configuration.info.push_back(vinfo); } @@ -2078,11 +2025,11 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } } -int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) +EQuestMission CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { - auto missionId = reader->readUInt8(); + auto missionId = static_cast(reader->readInt8Checked(0, 10)); - switch(static_cast(missionId)) + switch(missionId) { case EQuestMission::NONE: return missionId; @@ -2107,22 +2054,22 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } case EQuestMission::ARTIFACT: { - int artNumber = reader->readUInt8(); - for(int yy = 0; yy < artNumber; ++yy) + size_t artNumber = reader->readUInt8(); + for(size_t yy = 0; yy < artNumber; ++yy) { auto artid = reader->readArtifact(); guard->quest->mission.artifacts.push_back(artid); - map->allowedArtifact[artid] = false; //these are unavailable for random generation + map->allowedArtifact.erase(artid); //these are unavailable for random generation } break; } case EQuestMission::ARMY: { - int typeNumber = reader->readUInt8(); + size_t typeNumber = reader->readUInt8(); guard->quest->mission.creatures.resize(typeNumber); - for(int hh = 0; hh < typeNumber; ++hh) + for(size_t hh = 0; hh < typeNumber; ++hh) { - guard->quest->mission.creatures[hh].type = VLC->creh->objects[reader->readCreature()]; + guard->quest->mission.creatures[hh].type = reader->readCreature().toCreature(); guard->quest->mission.creatures[hh].count = reader->readUInt16(); } break; @@ -2150,7 +2097,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) if(missionSubID == 0) { - missionId = int(EQuestMission::HOTA_HERO_CLASS); + missionId = EQuestMission::HOTA_HERO_CLASS; std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); for(auto & hc : heroClasses) @@ -2159,7 +2106,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } if(missionSubID == 1) { - missionId = int(EQuestMission::HOTA_REACH_DATE); + missionId = EQuestMission::HOTA_REACH_DATE; guard->quest->mission.daysPassed = reader->readUInt32() + 1; break; } @@ -2183,7 +2130,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr objectTemplate) { - auto * object = new CGTownInstance(); + auto * object = new CGTownInstance(map->cb); if(features.levelAB) object->identifier = reader->readUInt32(); @@ -2201,7 +2148,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt if(hasGarrison) readCreatureSet(object, 7); - object->formation = static_cast(reader->readUInt8()); + object->formation = static_cast(reader->readInt8Checked(0, 1)); assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); bool hasCustomBuildings = reader->readBool(); @@ -2230,17 +2177,10 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt } { - std::set spellsMask; + std::set spellsMask = VLC->spellh->getDefaultAllowed(); // by default - include spells from mods reader->readBitmaskSpells(spellsMask, true); std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->possibleSpells)); - - auto defaultAllowed = VLC->spellh->getDefaultAllowed(); - - //add all spells from mods - for(int i = features.spellsCount; i < defaultAllowed.size(); ++i) - if(defaultAllowed[i]) - object->possibleSpells.emplace_back(i); } if(features.levelHOTA1) @@ -2266,7 +2206,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt else event.humanAffected = true; - event.computerAffected = reader->readUInt8(); + event.computerAffected = reader->readBool(); event.firstOccurence = reader->readUInt16(); event.nextOccurence = reader->readUInt8(); @@ -2406,6 +2346,8 @@ void CMapLoaderH3M::afterRead() p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset(); } } + + map->resolveQuestIdentifiers(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index d1c1591c6..84100c45a 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -35,6 +35,50 @@ class SpellID; class PlayerColor; class int3; +enum class EQuestMission { + NONE = 0, + LEVEL = 1, + PRIMARY_SKILL = 2, + KILL_HERO = 3, + KILL_CREATURE = 4, + ARTIFACT = 5, + ARMY = 6, + RESOURCES = 7, + HERO = 8, + PLAYER = 9, + HOTA_MULTI = 10, + // end of H3 missions + KEYMASTER = 100, + HOTA_HERO_CLASS = 101, + HOTA_REACH_DATE = 102 +}; + +enum class EVictoryConditionType : int8_t +{ + WINSTANDARD = -1, + ARTIFACT = 0, + GATHERTROOP = 1, + GATHERRESOURCE = 2, + BUILDCITY = 3, + BUILDGRAIL = 4, + BEATHERO = 5, + CAPTURECITY = 6, + BEATMONSTER = 7, + TAKEDWELLINGS = 8, + TAKEMINES = 9, + TRANSPORTITEM = 10, + HOTA_ELIMINATE_ALL_MONSTERS = 11, + HOTA_SURVIVE_FOR_DAYS = 12 +}; + +enum class ELossConditionType : int8_t +{ + LOSSSTANDARD = -1, + LOSSCASTLE = 0, + LOSSHERO = 1, + TIMEEXPIRES = 2 +}; + class DLL_LINKAGE CMapLoaderH3M : public IMapLoader { public: @@ -55,7 +99,7 @@ public: * * @return a unique ptr of the loaded map class */ - std::unique_ptr loadMap() override; + std::unique_ptr loadMap(IGameCallback * cb) override; /** * Loads the VCMI/H3 map header. @@ -204,7 +248,7 @@ private: * * @param guard the quest guard where that quest should be applied to */ - int readQuest(IQuestObject * guard, const int3 & position); + EQuestMission readQuest(IQuestObject * guard, const int3 & position); void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 0208204a1..ba7709e70 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -13,7 +13,7 @@ #include "../filesystem/CInputStream.h" #include "../filesystem/COutputStream.h" -#include "../JsonDetail.h" +#include "../json/JsonWriter.h" #include "CMap.h" #include "MapFormat.h" #include "../ArtifactUtils.h" @@ -28,6 +28,7 @@ #include "../mapObjects/ObjectTemplate.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/MiscObjects.h" #include "../modding/ModScope.h" #include "../modding/ModUtility.h" #include "../spells/CSpellHandler.h" @@ -128,73 +129,14 @@ namespace HeaderDetail namespace TriggeredEventsDetail { - static const std::array conditionNames = + static const std::array conditionNames = { "haveArtifact", "haveCreatures", "haveResources", "haveBuilding", "control", "destroy", "transport", "daysPassed", - "isHuman", "daysWithoutTown", "standardWin", "constValue", - - "have_0", "haveBuilding_0", "destroy_0" + "isHuman", "daysWithoutTown", "standardWin", "constValue" }; - static const std::array typeNames = { "victory", "defeat" }; - - static EMetaclass decodeMetaclass(const std::string & source) - { - if(source.empty()) - return EMetaclass::INVALID; - auto rawId = vstd::find_pos(NMetaclass::names, source); - - if(rawId >= 0) - return static_cast(rawId); - else - return EMetaclass::INVALID; - } - - static std::string encodeIdentifier(EMetaclass metaType, si32 type) - { - std::string metaclassName = NMetaclass::names[static_cast(metaType)]; - std::string identifier; - - switch(metaType) - { - case EMetaclass::ARTIFACT: - { - identifier = ArtifactID::encode(type); - } - break; - case EMetaclass::CREATURE: - { - identifier = CreatureID::encode(type); - } - break; - case EMetaclass::OBJECT: - { - //TODO - std::set subtypes = VLC->objtypeh->knownSubObjects(type); - if(!subtypes.empty()) - { - si32 subtype = *subtypes.begin(); - auto handler = VLC->objtypeh->getHandlerFor(type, subtype); - identifier = handler->getTypeName(); - } - } - break; - case EMetaclass::RESOURCE: - { - identifier = GameConstants::RESOURCE_NAMES[type]; - } - break; - default: - { - logGlobal->error("Unsupported metaclass %s for event condition", metaclassName); - return ""; - } - break; - } - - return ModUtility::makeFullIdentifier("", metaclassName, identifier); - } + static const std::array typeNames = { "victory", "defeat" }; static EventCondition JsonToCondition(const JsonNode & node) { @@ -210,54 +152,43 @@ namespace TriggeredEventsDetail { const JsonNode & data = node.Vector()[1]; + event.objectInstanceName = data["object"].String(); + event.value = data["value"].Integer(); + switch (event.condition) { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes - - std::string fullIdentifier = data["type"].String(); - std::string metaTypeName; - std::string scope; - std::string identifier; - ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); - - event.metaType = decodeMetaclass(metaTypeName); - - auto type = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), fullIdentifier, false); - - if(type) - event.objectType = type.value(); - event.objectInstanceName = data["object"].String(); - if(data["value"].isNumber()) - event.value = static_cast(data["value"].Integer()); - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if (data["type"].getType() == JsonNode::JsonType::DATA_STRING) - { - auto identifier = VLC->identifiers()->getIdentifier(data["type"]); - if(identifier) - event.objectType = identifier.value(); - else - throw std::runtime_error("Identifier resolution failed in event condition"); - } - - if (data["type"].isNumber()) - event.objectType = static_cast(data["type"].Float()); - - if (!data["value"].isNull()) - event.value = static_cast(data["value"].Float()); - } - break; + case EventCondition::HAVE_ARTIFACT: + case EventCondition::TRANSPORT: + if (data["type"].isNumber()) // compatibility + event.objectType = ArtifactID(data["type"].Integer()); + else + event.objectType = ArtifactID(ArtifactID::decode(data["type"].String())); + break; + case EventCondition::HAVE_CREATURES: + if (data["type"].isNumber()) // compatibility + event.objectType = CreatureID(data["type"].Integer()); + else + event.objectType = CreatureID(CreatureID::decode(data["type"].String())); + break; + case EventCondition::HAVE_RESOURCES: + if (data["type"].isNumber()) // compatibility + event.objectType = GameResID(data["type"].Integer()); + else + event.objectType = GameResID(GameResID::decode(data["type"].String())); + break; + case EventCondition::HAVE_BUILDING: + if (data["type"].isNumber()) // compatibility + event.objectType = BuildingID(data["type"].Integer()); + else + event.objectType = BuildingID(BuildingID::decode(data["type"].String())); + break; + case EventCondition::CONTROL: + case EventCondition::DESTROY: + if (data["type"].isNumber()) // compatibility + event.objectType = MapObjectID(data["type"].Integer()); + else + event.objectType = MapObjectID(MapObjectID::decode(data["type"].String())); + break; } if (!data["position"].isNull()) @@ -283,39 +214,11 @@ namespace TriggeredEventsDetail JsonNode data; - switch (event.condition) - { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes + if(!event.objectInstanceName.empty()) + data["object"].String() = event.objectInstanceName; - if(event.metaType != EMetaclass::INVALID) - data["type"].String() = encodeIdentifier(event.metaType, event.objectType); - - if(event.value > 0) - data["value"].Integer() = event.value; - - if(!event.objectInstanceName.empty()) - data["object"].String() = event.objectInstanceName; - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if(event.objectType != -1) - data["type"].Integer() = event.objectType; - - if(event.value != -1) - data["value"].Integer() = event.value; - } - break; - } + data["type"].String() = event.objectType.toString(); + data["value"].Integer() = event.value; if(event.position != int3(-1, -1, -1)) { @@ -389,28 +292,19 @@ RoadType * CMapFormatJson::getRoadByCode(const std::string & code) void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const { - //TODO: unify allowed factions with others - make them std::vector - - std::vector temp; - temp.resize(VLC->townh->size(), false); - auto standard = VLC->townh->getDefaultAllowed(); + std::set temp; if(handler.saving) { for(auto faction : VLC->townh->objects) if(faction->town && vstd::contains(value, faction->getId())) - temp[static_cast(faction->getIndex())] = true; + temp.insert(faction->getId()); } - handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, standard, temp); + handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, VLC->townh->getDefaultAllowed(), temp); if(!handler.saving) - { - value.clear(); - for (std::size_t i=0; i(i)); - } + value = temp; } void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) @@ -537,13 +431,10 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) { std::string temp; if(hero->type) - { temp = hero->type->getJsonKey(); - } else - { - temp = VLC->heroh->objects[hero->subID]->getJsonKey(); - } + temp = hero->getHeroType().toEntity(VLC)->getJsonKey(); + handler.serializeString("type", temp); } } @@ -654,13 +545,10 @@ void CMapFormatJson::writeTeams(JsonSerializer & handler) for(const std::set & teamData : teamsData) { - JsonNode team(JsonNode::JsonType::DATA_VECTOR); + JsonNode team; for(const PlayerColor & player : teamData) - { - JsonNode member(JsonNode::JsonType::DATA_STRING); - member.String() = GameConstants::PLAYER_COLOR_NAMES[player.getNum()]; - team.Vector().push_back(std::move(member)); - } + team.Vector().emplace_back(GameConstants::PLAYER_COLOR_NAMES[player.getNum()]); + dest.Vector().push_back(std::move(team)); } handler.serializeRaw("teams", dest, std::nullopt); @@ -695,7 +583,7 @@ void CMapFormatJson::readTriggeredEvent(TriggeredEvent & event, const JsonNode & void CMapFormatJson::writeTriggeredEvents(JsonSerializer & handler) { - JsonNode triggeredEvents(JsonNode::JsonType::DATA_STRUCT); + JsonNode triggeredEvents; for(const auto & event : mapHeader->triggeredEvents) writeTriggeredEvent(event, triggeredEvents[event.identifier]); @@ -762,11 +650,11 @@ void CMapFormatJson::writeDisposedHeroes(JsonSerializeFormat & handler) for(DisposedHero & hero : map->disposedHeroes) { - std::string type = HeroTypeID::encode(hero.heroId); + std::string type = HeroTypeID::encode(hero.heroId.getNum()); auto definition = definitions->enterStruct(type); - JsonNode players(JsonNode::JsonType::DATA_VECTOR); + JsonNode players; definition->serializeIdArray("availableFor", hero.players); } } @@ -787,19 +675,19 @@ void CMapFormatJson::serializeTimedEvents(JsonSerializeFormat & handler) void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) { - //todo:serializePredefinedHeroes + //todo:serializePredefinedHeroes - if(handler.saving) + if(handler.saving) { if(!map->predefinedHeroes.empty()) { auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); - for(auto & hero : map->predefinedHeroes) + for(auto & hero : map->predefinedHeroes) { - auto predefinedHero = handler.enterStruct(hero->getHeroTypeName()); + auto predefinedHero = handler.enterStruct(hero->getHeroTypeName()); - hero->serializeJsonDefinition(handler); + hero->serializeJsonDefinition(handler); } } } @@ -807,13 +695,13 @@ void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) { auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); - const JsonNode & data = handler.getCurrent(); + const JsonNode & data = handler.getCurrent(); - for(const auto & p : data.Struct()) + for(const auto & p : data.Struct()) { auto predefinedHero = handler.enterStruct(p.first); - auto * hero = new CGHeroInstance(); + auto * hero = new CGHeroInstance(map->cb); hero->ID = Obj::HERO; hero->setHeroTypeName(p.first); hero->serializeJsonDefinition(handler); @@ -831,7 +719,7 @@ void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler) serializePredefinedHeroes(handler); - handler.serializeLIC("allowedAbilities", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); + handler.serializeLIC("allowedAbilities", &SecondarySkill::decode, &SecondarySkill::encode, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); handler.serializeLIC("allowedArtifacts", &ArtifactID::decode, &ArtifactID::encode, VLC->arth->getDefaultAllowed(), map->allowedArtifact); @@ -887,10 +775,10 @@ CMapLoaderJson::CMapLoaderJson(CInputStream * stream) { } -std::unique_ptr CMapLoaderJson::loadMap() +std::unique_ptr CMapLoaderJson::loadMap(IGameCallback * cb) { LOG_TRACE(logGlobal); - std::unique_ptr result = std::make_unique(); + auto result = std::make_unique(cb); map = result.get(); mapHeader = map; readMap(); @@ -901,7 +789,7 @@ std::unique_ptr CMapLoaderJson::loadMapHeader() { LOG_TRACE(logGlobal); map = nullptr; - std::unique_ptr result = std::make_unique(); + auto result = std::make_unique(); mapHeader = result.get(); readHeader(false); return result; @@ -921,7 +809,7 @@ JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) auto data = loader.load(resource)->readAll(); - JsonNode res(reinterpret_cast(data.first.get()), data.second); + JsonNode res(reinterpret_cast(data.first.get()), data.second); return res; } @@ -966,7 +854,7 @@ void CMapLoaderJson::readHeader(const bool complete) { for(auto & mod : header["mods"].Vector()) { - CModInfo::VerificationInfo info; + ModVerificationInfo info; info.version = CModVersion::fromString(mod["version"].String()); info.checksum = mod["checksum"].Integer(); info.name = mod["name"].String(); @@ -1158,7 +1046,7 @@ void CMapLoaderJson::MapObjectLoader::construct() if(typeName.empty()) { logGlobal->error("Object type missing"); - logGlobal->debug(configuration.toJson()); + logGlobal->debug(configuration.toString()); return; } @@ -1178,20 +1066,20 @@ void CMapLoaderJson::MapObjectLoader::construct() else if(subtypeName.empty()) { logGlobal->error("Object subtype missing"); - logGlobal->debug(configuration.toJson()); + logGlobal->debug(configuration.toString()); return; } auto handler = VLC->objtypeh->getHandlerFor( ModScope::scopeMap(), typeName, subtypeName); - auto * appearance = new ObjectTemplate; + auto appearance = std::make_shared(); appearance->id = Obj(handler->getIndex()); appearance->subid = handler->getSubIndex(); appearance->readJson(configuration["template"], false); // Will be destroyed soon and replaced with shared template - instance = handler->create(std::shared_ptr(appearance)); + instance = handler->create(owner->map->cb, appearance); instance->id = ObjectInstanceID(static_cast(owner->map->objects.size())); instance->instanceName = jsonKey; @@ -1229,7 +1117,7 @@ void CMapLoaderJson::MapObjectLoader::configure() else if(art->ID == Obj::ARTIFACT) { //specific artifact - artID = ArtifactID(art->subID); + artID = art->getArtifact(); } art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID.getNum()); @@ -1263,8 +1151,23 @@ void CMapLoaderJson::readObjects() std::sort(map->heroesOnMap.begin(), map->heroesOnMap.end(), [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) { - return a->subID < b->subID; + return a->getObjTypeIndex() < b->getObjTypeIndex(); }); + + + std::set debugHeroesOnMap; + for (auto const & object : map->objects) + { + if(object->ID != Obj::HERO && object->ID != Obj::PRISON) + continue; + + auto * hero = dynamic_cast(object.get()); + + if (debugHeroesOnMap.count(hero->getHeroType())) + logGlobal->error("Hero is already on the map at %s", hero->visitablePos().toString()); + + debugHeroesOnMap.insert(hero->getHeroType()); + } } void CMapLoaderJson::readTranslations() @@ -1295,7 +1198,7 @@ CMapSaverJson::~CMapSaverJson() = default; void CMapSaverJson::addToArchive(const JsonNode & data, const std::string & filename) { std::ostringstream out; - JsonWriter writer(out); + JsonWriter writer(out, false); writer.writeNode(data); out.flush(); @@ -1423,7 +1326,7 @@ void CMapSaverJson::writeTerrain() void CMapSaverJson::writeObjects() { logGlobal->trace("Saving objects"); - JsonNode data(JsonNode::JsonType::DATA_STRUCT); + JsonNode data; JsonSerializer handler(mapObjectResolver.get(), data); @@ -1437,7 +1340,7 @@ void CMapSaverJson::writeObjects() if(map->grailPos.valid()) { - JsonNode grail(JsonNode::JsonType::DATA_STRUCT); + JsonNode grail; grail["type"].String() = "grail"; grail["x"].Float() = map->grailPos.x; @@ -1470,7 +1373,7 @@ void CMapSaverJson::writeTranslations() if(Languages::getLanguageOptions(language).identifier.empty()) { logGlobal->error("Serializing of unsupported language %s is not permitted", language); - continue;; + continue; } logGlobal->trace("Saving translations, language: %s", language); addToArchive(s.second, language + ".json"); diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index 6efe186b4..117e9f2bb 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -11,7 +11,6 @@ #pragma once #include "CMapService.h" -#include "../JsonNode.h" #include "../filesystem/CZipSaver.h" #include "../filesystem/CZipLoader.h" @@ -168,7 +167,7 @@ public: * * @return a unique ptr of the loaded map class */ - std::unique_ptr loadMap() override; + std::unique_ptr loadMap(IGameCallback * cb) override; /** * Loads the VCMI/Json map header. diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index afa50c338..f51334a2a 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -11,7 +11,6 @@ #include "StdInc.h" #include "MapIdentifiersH3M.h" -#include "../JsonNode.h" #include "../VCMI_Lib.h" #include "../CTownHandler.h" #include "../CHeroHandler.h" @@ -29,7 +28,7 @@ void MapIdentifiersH3M::loadMapping(std::map & resul for (auto entry : mapping.Struct()) { IdentifierID sourceID (entry.second.Integer()); - IdentifierID targetID (*VLC->identifiers()->getIdentifier(entry.second.meta, identifierName, entry.first)); + IdentifierID targetID (*VLC->identifiers()->getIdentifier(entry.second.getModScope(), identifierName, entry.first)); result[sourceID] = targetID; } @@ -37,15 +36,18 @@ void MapIdentifiersH3M::loadMapping(std::map & resul void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) { + if (!mapping["supported"].Bool()) + throw std::runtime_error("Unsupported map format!"); + for (auto entryFaction : mapping["buildings"].Struct()) { - FactionID factionID (*VLC->identifiers()->getIdentifier(entryFaction.second.meta, "faction", entryFaction.first)); + FactionID factionID (*VLC->identifiers()->getIdentifier(entryFaction.second.getModScope(), "faction", entryFaction.first)); auto buildingMap = entryFaction.second; for (auto entryBuilding : buildingMap.Struct()) { BuildingID sourceID (entryBuilding.second.Integer()); - BuildingID targetID (*VLC->identifiers()->getIdentifier(entryBuilding.second.meta, "building." + VLC->factions()->getById(factionID)->getJsonKey(), entryBuilding.first)); + BuildingID targetID (*VLC->identifiers()->getIdentifier(entryBuilding.second.getModScope(), "building." + VLC->factions()->getById(factionID)->getJsonKey(), entryBuilding.first)); mappingFactionBuilding[factionID][sourceID] = targetID; } @@ -68,7 +70,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) { for (auto entryInner : entryOuter.second.Struct()) { - auto handler = VLC->objtypeh->getHandlerFor( entryInner.second.meta, entryOuter.first, entryInner.first); + auto handler = VLC->objtypeh->getHandlerFor( entryInner.second.getModScope(), entryOuter.first, entryInner.first); auto entryValues = entryInner.second.Vector(); ObjectTypeIdentifier h3mID{Obj(entryValues[0].Integer()), int32_t(entryValues[1].Integer())}; @@ -78,7 +80,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) } else { - auto handler = VLC->objtypeh->getHandlerFor( entryOuter.second.meta, entryOuter.first, entryOuter.first); + auto handler = VLC->objtypeh->getHandlerFor( entryOuter.second.getModScope(), entryOuter.first, entryOuter.first); auto entryValues = entryOuter.second.Vector(); ObjectTypeIdentifier h3mID{Obj(entryValues[0].Integer()), int32_t(entryValues[1].Integer())}; diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 772c251de..d2db92dff 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -81,6 +81,20 @@ ArtifactID MapReaderH3M::readArtifact() return ArtifactID::NONE; } +ArtifactID MapReaderH3M::readArtifact8() +{ + ArtifactID result(reader->readUInt8()); + + if(result.getNum() == 0xff) + return ArtifactID::NONE; + + if (result.getNum() < features.artifactsCount) + return remapIdentifier(result); + + logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); + return ArtifactID::NONE; +} + ArtifactID MapReaderH3M::readArtifact32() { ArtifactID result(reader->readInt32()); @@ -135,7 +149,7 @@ CreatureID MapReaderH3M::readCreature() return CreatureID::NONE; if(result.getNum() < features.creaturesCount) - return remapIdentifier(result);; + return remapIdentifier(result); // this may be random creature in army/town, to be randomized later CreatureID randomIndex(result.getNum() - features.creatureIdentifierInvalid - 1); @@ -152,7 +166,7 @@ TerrainId MapReaderH3M::readTerrain() { TerrainId result(readUInt8()); assert(result.getNum() < features.terrainsCount); - return remapIdentifier(result);; + return remapIdentifier(result); } RoadId MapReaderH3M::readRoad() @@ -169,11 +183,18 @@ RiverId MapReaderH3M::readRiver() return result; } +PrimarySkill MapReaderH3M::readPrimary() +{ + PrimarySkill result(readUInt8()); + assert(result <= PrimarySkill::KNOWLEDGE ); + return result; +} + SecondarySkill MapReaderH3M::readSkill() { SecondarySkill result(readUInt8()); assert(result.getNum() < features.skillsCount); - return remapIdentifier(result);; + return remapIdentifier(result); } SpellID MapReaderH3M::readSpell() @@ -185,7 +206,7 @@ SpellID MapReaderH3M::readSpell() return SpellID::PRESET; assert(result.getNum() < features.spellsCount); - return remapIdentifier(result);; + return remapIdentifier(result); } SpellID MapReaderH3M::readSpell32() @@ -197,6 +218,13 @@ SpellID MapReaderH3M::readSpell32() return result; } +GameResID MapReaderH3M::readGameResID() +{ + GameResID result(readInt8()); + assert(result.getNum() < features.resourcesCount); + return result; +} + PlayerColor MapReaderH3M::readPlayer() { uint8_t value = readUInt8(); @@ -266,12 +294,12 @@ void MapReaderH3M::readBitmaskHeroClassesSized(std::set & dest, boo readBitmask(dest, classesBytes, classesCount, invert); } -void MapReaderH3M::readBitmaskHeroes(std::vector & dest, bool invert) +void MapReaderH3M::readBitmaskHeroes(std::set & dest, bool invert) { readBitmask(dest, features.heroesBytes, features.heroesCount, invert); } -void MapReaderH3M::readBitmaskHeroesSized(std::vector & dest, bool invert) +void MapReaderH3M::readBitmaskHeroesSized(std::set & dest, bool invert) { uint32_t heroesCount = readUInt32(); uint32_t heroesBytes = (heroesCount + 7) / 8; @@ -280,12 +308,12 @@ void MapReaderH3M::readBitmaskHeroesSized(std::vector & dest, bool invert) readBitmask(dest, heroesBytes, heroesCount, invert); } -void MapReaderH3M::readBitmaskArtifacts(std::vector &dest, bool invert) +void MapReaderH3M::readBitmaskArtifacts(std::set &dest, bool invert) { readBitmask(dest, features.artifactsBytes, features.artifactsCount, invert); } -void MapReaderH3M::readBitmaskArtifactsSized(std::vector &dest, bool invert) +void MapReaderH3M::readBitmaskArtifactsSized(std::set &dest, bool invert) { uint32_t artifactsCount = reader->readUInt32(); uint32_t artifactsBytes = (artifactsCount + 7) / 8; @@ -294,28 +322,18 @@ void MapReaderH3M::readBitmaskArtifactsSized(std::vector &dest, bool inver readBitmask(dest, artifactsBytes, artifactsCount, invert); } -void MapReaderH3M::readBitmaskSpells(std::vector & dest, bool invert) -{ - readBitmask(dest, features.spellsBytes, features.spellsCount, invert); -} - void MapReaderH3M::readBitmaskSpells(std::set & dest, bool invert) { readBitmask(dest, features.spellsBytes, features.spellsCount, invert); } -void MapReaderH3M::readBitmaskSkills(std::vector & dest, bool invert) -{ - readBitmask(dest, features.skillsBytes, features.skillsCount, invert); -} - void MapReaderH3M::readBitmaskSkills(std::set & dest, bool invert) { readBitmask(dest, features.skillsBytes, features.skillsCount, invert); } template -void MapReaderH3M::readBitmask(std::vector & dest, const int bytesToRead, const int objectsToRead, bool invert) +void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) { for(int byte = 0; byte < bytesToRead; ++byte) { @@ -331,26 +349,15 @@ void MapReaderH3M::readBitmask(std::vector & dest, const int bytesToRead, Identifier h3mID(index); Identifier vcmiID = remapIdentifier(h3mID); - if (vcmiID.getNum() >= dest.size()) - dest.resize(vcmiID.getNum() + 1); - dest[vcmiID.getNum()] = result; + if (result) + dest.insert(vcmiID); + else + dest.erase(vcmiID); } } } } -template -void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) -{ - std::vector bitmap; - bitmap.resize(objectsToRead, false); - readBitmask(bitmap, bytesToRead, objectsToRead, invert); - - for(int i = 0; i < bitmap.size(); i++) - if(bitmap[i]) - dest.insert(static_cast(i)); -} - int3 MapReaderH3M::readInt3() { int3 p; @@ -400,32 +407,35 @@ bool MapReaderH3M::readBool() return result != 0; } -ui8 MapReaderH3M::readUInt8() +int8_t MapReaderH3M::readInt8Checked(int8_t lowerLimit, int8_t upperLimit) +{ + int8_t result = readInt8(); + assert(result >= lowerLimit); + assert(result <= upperLimit); + return std::clamp(result, lowerLimit, upperLimit); +} + +uint8_t MapReaderH3M::readUInt8() { return reader->readUInt8(); } -si8 MapReaderH3M::readInt8() +int8_t MapReaderH3M::readInt8() { return reader->readInt8(); } -ui16 MapReaderH3M::readUInt16() +uint16_t MapReaderH3M::readUInt16() { return reader->readUInt16(); } -si16 MapReaderH3M::readInt16() -{ - return reader->readInt16(); -} - -ui32 MapReaderH3M::readUInt32() +uint32_t MapReaderH3M::readUInt32() { return reader->readUInt32(); } -si32 MapReaderH3M::readInt32() +int32_t MapReaderH3M::readInt32() { return reader->readInt32(); } @@ -435,9 +445,4 @@ std::string MapReaderH3M::readBaseString() return reader->readBaseString(); } -CBinaryReader & MapReaderH3M::getInternalReader() -{ - return *reader; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index 9c34aba3d..42b446ba5 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -32,6 +32,7 @@ public: void setIdentifierRemapper(const MapIdentifiersH3M & remapper); ArtifactID readArtifact(); + ArtifactID readArtifact8(); ArtifactID readArtifact32(); CreatureID readCreature(); HeroTypeID readHero(); @@ -39,9 +40,11 @@ public: TerrainId readTerrain(); RoadId readRoad(); RiverId readRiver(); + PrimarySkill readPrimary(); SecondarySkill readSkill(); SpellID readSpell(); SpellID readSpell32(); + GameResID readGameResID(); PlayerColor readPlayer(); PlayerColor readPlayer32(); @@ -50,13 +53,11 @@ public: void readBitmaskPlayers(std::set & dest, bool invert); void readBitmaskResources(std::set & dest, bool invert); void readBitmaskHeroClassesSized(std::set & dest, bool invert); - void readBitmaskHeroes(std::vector & dest, bool invert); - void readBitmaskHeroesSized(std::vector & dest, bool invert); - void readBitmaskArtifacts(std::vector & dest, bool invert); - void readBitmaskArtifactsSized(std::vector & dest, bool invert); - void readBitmaskSpells(std::vector & dest, bool invert); + void readBitmaskHeroes(std::set & dest, bool invert); + void readBitmaskHeroesSized(std::set & dest, bool invert); + void readBitmaskArtifacts(std::set & dest, bool invert); + void readBitmaskArtifactsSized(std::set & dest, bool invert); void readBitmaskSpells(std::set & dest, bool invert); - void readBitmaskSkills(std::vector & dest, bool invert); void readBitmaskSkills(std::set & dest, bool invert); int3 readInt3(); @@ -70,17 +71,17 @@ public: bool readBool(); - ui8 readUInt8(); - si8 readInt8(); - ui16 readUInt16(); - si16 readInt16(); - ui32 readUInt32(); - si32 readInt32(); + uint8_t readUInt8(); + int8_t readInt8(); + int8_t readInt8Checked(int8_t lowerLimit, int8_t upperLimit); + + uint16_t readUInt16(); + + uint32_t readUInt32(); + int32_t readInt32(); std::string readBaseString(); - CBinaryReader & getInternalReader(); - private: template Identifier remapIdentifier(const Identifier & identifier); @@ -88,9 +89,6 @@ private: template void readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert); - template - void readBitmask(std::vector & dest, int bytesToRead, int objectsToRead, bool invert); - MapFormatFeaturesH3M features; MapIdentifiersH3M remapper; diff --git a/lib/mapping/ObstacleProxy.cpp b/lib/mapping/ObstacleProxy.cpp index fd295572b..920afc4e3 100644 --- a/lib/mapping/ObstacleProxy.cpp +++ b/lib/mapping/ObstacleProxy.cpp @@ -13,6 +13,7 @@ #include "../mapping/CMap.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGObjectInstance.h" #include "../mapObjects/ObjectTemplate.h" VCMI_LIB_NAMESPACE_BEGIN @@ -65,7 +66,7 @@ bool ObstacleProxy::isProhibited(const rmg::Area& objArea) const return false; }; -int ObstacleProxy::getWeightedObjects(const int3 & tile, CRandomGenerator & rand, std::list & allObjects, std::vector> & weightedObjects) +int ObstacleProxy::getWeightedObjects(const int3 & tile, CRandomGenerator & rand, IGameCallback * cb, std::list & allObjects, std::vector> & weightedObjects) { int maxWeight = std::numeric_limits::min(); for(auto & possibleObstacle : possibleObstacles) @@ -79,21 +80,32 @@ int ObstacleProxy::getWeightedObjects(const int3 & tile, CRandomGenerator & rand for(const auto & temp : shuffledObstacles) { auto handler = VLC->objtypeh->getHandlerFor(temp->id, temp->subid); - auto * obj = handler->create(temp); + auto * obj = handler->create(nullptr, temp); allObjects.emplace_back(*obj); rmg::Object * rmgObject = &allObjects.back(); for(const auto & offset : obj->getBlockedOffsets()) { - rmgObject->setPosition(tile - offset); + auto newPos = tile - offset; - if(!isInTheMap(rmgObject->getPosition())) + if(!isInTheMap(newPos)) continue; - if(!rmgObject->getArea().getSubarea([this](const int3 & t) + rmgObject->setPosition(newPos); + + bool isInTheMapEntirely = true; + for (const auto & t : rmgObject->getArea().getTiles()) + { + if (!isInTheMap(t)) + { + isInTheMapEntirely = false; + break; + } + + } + if (!isInTheMapEntirely) { - return !isInTheMap(t); - }).empty()) continue; + } if(isProhibited(rmgObject->getArea())) continue; @@ -135,7 +147,7 @@ int ObstacleProxy::getWeightedObjects(const int3 & tile, CRandomGenerator & rand return maxWeight; } -std::set ObstacleProxy::createObstacles(CRandomGenerator & rand) +std::set ObstacleProxy::createObstacles(CRandomGenerator & rand, IGameCallback * cb) { //reverse order, since obstacles begin in bottom-right corner, while the map coordinates begin in top-left auto blockedTiles = blockedArea.getTilesVector(); @@ -148,7 +160,7 @@ std::set ObstacleProxy::createObstacles(CRandomGenerator & ra std::list allObjects; std::vector> weightedObjects; - int maxWeight = getWeightedObjects(tile, rand, allObjects, weightedObjects); + int maxWeight = getWeightedObjects(tile, rand, cb, allObjects, weightedObjects); if(weightedObjects.empty()) { @@ -210,7 +222,7 @@ bool EditorObstaclePlacer::isInTheMap(const int3& tile) std::set EditorObstaclePlacer::placeObstacles(CRandomGenerator & rand) { - auto obstacles = createObstacles(rand); + auto obstacles = createObstacles(rand, map->cb); finalInsertion(map->getEditManager(), obstacles); return obstacles; } diff --git a/lib/mapping/ObstacleProxy.h b/lib/mapping/ObstacleProxy.h index 70f1f46df..3f6e92f09 100644 --- a/lib/mapping/ObstacleProxy.h +++ b/lib/mapping/ObstacleProxy.h @@ -19,6 +19,7 @@ class CMapEditManager; class CGObjectInstance; class ObjectTemplate; class CRandomGenerator; +class IGameCallback; class DLL_LINKAGE ObstacleProxy { @@ -41,7 +42,7 @@ public: virtual void placeObject(rmg::Object & object, std::set & instances); - virtual std::set createObstacles(CRandomGenerator & rand); + virtual std::set createObstacles(CRandomGenerator & rand, IGameCallback * cb); virtual bool isInTheMap(const int3& tile) = 0; @@ -50,7 +51,7 @@ public: virtual void postProcess(const rmg::Object& object) {}; protected: - int getWeightedObjects(const int3& tile, CRandomGenerator& rand, std::list& allObjects, std::vector>& weightedObjects); + int getWeightedObjects(const int3& tile, CRandomGenerator& rand, IGameCallback * cb, std::list& allObjects, std::vector>& weightedObjects); rmg::Area blockedArea; diff --git a/lib/modding/ActiveModsInSaveList.cpp b/lib/modding/ActiveModsInSaveList.cpp new file mode 100644 index 000000000..264e122ae --- /dev/null +++ b/lib/modding/ActiveModsInSaveList.cpp @@ -0,0 +1,112 @@ +/* + * ActiveModsInSaveList.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "ActiveModsInSaveList.h" + +#include "../VCMI_Lib.h" +#include "CModInfo.h" +#include "CModHandler.h" +#include "ModIncompatibility.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::vector ActiveModsInSaveList::getActiveMods() +{ + return VLC->modh->getActiveMods(); +} + +const ModVerificationInfo & ActiveModsInSaveList::getVerificationInfo(TModID mod) +{ + return VLC->modh->getModInfo(mod).getVerificationInfo(); +} + +void ActiveModsInSaveList::verifyActiveMods(const std::vector> & modList) +{ + auto searchVerificationInfo = [&modList](const TModID & m) -> const ModVerificationInfo* + { + for(auto & i : modList) + if(i.first == m) + return &i.second; + return nullptr; + }; + + std::vector missingMods, excessiveMods; + ModIncompatibility::ModListWithVersion missingModsResult; + ModIncompatibility::ModList excessiveModsResult; + + for(const auto & m : VLC->modh->getActiveMods()) + { + if(searchVerificationInfo(m)) + continue; + + //TODO: support actual disabling of these mods + if(VLC->modh->getModInfo(m).checkModGameplayAffecting()) + excessiveMods.push_back(m); + } + + for(const auto & infoPair : modList) + { + auto & remoteModId = infoPair.first; + auto & remoteModInfo = infoPair.second; + + bool modAffectsGameplay = remoteModInfo.impactsGameplay; + //parent mod affects gameplay if child affects too + for(const auto & subInfoPair : modList) + modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); + + if(!vstd::contains(VLC->modh->getAllMods(), remoteModId)) + { + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //mod is not installed + continue; + } + + auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo(); + modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting(); + bool modVersionCompatible = localModInfo.version.isNull() + || remoteModInfo.version.isNull() + || localModInfo.version.compatible(remoteModInfo.version); + bool modLocalyEnabled = vstd::contains(VLC->modh->getActiveMods(), remoteModId); + + if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) + continue; + + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //incompatible mod impacts gameplay + } + + //filter mods + for(auto & m : missingMods) + { + if(auto * vInfo = searchVerificationInfo(m)) + { + assert(vInfo->parent != m); + if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) + continue; + missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); + } + } + for(auto & m : excessiveMods) + { + auto & vInfo = VLC->modh->getModInfo(m).getVerificationInfo(); + assert(vInfo.parent != m); + if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) + continue; + excessiveModsResult.push_back(vInfo.name); + } + + if(!missingModsResult.empty() || !excessiveModsResult.empty()) + throw ModIncompatibility(missingModsResult, excessiveModsResult); + + //TODO: support actual enabling of required mods +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ActiveModsInSaveList.h b/lib/modding/ActiveModsInSaveList.h new file mode 100644 index 000000000..9798b1ac8 --- /dev/null +++ b/lib/modding/ActiveModsInSaveList.h @@ -0,0 +1,51 @@ +/* + * ActiveModsInSaveList.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +#include "ModVerificationInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class ActiveModsInSaveList +{ + std::vector getActiveMods(); + const ModVerificationInfo & getVerificationInfo(TModID mod); + + /// Checks whether provided mod list is compatible with current VLC and throws on failure + void verifyActiveMods(const std::vector> & modList); +public: + template void serialize(Handler &h) + { + if(h.saving) + { + std::vector activeMods = getActiveMods(); + h & activeMods; + for(const auto & m : activeMods) + h & getVerificationInfo(m); + } + else + { + std::vector saveActiveMods; + h & saveActiveMods; + + std::vector> saveModInfos(saveActiveMods.size()); + for(int i = 0; i < saveActiveMods.size(); ++i) + { + saveModInfos[i].first = saveActiveMods[i]; + h & saveModInfos[i].second; + } + + verifyActiveMods(saveModInfos); + } + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 983e80130..f05bdab6f 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -21,9 +21,11 @@ #include "../CStopWatch.h" #include "../GameSettings.h" #include "../Languages.h" +#include "../MetaString.h" #include "../ScriptHandler.h" #include "../constants/StringConstants.h" #include "../filesystem/Filesystem.h" +#include "../json/JsonUtils.h" #include "../spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -133,6 +135,24 @@ std::vector CModHandler::validateAndSortDependencies(std::vector (); + + auto addErrorMessage = [this](const std::string & textID, const std::string & brokenModID, const std::string & missingModID) + { + modLoadErrors->appendTextID(textID); + + if (allMods.count(brokenModID)) + modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name); + else + modLoadErrors->replaceRawString(brokenModID); + + if (allMods.count(missingModID)) + modLoadErrors->replaceRawString(allMods.at(missingModID).getVerificationInfo().name); + else + modLoadErrors->replaceRawString(missingModID); + + }; + // Left mods have unresolved dependencies, output all to log. for(const auto & brokenModID : modsToResolve) { @@ -140,17 +160,17 @@ std::vector CModHandler::validateAndSortDependencies(std::vector error("Mod '%s' has been disabled: dependency '%s' is missing.", brokenMod.getVerificationInfo().name, dependency); + addErrorMessage("vcmi.server.errors.modNoDependency", brokenModID, dependency); } for(const TModID & conflict : brokenMod.conflicts) { if(vstd::contains(resolvedModIDs, conflict)) - logMod->error("Mod '%s' has been disabled: conflicts with enabled mod '%s'.", brokenMod.getVerificationInfo().name, conflict); + addErrorMessage("vcmi.server.errors.modConflict", brokenModID, conflict); } for(const TModID & reverseConflict : resolvedModIDs) { if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID)) - logMod->error("Mod '%s' has been disabled: conflicts with enabled mod '%s'.", brokenMod.getVerificationInfo().name, reverseConflict); + addErrorMessage("vcmi.server.errors.modConflict", brokenModID, reverseConflict); } } return sortedValidMods; @@ -206,7 +226,10 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName))) { - CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName))); + JsonParsingSettings settings; + settings.mode = JsonParsingSettings::JsonFormatMode::JSON; // TODO: remove once Android launcher with its strict parser is gone + + CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName), settings)); if (!parent.empty()) // this is submod, add parent to dependencies mod.dependencies.insert(parent); @@ -218,24 +241,17 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co } } -void CModHandler::loadMods(bool onlyEssential) +void CModHandler::loadMods() { JsonNode modConfig; - if(onlyEssential) - { - loadOneMod("vcmi", "", modConfig, true);//only vcmi and submods - } - else - { - modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); - loadMods("", "", modConfig["activeMods"], true); - } + modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); + loadMods("", "", modConfig["activeMods"], true); coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); } -std::vector CModHandler::getAllMods() +std::vector CModHandler::getAllMods() const { std::vector modlist; modlist.reserve(allMods.size()); @@ -244,11 +260,16 @@ std::vector CModHandler::getAllMods() return modlist; } -std::vector CModHandler::getActiveMods() +std::vector CModHandler::getActiveMods() const { return activeMods; } +std::string CModHandler::getModLoadErrors() const +{ + return modLoadErrors->toString(); +} + const CModInfo & CModHandler::getModInfo(const TModID & modId) const { return allMods.at(modId); @@ -320,22 +341,27 @@ void CModHandler::loadModFilesystems() } } -TModID CModHandler::findResourceOrigin(const ResourcePath & name) +TModID CModHandler::findResourceOrigin(const ResourcePath & name) const { - for(const auto & modID : boost::adaptors::reverse(activeMods)) + try { - if(CResourceHandler::get(modID)->existsResource(name)) - return modID; + for(const auto & modID : boost::adaptors::reverse(activeMods)) + { + if(CResourceHandler::get(modID)->existsResource(name)) + return modID; + } + + if(CResourceHandler::get("core")->existsResource(name)) + return "core"; + + if(CResourceHandler::get("mapEditor")->existsResource(name)) + return "core"; // Workaround for loading maps via map editor } - - if(CResourceHandler::get("core")->existsResource(name)) - return "core"; - - if(CResourceHandler::get("mapEditor")->existsResource(name)) - return "core"; // Workaround for loading maps via map editor - - assert(0); - return ""; + catch( const std::out_of_range & e) + { + // no-op + } + throw std::runtime_error("Resource with name " + name.getName() + " and type " + EResTypeHelper::getEResTypeAsString(name.getType()) + " wasn't found."); } std::string CModHandler::getModLanguage(const TModID& modId) const @@ -489,90 +515,8 @@ void CModHandler::afterLoad(bool onlyEssential) if(!onlyEssential) { std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); - file << modSettings.toJson(); + file << modSettings.toString(); } - -} - -void CModHandler::trySetActiveMods(const std::vector> & modList) -{ - auto searchVerificationInfo = [&modList](const TModID & m) -> const CModInfo::VerificationInfo* - { - for(auto & i : modList) - if(i.first == m) - return &i.second; - return nullptr; - }; - - std::vector missingMods, excessiveMods; - ModIncompatibility::ModListWithVersion missingModsResult; - ModIncompatibility::ModList excessiveModsResult; - - for(const auto & m : activeMods) - { - if(searchVerificationInfo(m)) - continue; - - //TODO: support actual disabling of these mods - if(getModInfo(m).checkModGameplayAffecting()) - excessiveMods.push_back(m); - } - - for(const auto & infoPair : modList) - { - auto & remoteModId = infoPair.first; - auto & remoteModInfo = infoPair.second; - - bool modAffectsGameplay = remoteModInfo.impactsGameplay; - //parent mod affects gameplay if child affects too - for(const auto & subInfoPair : modList) - modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); - - if(!allMods.count(remoteModId)) - { - if(modAffectsGameplay) - missingMods.push_back(remoteModId); //mod is not installed - continue; - } - - auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); - modAffectsGameplay |= getModInfo(remoteModId).checkModGameplayAffecting(); - bool modVersionCompatible = localModInfo.version.isNull() - || remoteModInfo.version.isNull() - || localModInfo.version.compatible(remoteModInfo.version); - bool modLocalyEnabled = vstd::contains(activeMods, remoteModId); - - if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) - continue; - - if(modAffectsGameplay) - missingMods.push_back(remoteModId); //incompatible mod impacts gameplay - } - - //filter mods - for(auto & m : missingMods) - { - if(auto * vInfo = searchVerificationInfo(m)) - { - assert(vInfo->parent != m); - if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) - continue; - missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); - } - } - for(auto & m : excessiveMods) - { - auto & vInfo = getModInfo(m).getVerificationInfo(); - assert(vInfo.parent != m); - if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) - continue; - excessiveModsResult.push_back(vInfo.name); - } - - if(!missingModsResult.empty() || !excessiveModsResult.empty()) - throw ModIncompatibility(missingModsResult, excessiveModsResult); - - //TODO: support actual enabling of required mods } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index af0959fe9..30eea7a38 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -9,25 +9,28 @@ */ #pragma once -#include "CModInfo.h" - VCMI_LIB_NAMESPACE_BEGIN class CModHandler; class CModIndentifier; +class CModInfo; +struct CModVersion; class JsonNode; class IHandlerBase; class CIdentifierStorage; class CContentHandler; +struct ModVerificationInfo; class ResourcePath; +class MetaString; using TModID = std::string; -class DLL_LINKAGE CModHandler : boost::noncopyable +class DLL_LINKAGE CModHandler final : boost::noncopyable { std::map allMods; std::vector activeMods;//active mods, in order in which they were loaded std::unique_ptr coreMod; + mutable std::unique_ptr modLoadErrors; bool hasCircularDependency(const TModID & mod, std::set currentList = std::set()) const; @@ -50,27 +53,27 @@ class DLL_LINKAGE CModHandler : boost::noncopyable CModVersion getModVersion(TModID modName) const; - /// Attempt to set active mods according to provided list of mods from save, throws on failure - void trySetActiveMods(const std::vector> & modList); - public: std::shared_ptr content; //(!)Do not serialize FIXME: make private /// receives list of available mods and trying to load mod.json from all of them void initializeConfig(); - void loadMods(bool onlyEssential = false); + void loadMods(); void loadModFilesystems(); /// returns ID of mod that provides selected file resource - TModID findResourceOrigin(const ResourcePath & name); + TModID findResourceOrigin(const ResourcePath & name) const; std::string getModLanguage(const TModID & modId) const; std::set getModDependencies(const TModID & modId, bool & isModFound) const; /// returns list of all (active) mods - std::vector getAllMods(); - std::vector getActiveMods(); + std::vector getAllMods() const; + std::vector getActiveMods() const; + + /// Returns human-readable string that describes erros encounter during mod loading, such as missing dependencies + std::string getModLoadErrors() const; const CModInfo & getModInfo(const TModID & modId) const; @@ -79,32 +82,7 @@ public: void afterLoad(bool onlyEssential); CModHandler(); - virtual ~CModHandler(); - - template void serialize(Handler &h, const int version) - { - if(h.saving) - { - h & activeMods; - for(const auto & m : activeMods) - h & getModInfo(m).getVerificationInfo(); - } - else - { - loadMods(); - std::vector saveActiveMods; - h & saveActiveMods; - - std::vector> saveModInfos(saveActiveMods.size()); - for(int i = 0; i < saveActiveMods.size(); ++i) - { - saveModInfos[i].first = saveActiveMods[i]; - h & saveModInfos[i].second; - } - - trySetActiveMods(saveModInfos); - } - } + ~CModHandler(); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 0e534da45..8690c17d0 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -18,10 +18,20 @@ VCMI_LIB_NAMESPACE_BEGIN static JsonNode addMeta(JsonNode config, const std::string & meta) { - config.setMeta(meta); + config.setModScope(meta); return config; } +std::set CModInfo::readModList(const JsonNode & input) +{ + std::set result; + + for (auto const & string : input.convertTo>()) + result.insert(boost::to_lower_copy(string)); + + return result; +} + CModInfo::CModInfo(): explicitlyEnabled(false), implicitlyEnabled(true), @@ -32,14 +42,18 @@ CModInfo::CModInfo(): CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): identifier(identifier), - dependencies(config["depends"].convertTo>()), - conflicts(config["conflicts"].convertTo>()), + dependencies(readModList(config["depends"])), + conflicts(readModList(config["conflicts"])), explicitlyEnabled(false), implicitlyEnabled(true), validation(PENDING), config(addMeta(config, identifier)) { - verificationInfo.name = config["name"].String(); + if (!config["name"].String().empty()) + verificationInfo.name = config["name"].String(); + else + verificationInfo.name = identifier; + verificationInfo.version = CModVersion::fromString(config["version"].String()); verificationInfo.parent = identifier.substr(0, identifier.find_last_of('.')); if(verificationInfo.parent == identifier) @@ -113,7 +127,7 @@ void CModInfo::loadLocalData(const JsonNode & data) if (config["modType"].String() == "Translation") { - if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) + if (baseLanguage != CGeneralTextHandler::getPreferredLanguage()) { if (identifier.find_last_of('.') == std::string::npos) logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); @@ -177,8 +191,9 @@ bool CModInfo::checkModGameplayAffecting() const return *modGameplayAffecting; } -const CModInfo::VerificationInfo & CModInfo::getVerificationInfo() const +const ModVerificationInfo & CModInfo::getVerificationInfo() const { + assert(!verificationInfo.name.empty()); return verificationInfo; } diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index f9f227e2a..3d6b40320 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -9,19 +9,18 @@ */ #pragma once -#include "../JsonNode.h" -#include "CModVersion.h" +#include "../json/JsonNode.h" +#include "ModVerificationInfo.h" VCMI_LIB_NAMESPACE_BEGIN -using TModID = std::string; - class DLL_LINKAGE CModInfo { /// cached result of checkModGameplayAffecting() call /// Do not serialize - depends on local mod version, not server/save mod version mutable std::optional modGameplayAffecting; + static std::set readModList(const JsonNode & input); public: enum EValidationStatus { @@ -30,34 +29,6 @@ public: PASSED }; - struct VerificationInfo - { - /// human-readable mod name - std::string name; - - /// version of the mod - CModVersion version; - - /// CRC-32 checksum of the mod - ui32 checksum = 0; - - /// parent mod ID, empty if root-level mod - TModID parent; - - /// for serialization purposes - bool impactsGameplay = true; - - template - void serialize(Handler & h, const int v) - { - h & name; - h & version; - h & checksum; - h & parent; - h & impactsGameplay; - } - }; - /// identifier, identical to name of folder with mod std::string identifier; @@ -94,7 +65,7 @@ public: /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects bool checkModGameplayAffecting() const; - const VerificationInfo & getVerificationInfo() const; + const ModVerificationInfo & getVerificationInfo() const; private: /// true if mod is enabled by user, e.g. in Launcher UI @@ -103,7 +74,7 @@ private: /// true if mod can be loaded - compatible and has no missing deps bool implicitlyEnabled; - VerificationInfo verificationInfo; + ModVerificationInfo verificationInfo; void loadLocalData(const JsonNode & data); }; diff --git a/lib/modding/CModVersion.h b/lib/modding/CModVersion.h index 15221dab3..2bee49153 100644 --- a/lib/modding/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -10,7 +10,7 @@ #pragma once -#ifdef __UCLIBC__ +#if defined(__UCLIBC__) || defined(__FreeBSD__) || defined(__OpenBSD__) #undef major #undef minor #undef patch @@ -18,6 +18,8 @@ VCMI_LIB_NAMESPACE_BEGIN +using TModID = std::string; + struct DLL_LINKAGE CModVersion { static const int Any = -1; @@ -36,7 +38,7 @@ struct DLL_LINKAGE CModVersion bool compatible(const CModVersion & other, bool checkMinor = false, bool checkPatch = false) const; bool isNull() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & major; h & minor; diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 977ad172b..5817188e8 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -31,6 +31,7 @@ #include "../ScriptHandler.h" #include "../constants/StringConstants.h" #include "../TerrainHandler.h" +#include "../json/JsonUtils.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../rmg/CRmgTemplateStorage.h" #include "../spells/CSpellHandler.h" @@ -44,7 +45,7 @@ ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string { for(auto & node : originalData) { - node.setMeta(ModScope::scopeBuiltin()); + node.setModScope(ModScope::scopeBuiltin()); } } @@ -52,7 +53,7 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const std:: { bool result = false; JsonNode data = JsonUtils::assembleFromFiles(fileList, result); - data.setMeta(modName); + data.setModScope(modName); ModInfo & modInfo = modData[modName]; @@ -103,7 +104,7 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) const std::string & name = entry.first; JsonNode & data = entry.second; - if (data.meta != modName) + if (data.getModScope() != modName) { // in this scenario, entire object record comes from another mod // normally, this is used to "patch" object from another mod (which is legal) @@ -111,15 +112,17 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) // - another mod attempts to add object into this mod (technically can be supported, but might lead to weird edge cases) // - another mod attempts to edit object from this mod that no longer exist - DANGER since such patch likely has very incomplete data // so emit warning and skip such case - logMod->warn("Mod %s attempts to edit object %s from mod %s but no such object exist!", data.meta, name, modName); + logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.getModScope(), name, objectName, modName); continue; } - if (vstd::contains(data.Struct(), "index") && !data["index"].isNull()) - { - if (modName != "core") - logMod->warn("Mod %s is attempting to load original data! This should be reserved for built-in mod.", modName); + bool hasIndex = vstd::contains(data.Struct(), "index") && !data["index"].isNull(); + if (hasIndex && modName != "core") + logMod->error("Mod %s is attempting to load original data! This option is reserved for built-in mod.", modName); + + if (hasIndex && modName == "core") + { // try to add H3 object data size_t index = static_cast(data["index"].Float()); @@ -155,28 +158,55 @@ void ContentTypeHandler::loadCustom() void ContentTypeHandler::afterLoadFinalization() { + for (auto const & data : modData) + { + if (data.second.modData.isNull()) + { + for (auto node : data.second.patches.Struct()) + logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first); + } + + for(auto & otherMod : modData) + { + if (otherMod.first == data.first) + continue; + + if (otherMod.second.modData.isNull()) + continue; + + for(auto & otherObject : otherMod.second.modData.Struct()) + { + if (data.second.modData.Struct().count(otherObject.first)) + { + logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first); + logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first); + } + } + } + } + handler->afterLoadFinalization(); } void CContentHandler::init() { - handlers.insert(std::make_pair("heroClasses", ContentTypeHandler(&VLC->heroh->classes, "heroClass"))); - handlers.insert(std::make_pair("artifacts", ContentTypeHandler(VLC->arth, "artifact"))); - handlers.insert(std::make_pair("creatures", ContentTypeHandler(VLC->creh, "creature"))); - handlers.insert(std::make_pair("factions", ContentTypeHandler(VLC->townh, "faction"))); - handlers.insert(std::make_pair("objects", ContentTypeHandler(VLC->objtypeh, "object"))); - handlers.insert(std::make_pair("heroes", ContentTypeHandler(VLC->heroh, "hero"))); - handlers.insert(std::make_pair("spells", ContentTypeHandler(VLC->spellh, "spell"))); - handlers.insert(std::make_pair("skills", ContentTypeHandler(VLC->skillh, "skill"))); - handlers.insert(std::make_pair("templates", ContentTypeHandler(VLC->tplh, "template"))); + handlers.insert(std::make_pair("heroClasses", ContentTypeHandler(VLC->heroclassesh.get(), "heroClass"))); + handlers.insert(std::make_pair("artifacts", ContentTypeHandler(VLC->arth.get(), "artifact"))); + handlers.insert(std::make_pair("creatures", ContentTypeHandler(VLC->creh.get(), "creature"))); + handlers.insert(std::make_pair("factions", ContentTypeHandler(VLC->townh.get(), "faction"))); + handlers.insert(std::make_pair("objects", ContentTypeHandler(VLC->objtypeh.get(), "object"))); + handlers.insert(std::make_pair("heroes", ContentTypeHandler(VLC->heroh.get(), "hero"))); + handlers.insert(std::make_pair("spells", ContentTypeHandler(VLC->spellh.get(), "spell"))); + handlers.insert(std::make_pair("skills", ContentTypeHandler(VLC->skillh.get(), "skill"))); + handlers.insert(std::make_pair("templates", ContentTypeHandler(VLC->tplh.get(), "template"))); #if SCRIPTING_ENABLED - handlers.insert(std::make_pair("scripts", ContentTypeHandler(VLC->scriptHandler, "script"))); + handlers.insert(std::make_pair("scripts", ContentTypeHandler(VLC->scriptHandler.get(), "script"))); #endif - handlers.insert(std::make_pair("battlefields", ContentTypeHandler(VLC->battlefieldsHandler, "battlefield"))); - handlers.insert(std::make_pair("terrains", ContentTypeHandler(VLC->terrainTypeHandler, "terrain"))); - handlers.insert(std::make_pair("rivers", ContentTypeHandler(VLC->riverTypeHandler, "river"))); - handlers.insert(std::make_pair("roads", ContentTypeHandler(VLC->roadTypeHandler, "road"))); - handlers.insert(std::make_pair("obstacles", ContentTypeHandler(VLC->obstacleHandler, "obstacle"))); + handlers.insert(std::make_pair("battlefields", ContentTypeHandler(VLC->battlefieldsHandler.get(), "battlefield"))); + handlers.insert(std::make_pair("terrains", ContentTypeHandler(VLC->terrainTypeHandler.get(), "terrain"))); + handlers.insert(std::make_pair("rivers", ContentTypeHandler(VLC->riverTypeHandler.get(), "river"))); + handlers.insert(std::make_pair("roads", ContentTypeHandler(VLC->roadTypeHandler.get(), "road"))); + handlers.insert(std::make_pair("obstacles", ContentTypeHandler(VLC->obstacleHandler.get(), "obstacle"))); //TODO: any other types of moddables? } diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h index e1224013e..7093c12d5 100644 --- a/lib/modding/ContentTypeHandler.h +++ b/lib/modding/ContentTypeHandler.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../JsonNode.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index cce9700c5..589a79f44 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -13,7 +13,6 @@ #include "CModHandler.h" #include "ModScope.h" -#include "../JsonNode.h" #include "../VCMI_Lib.h" #include "../constants/StringConstants.h" #include "../spells/CSpellHandler.h" @@ -26,9 +25,9 @@ CIdentifierStorage::CIdentifierStorage() { //TODO: moddable spell schools for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) - registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); + registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id.getNum()); - registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(SpellSchool::ANY)); + registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool::ANY.getNum()); for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); @@ -53,6 +52,10 @@ CIdentifierStorage::CIdentifierStorage() registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementSea", 0); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareGorgon", 0); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareCommander", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareNoRangePenalty", 2); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangePenalty", 3); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareObstaclePenalty", 4); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangeObstaclePenalty", 5); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthRegular", 0); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthSpecial", 1); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsMonsters", 0); @@ -132,6 +135,7 @@ CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameW result.name = typeAndName.second; result.callback = callback; result.optional = optional; + result.dynamicType = true; return result; } @@ -160,6 +164,7 @@ CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameA result.name = typeAndName.second; result.callback = callback; result.optional = optional; + result.dynamicType = false; return result; } @@ -175,12 +180,12 @@ void CIdentifierStorage::requestIdentifier(const std::string & scope, const std: void CIdentifierStorage::requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const { - requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, false)); + requestIdentifier(ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), callback, false)); } void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::function & callback) const { - requestIdentifier(ObjectCallback::fromNameWithType(name.meta, name.String(), callback, false)); + requestIdentifier(ObjectCallback::fromNameWithType(name.getModScope(), name.String(), callback, false)); } void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const @@ -190,65 +195,128 @@ void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const s void CIdentifierStorage::tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const { - requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, true)); + requestIdentifier(ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), callback, true)); } std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) const { assert(state != ELoadingState::LOADING); - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s of type %s from mod %s", name , type ,scope); - - return std::optional(); + auto options = ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent); + return getIdentifierImpl(options, silent); } std::optional CIdentifierStorage::getIdentifier(const std::string & type, const JsonNode & name, bool silent) const { assert(state != ELoadingState::LOADING); - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(name.meta, type, name.String(), std::function(), silent)); + auto options = ObjectCallback::fromNameAndType(name.getModScope(), type, name.String(), std::function(), silent); - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s of type %s from mod %s", name.String(), type, name.meta); - - return std::optional(); + return getIdentifierImpl(options, silent); } std::optional CIdentifierStorage::getIdentifier(const JsonNode & name, bool silent) const { assert(state != ELoadingState::LOADING); - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(name.meta, name.String(), std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s from mod %s", name.String(), name.meta); - - return std::optional(); + auto options = ObjectCallback::fromNameWithType(name.getModScope(), name.String(), std::function(), silent); + return getIdentifierImpl(options, silent); } std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & fullName, bool silent) const { assert(state != ELoadingState::LOADING); - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent)); + auto options = ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent); + return getIdentifierImpl(options, silent); +} + +std::optional CIdentifierStorage::getIdentifierImpl(const ObjectCallback & options, bool silent) const +{ + auto idList = getPossibleIdentifiers(options); if (idList.size() == 1) return idList.front().id; if (!silent) - logMod->error("Failed to resolve identifier %s from mod %s", fullName, scope); - + showIdentifierResolutionErrorDetails(options); return std::optional(); } +void CIdentifierStorage::showIdentifierResolutionErrorDetails(const ObjectCallback & options) const +{ + auto idList = getPossibleIdentifiers(options); + + logMod->error("Failed to resolve identifier '%s' of type '%s' from mod '%s'", options.name, options.type, options.localScope); + + if (options.dynamicType && options.type.empty()) + { + bool suggestionFound = false; + + for (auto const & entry : registeredObjects) + { + if (!boost::algorithm::ends_with(entry.first, options.name)) + continue; + + suggestionFound = true; + logMod->error("Perhaps you wanted to use identifier '%s' from mod '%s' instead?", entry.first, entry.second.scope); + } + + if (suggestionFound) + return; + } + + if (idList.empty()) + { + // check whether identifier is unavailable due to a missing dependency on a mod + ObjectCallback testOptions = options; + testOptions.localScope = ModScope::scopeGame(); + testOptions.remoteScope = {}; + + auto testList = getPossibleIdentifiers(testOptions); + if (testList.empty()) + { + logMod->error("Identifier '%s' of type '%s' does not exists in any loaded mod!", options.name, options.type); + } + else + { + // such identifiers exists, but were not picked for some reason + if (options.remoteScope.empty()) + { + // attempt to access identifier from mods that is not dependency + for (auto const & testOption : testList) + { + logMod->error("Identifier '%s' exists in mod %s", options.name, testOption.scope); + logMod->error("Please add mod '%s' as dependency of mod '%s' to access this identifier", testOption.scope, options.localScope); + } + } + else + { + // attempt to access identifier in form 'modName:object', but identifier is only present in different mod + for (auto const & testOption : testList) + { + logMod->error("Identifier '%s' exists in mod '%s' but identifier was explicitly requested from mod '%s'!", options.name, testOption.scope, options.remoteScope); + if (options.dynamicType) + logMod->error("Please use form '%s.%s' or '%s:%s.%s' to access this identifier", options.type, options.name, testOption.scope, options.type, options.name); + else + logMod->error("Please use form '%s' or '%s:%s' to access this identifier", options.name, testOption.scope, options.name); + } + } + } + } + else + { + logMod->error("Multiple possible candidates:"); + for (auto const & testOption : idList) + { + logMod->error("Identifier %s exists in mod %s", options.name, testOption.scope); + if (options.dynamicType) + logMod->error("Please use '%s:%s.%s' to access this identifier", testOption.scope, options.type, options.name); + else + logMod->error("Please use '%s:%s' to access this identifier", testOption.scope, options.name); + } + } +} + void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier) { assert(state != ELoadingState::FINISHED); @@ -362,17 +430,7 @@ bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const } // error found. Try to generate some debug info - if(identifiers.empty()) - logMod->error("Unknown identifier!"); - else - logMod->error("Ambiguous identifier request!"); - - logMod->error("Request for %s.%s from mod %s", request.type, request.name, request.localScope); - - for(const auto & id : identifiers) - { - logMod->error("\tID is available in mod %s", id.scope); - } + showIdentifierResolutionErrorDetails(request); return false; } @@ -381,26 +439,16 @@ void CIdentifierStorage::finalize() assert(state == ELoadingState::LOADING); state = ELoadingState::FINALIZING; - bool errorsFound = false; while ( !scheduledRequests.empty() ) { // Use local copy since new requests may appear during resolving, invalidating any iterators auto request = scheduledRequests.back(); scheduledRequests.pop_back(); - - if (!resolveIdentifier(request)) - errorsFound = true; + resolveIdentifier(request); } - debugDumpIdentifiers(); - - if (errorsFound) - logMod->error("All known identifiers were dumped into log file"); - - assert(errorsFound == false); state = ELoadingState::FINISHED; - } void CIdentifierStorage::debugDumpIdentifiers() diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h index 004d724c7..6a657d715 100644 --- a/lib/modding/IdentifierStorage.h +++ b/lib/modding/IdentifierStorage.h @@ -32,6 +32,7 @@ class DLL_LINKAGE CIdentifierStorage std::string name; /// string ID std::function callback; bool optional; + bool dynamicType; /// Builds callback from identifier in form "targetMod:type.name" static ObjectCallback fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional); @@ -52,12 +53,6 @@ class DLL_LINKAGE CIdentifierStorage { return id == other.id && scope == other.scope; } - - template void serialize(Handler &h, const int version) - { - h & id; - h & scope; - } }; std::multimap registeredObjects; @@ -75,6 +70,8 @@ class DLL_LINKAGE CIdentifierStorage bool resolveIdentifier(const ObjectCallback & callback) const; std::vector getPossibleIdentifiers(const ObjectCallback & callback) const; + void showIdentifierResolutionErrorDetails(const ObjectCallback & callback) const; + std::optional getIdentifierImpl(const ObjectCallback & callback, bool silent) const; public: CIdentifierStorage(); virtual ~CIdentifierStorage() = default; @@ -102,12 +99,6 @@ public: /// called at the very end of loading to check for any missing ID's void finalize(); - - template void serialize(Handler &h, const int version) - { - h & registeredObjects; - h & state; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModIncompatibility.h b/lib/modding/ModIncompatibility.h index 23af7deaf..1ae784f35 100644 --- a/lib/modding/ModIncompatibility.h +++ b/lib/modding/ModIncompatibility.h @@ -51,7 +51,8 @@ public: } private: - std::string messageMissingMods, messageExcessiveMods; + std::string messageMissingMods; + std::string messageExcessiveMods; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModVerificationInfo.h b/lib/modding/ModVerificationInfo.h new file mode 100644 index 000000000..3737b0e97 --- /dev/null +++ b/lib/modding/ModVerificationInfo.h @@ -0,0 +1,44 @@ +/* + * ModVerificationInfo.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "CModVersion.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct ModVerificationInfo +{ + /// human-readable mod name + std::string name; + + /// version of the mod + CModVersion version; + + /// CRC-32 checksum of the mod + ui32 checksum = 0; + + /// parent mod ID, empty if root-level mod + TModID parent; + + /// for serialization purposes + bool impactsGameplay = true; + + template + void serialize(Handler & h) + { + h & name; + h & version; + h & checksum; + h & parent; + h & impactsGameplay; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkConnection.cpp b/lib/network/NetworkConnection.cpp new file mode 100644 index 000000000..7b31ca04b --- /dev/null +++ b/lib/network/NetworkConnection.cpp @@ -0,0 +1,105 @@ +/* + * NetworkConnection.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "NetworkConnection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +NetworkConnection::NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr & socket) + : socket(socket) + , listener(listener) +{ + socket->set_option(boost::asio::ip::tcp::no_delay(true)); + socket->set_option(boost::asio::socket_base::send_buffer_size(4194304)); + socket->set_option(boost::asio::socket_base::receive_buffer_size(4194304)); +} + +void NetworkConnection::start() +{ + boost::asio::async_read(*socket, + readBuffer, + boost::asio::transfer_exactly(messageHeaderSize), + [self = shared_from_this()](const auto & ec, const auto & endpoint) { self->onHeaderReceived(ec); }); +} + +void NetworkConnection::onHeaderReceived(const boost::system::error_code & ecHeader) +{ + if (ecHeader) + { + listener.onDisconnected(shared_from_this(), ecHeader.message()); + return; + } + + if (readBuffer.size() < messageHeaderSize) + throw std::runtime_error("Failed to read header!"); + + uint32_t messageSize; + readBuffer.sgetn(reinterpret_cast(&messageSize), sizeof(messageSize)); + + if (messageSize > messageMaxSize) + { + listener.onDisconnected(shared_from_this(), "Invalid packet size!"); + return; + } + + if (messageSize == 0) + { + listener.onDisconnected(shared_from_this(), "Zero-sized packet!"); + return; + } + + boost::asio::async_read(*socket, + readBuffer, + boost::asio::transfer_exactly(messageSize), + [self = shared_from_this(), messageSize](const auto & ecPayload, const auto & endpoint) { self->onPacketReceived(ecPayload, messageSize); }); +} + +void NetworkConnection::onPacketReceived(const boost::system::error_code & ec, uint32_t expectedPacketSize) +{ + if (ec) + { + listener.onDisconnected(shared_from_this(), ec.message()); + return; + } + + if (readBuffer.size() < expectedPacketSize) + { + throw std::runtime_error("Failed to read packet!"); + } + + std::vector message(expectedPacketSize); + readBuffer.sgetn(reinterpret_cast(message.data()), expectedPacketSize); + listener.onPacketReceived(shared_from_this(), message); + + start(); +} + +void NetworkConnection::sendPacket(const std::vector & message) +{ + boost::system::error_code ec; + + // create array with single element - boost::asio::buffer can be constructed from containers, but not from plain integer + std::array messageSize{static_cast(message.size())}; + + boost::asio::write(*socket, boost::asio::buffer(messageSize), ec ); + boost::asio::write(*socket, boost::asio::buffer(message), ec ); + + //Note: ignoring error code, intended +} + +void NetworkConnection::close() +{ + boost::system::error_code ec; + socket->close(ec); + + //NOTE: ignoring error code, intended +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkConnection.h b/lib/network/NetworkConnection.h new file mode 100644 index 000000000..beaaba376 --- /dev/null +++ b/lib/network/NetworkConnection.h @@ -0,0 +1,37 @@ +/* + * NetworkConnection.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "NetworkDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class NetworkConnection : public INetworkConnection, public std::enable_shared_from_this +{ + static const int messageHeaderSize = sizeof(uint32_t); + static const int messageMaxSize = 64 * 1024 * 1024; // arbitrary size to prevent potential massive allocation if we receive garbage input + + std::shared_ptr socket; + + NetworkBuffer readBuffer; + INetworkConnectionListener & listener; + + void onHeaderReceived(const boost::system::error_code & ec); + void onPacketReceived(const boost::system::error_code & ec, uint32_t expectedPacketSize); + +public: + NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr & socket); + + void start(); + void close() override; + void sendPacket(const std::vector & message) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkDefines.h b/lib/network/NetworkDefines.h new file mode 100644 index 000000000..6b86ff23a --- /dev/null +++ b/lib/network/NetworkDefines.h @@ -0,0 +1,24 @@ +/* + * NetworkDefines.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include + +#include "NetworkInterface.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using NetworkContext = boost::asio::io_service; +using NetworkSocket = boost::asio::ip::tcp::socket; +using NetworkAcceptor = boost::asio::ip::tcp::acceptor; +using NetworkBuffer = boost::asio::streambuf; +using NetworkTimer = boost::asio::steady_timer; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkHandler.cpp b/lib/network/NetworkHandler.cpp new file mode 100644 index 000000000..ddb15e091 --- /dev/null +++ b/lib/network/NetworkHandler.cpp @@ -0,0 +1,71 @@ +/* + * NetworkHandler.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "NetworkHandler.h" + +#include "NetworkServer.h" +#include "NetworkConnection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::unique_ptr INetworkHandler::createHandler() +{ + return std::make_unique(); +} + +NetworkHandler::NetworkHandler() + : io(std::make_shared()) +{} + +std::unique_ptr NetworkHandler::createServerTCP(INetworkServerListener & listener) +{ + return std::make_unique(listener, io); +} + +void NetworkHandler::connectToRemote(INetworkClientListener & listener, const std::string & host, uint16_t port) +{ + auto socket = std::make_shared(*io); + boost::asio::ip::tcp::resolver resolver(*io); + auto endpoints = resolver.resolve(host, std::to_string(port)); + boost::asio::async_connect(*socket, endpoints, [socket, &listener](const boost::system::error_code& error, const boost::asio::ip::tcp::endpoint& endpoint) + { + if (error) + { + listener.onConnectionFailed(error.message()); + return; + } + auto connection = std::make_shared(listener, socket); + connection->start(); + + listener.onConnectionEstablished(connection); + }); +} + +void NetworkHandler::run() +{ + boost::asio::executor_work_guardget_executor())> work{io->get_executor()}; + io->run(); +} + +void NetworkHandler::createTimer(INetworkTimerListener & listener, std::chrono::milliseconds duration) +{ + auto timer = std::make_shared(*io, duration); + timer->async_wait([&listener, timer](const boost::system::error_code& error){ + if (!error) + listener.onTimer(); + }); +} + +void NetworkHandler::stop() +{ + io->stop(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkHandler.h b/lib/network/NetworkHandler.h new file mode 100644 index 000000000..c5b326c67 --- /dev/null +++ b/lib/network/NetworkHandler.h @@ -0,0 +1,31 @@ +/* + * NetworkHandler.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "NetworkDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class NetworkHandler : public INetworkHandler +{ + std::shared_ptr io; + +public: + NetworkHandler(); + + std::unique_ptr createServerTCP(INetworkServerListener & listener) override; + void connectToRemote(INetworkClientListener & listener, const std::string & host, uint16_t port) override; + void createTimer(INetworkTimerListener & listener, std::chrono::milliseconds duration) override; + + void run() override; + void stop() override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkInterface.h b/lib/network/NetworkInterface.h new file mode 100644 index 000000000..58bd11023 --- /dev/null +++ b/lib/network/NetworkInterface.h @@ -0,0 +1,106 @@ +/* + * NetworkHandler.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +/// Base class for connections with other services, either incoming or outgoing +class DLL_LINKAGE INetworkConnection : boost::noncopyable +{ +public: + virtual ~INetworkConnection() = default; + virtual void sendPacket(const std::vector & message) = 0; + virtual void close() = 0; +}; + +using NetworkConnectionPtr = std::shared_ptr; +using NetworkConnectionWeakPtr = std::weak_ptr; + +/// Base class for outgoing connections support +class DLL_LINKAGE INetworkClient : boost::noncopyable +{ +public: + virtual ~INetworkClient() = default; + + virtual bool isConnected() const = 0; + virtual void sendPacket(const std::vector & message) = 0; +}; + +/// Base class for incoming connections support +class DLL_LINKAGE INetworkServer : boost::noncopyable +{ +public: + virtual ~INetworkServer() = default; + + virtual void start(uint16_t port) = 0; +}; + +/// Base interface that must be implemented by user of networking API to handle any connection callbacks +class DLL_LINKAGE INetworkConnectionListener +{ +public: + virtual void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) = 0; + virtual void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) = 0; + + virtual ~INetworkConnectionListener() = default; +}; + +/// Interface that must be implemented by user of networking API to handle outgoing connection callbacks +class DLL_LINKAGE INetworkClientListener : public INetworkConnectionListener +{ +public: + virtual void onConnectionFailed(const std::string & errorMessage) = 0; + virtual void onConnectionEstablished(const std::shared_ptr &) = 0; +}; + +/// Interface that must be implemented by user of networking API to handle incoming connection callbacks +class DLL_LINKAGE INetworkServerListener : public INetworkConnectionListener +{ +public: + virtual void onNewConnection(const std::shared_ptr &) = 0; +}; + +/// Interface that must be implemented by user of networking API to handle timers on network thread +class DLL_LINKAGE INetworkTimerListener +{ +public: + virtual ~INetworkTimerListener() = default; + + virtual void onTimer() = 0; +}; + +/// Main class for handling of all network activity +class DLL_LINKAGE INetworkHandler : boost::noncopyable +{ +public: + virtual ~INetworkHandler() = default; + + /// Constructs default implementation + static std::unique_ptr createHandler(); + + /// Creates an instance of TCP server that allows to receiving connections on a local port + virtual std::unique_ptr createServerTCP(INetworkServerListener & listener) = 0; + + /// Creates an instance of TCP client that allows to establish single outgoing connection to a remote port + /// On success: INetworkTimerListener::onConnectionEstablished() will be called, established connection provided as parameter + /// On failure: INetworkTimerListener::onConnectionFailed will be called with human-readable error message + virtual void connectToRemote(INetworkClientListener & listener, const std::string & host, uint16_t port) = 0; + + /// Creates a timer that will be called once, after specified interval has passed + /// On success: INetworkTimerListener::onTimer() will be called + /// On failure: no-op + virtual void createTimer(INetworkTimerListener & listener, std::chrono::milliseconds duration) = 0; + + /// Starts network processing on this thread. Does not returns until networking processing has been terminated + virtual void run() = 0; + virtual void stop() = 0; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkServer.cpp b/lib/network/NetworkServer.cpp new file mode 100644 index 000000000..b408ed525 --- /dev/null +++ b/lib/network/NetworkServer.cpp @@ -0,0 +1,62 @@ +/* + * NetworkServer.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "NetworkServer.h" +#include "NetworkConnection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +NetworkServer::NetworkServer(INetworkServerListener & listener, const std::shared_ptr & context) + : io(context) + , listener(listener) +{ +} + +void NetworkServer::start(uint16_t port) +{ + acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)); + startAsyncAccept(); +} + +void NetworkServer::startAsyncAccept() +{ + auto upcomingConnection = std::make_shared(*io); + acceptor->async_accept(*upcomingConnection, [this, upcomingConnection](const auto & ec) { connectionAccepted(upcomingConnection, ec); }); +} + +void NetworkServer::connectionAccepted(std::shared_ptr upcomingConnection, const boost::system::error_code & ec) +{ + if(ec) + { + throw std::runtime_error("Something wrong during accepting: " + ec.message()); + } + + logNetwork->info("We got a new connection! :)"); + auto connection = std::make_shared(*this, upcomingConnection); + connections.insert(connection); + connection->start(); + listener.onNewConnection(connection); + startAsyncAccept(); +} + +void NetworkServer::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + logNetwork->info("Connection lost! Reason: %s", errorMessage); + assert(connections.count(connection)); + connections.erase(connection); + listener.onDisconnected(connection, errorMessage); +} + +void NetworkServer::onPacketReceived(const std::shared_ptr & connection, const std::vector & message) +{ + listener.onPacketReceived(connection, message); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/network/NetworkServer.h b/lib/network/NetworkServer.h new file mode 100644 index 000000000..8fc0e8988 --- /dev/null +++ b/lib/network/NetworkServer.h @@ -0,0 +1,35 @@ +/* + * NetworkServer.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "NetworkDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class NetworkServer : public INetworkConnectionListener, public INetworkServer +{ + std::shared_ptr io; + std::shared_ptr acceptor; + std::set> connections; + + INetworkServerListener & listener; + + void connectionAccepted(std::shared_ptr, const boost::system::error_code & ec); + void startAsyncAccept(); + + void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; + void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; +public: + NetworkServer(INetworkServerListener & listener, const std::shared_ptr & context); + + void start(uint16_t port) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/ArtifactLocation.h b/lib/networkPacks/ArtifactLocation.h index 777a1fdf5..42739083f 100644 --- a/lib/networkPacks/ArtifactLocation.h +++ b/lib/networkPacks/ArtifactLocation.h @@ -9,67 +9,40 @@ */ #pragma once -#include "../ConstTransitivePtr.h" #include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN -class CGHeroInstance; -class CStackInstance; -class CArmedInstance; -class CArtifactSet; -class CBonusSystemNode; -struct ArtSlotInfo; - -using TArtHolder = std::variant, ConstTransitivePtr>; - struct ArtifactLocation { - TArtHolder artHolder;//TODO: identify holder by id - ArtifactPosition slot = ArtifactPosition::PRE_FIRST; + ObjectInstanceID artHolder; + ArtifactPosition slot; + std::optional creature; ArtifactLocation() - : artHolder(ConstTransitivePtr()) + : artHolder(ObjectInstanceID::NONE) + , slot(ArtifactPosition::PRE_FIRST) + , creature(std::nullopt) { } - template - ArtifactLocation(const T * ArtHolder, ArtifactPosition Slot) - : artHolder(const_cast(ArtHolder)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) + ArtifactLocation(const ObjectInstanceID id, const ArtifactPosition & slot = ArtifactPosition::PRE_FIRST) + : artHolder(id) + , slot(slot) + , creature(std::nullopt) { } - ArtifactLocation(TArtHolder ArtHolder, const ArtifactPosition & Slot) - : artHolder(std::move(std::move(ArtHolder))) - , slot(Slot) + ArtifactLocation(const ObjectInstanceID id, const std::optional creatureSlot) + : artHolder(id) + , slot(ArtifactPosition::PRE_FIRST) + , creature(creatureSlot) { } - template - bool isHolder(const T *t) const - { - if(auto ptrToT = std::get>(artHolder)) - { - return ptrToT == t; - } - return false; - } - - DLL_LINKAGE void removeArtifact(); // BE CAREFUL, this operation modifies holder (gs) - - DLL_LINKAGE const CArmedInstance *relatedObj() const; //hero or the stack owner - DLL_LINKAGE PlayerColor owningPlayer() const; - DLL_LINKAGE CArtifactSet *getHolderArtSet(); - DLL_LINKAGE CBonusSystemNode *getHolderNode(); - DLL_LINKAGE CArtifactSet *getHolderArtSet() const; - DLL_LINKAGE const CBonusSystemNode *getHolderNode() const; - - DLL_LINKAGE const CArtifactInstance *getArt() const; - DLL_LINKAGE CArtifactInstance *getArt(); - DLL_LINKAGE const ArtSlotInfo *getSlot() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler & h) { h & artHolder; h & slot; + h & creature; } }; diff --git a/lib/networkPacks/BattleChanges.h b/lib/networkPacks/BattleChanges.h index 510505e23..1a06195bb 100644 --- a/lib/networkPacks/BattleChanges.h +++ b/lib/networkPacks/BattleChanges.h @@ -9,7 +9,7 @@ */ #pragma once -#include "JsonNode.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -47,7 +47,7 @@ public: { } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; h & healthDelta; @@ -69,7 +69,7 @@ public: { } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; h & data; diff --git a/lib/networkPacks/Component.h b/lib/networkPacks/Component.h index fa764e085..91170f10c 100644 --- a/lib/networkPacks/Component.h +++ b/lib/networkPacks/Component.h @@ -9,44 +9,67 @@ */ #pragma once +#include "../constants/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + VCMI_LIB_NAMESPACE_BEGIN -class CStackBasicDescriptor; +enum class ComponentType : int8_t +{ + NONE = -1, + PRIM_SKILL, + SEC_SKILL, + RESOURCE, + RESOURCE_PER_DAY, + CREATURE, + ARTIFACT, + SPELL_SCROLL, + MANA, + EXPERIENCE, + LEVEL, + SPELL, + MORALE, + LUCK, + BUILDING, + HERO_PORTRAIT, + FLAG +}; + +using ComponentSubType = VariantIdentifier; struct Component { - enum class EComponentType : uint8_t - { - PRIM_SKILL, - SEC_SKILL, - RESOURCE, - CREATURE, - ARTIFACT, - EXPERIENCE, - SPELL, - MORALE, - LUCK, - BUILDING, - HERO_PORTRAIT, - FLAG, - INVALID //should be last - }; - EComponentType id = EComponentType::INVALID; - ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels - si32 val = 0; // + give; - take - si16 when = 0; // 0 - now; +x - within x days; -x - per x days + ComponentType type = ComponentType::NONE; + ComponentSubType subType; + std::optional value; // + give; - take - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { - h & id; - h & subtype; - h & val; - h & when; + h & type; + h & subType; + h & value; } + Component() = default; - DLL_LINKAGE explicit Component(const CStackBasicDescriptor &stack); - Component(Component::EComponentType Type, ui16 Subtype, si32 Val, si16 When) - :id(Type),subtype(Subtype),val(Val),when(When) + + template, bool> = true> + Component(ComponentType type, Numeric value) + : type(type) + , value(value) + { + } + + template, bool> = true> + Component(ComponentType type, IdentifierType subType) + : type(type) + , subType(subType) + { + } + + Component(ComponentType type, ComponentSubType subType, int32_t value) + : type(type) + , subType(subType) + , value(value) { } }; diff --git a/lib/networkPacks/EntityChanges.h b/lib/networkPacks/EntityChanges.h index 296148297..f502c309d 100644 --- a/lib/networkPacks/EntityChanges.h +++ b/lib/networkPacks/EntityChanges.h @@ -9,9 +9,9 @@ */ #pragma once -#include +#include "../json/JsonNode.h" -#include "../JsonNode.h" +#include VCMI_LIB_NAMESPACE_BEGIN @@ -21,7 +21,7 @@ public: Metatype metatype = Metatype::UNKNOWN; int32_t entityIndex = 0; JsonNode data; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & metatype; h & entityIndex; diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index 72d8dce9d..fe1d9181e 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -87,7 +87,6 @@ public: virtual void visitInfoWindow(InfoWindow & pack) {} virtual void visitSetObjectProperty(SetObjectProperty & pack) {} virtual void visitChangeObjectVisitors(ChangeObjectVisitors & pack) {} - virtual void visitPrepareHeroLevelUp(PrepareHeroLevelUp & pack) {} virtual void visitHeroLevelUp(HeroLevelUp & pack) {} virtual void visitCommanderLevelUp(CommanderLevelUp & pack) {} virtual void visitBlockingDialog(BlockingDialog & pack) {} @@ -152,7 +151,8 @@ public: virtual void visitLobbyChatMessage(LobbyChatMessage & pack) {} virtual void visitLobbyGuiAction(LobbyGuiAction & pack) {} virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) {} - virtual void visitLobbyEndGame(LobbyEndGame & pack) {} + virtual void visitLobbyRestartGame(LobbyRestartGame & pack) {} + virtual void visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) {} virtual void visitLobbyStartGame(LobbyStartGame & pack) {} virtual void visitLobbyChangeHost(LobbyChangeHost & pack) {} virtual void visitLobbyUpdateState(LobbyUpdateState & pack) {} @@ -165,6 +165,7 @@ public: virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) {} virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {} virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {} + virtual void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) {} virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) {} virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) {} virtual void visitLobbyShowMessage(LobbyShowMessage & pack) {} diff --git a/lib/networkPacks/NetPacksBase.h b/lib/networkPacks/NetPacksBase.h index dc22af57e..c158a2953 100644 --- a/lib/networkPacks/NetPacksBase.h +++ b/lib/networkPacks/NetPacksBase.h @@ -20,12 +20,14 @@ class ICPackVisitor; struct DLL_LINKAGE CPack { - std::shared_ptr c; // Pointer to connection that pack received from + /// Pointer to connection that pack received from + /// Only set & used on server + std::shared_ptr c; CPack() = default; virtual ~CPack() = default; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { logNetwork->error("CPack serialized... this should not happen!"); assert(false && "CPack serialized"); @@ -62,9 +64,9 @@ struct DLL_LINKAGE Query : public CPackForClient struct DLL_LINKAGE CPackForServer : public CPack { mutable PlayerColor player = PlayerColor::NEUTRAL; - mutable si32 requestID; + mutable uint32_t requestID = 0; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & player; h & requestID; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index cc6e6a4d4..00ed57775 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -34,6 +34,9 @@ #include "TerrainHandler.h" #include "mapObjects/CGCreature.h" #include "mapObjects/CGMarket.h" +#include "mapObjects/CGTownInstance.h" +#include "mapObjects/CQuest.h" +#include "mapObjects/MiscObjects.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "campaign/CampaignState.h" @@ -380,11 +383,6 @@ void ChangeObjectVisitors::visitTyped(ICPackVisitor & visitor) visitor.visitChangeObjectVisitors(*this); } -void PrepareHeroLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPrepareHeroLevelUp(*this); -} - void HeroLevelUp::visitTyped(ICPackVisitor & visitor) { visitor.visitHeroLevelUp(*this); @@ -710,9 +708,9 @@ void LobbyLoadProgress::visitTyped(ICPackVisitor & visitor) visitor.visitLobbyLoadProgress(*this); } -void LobbyEndGame::visitTyped(ICPackVisitor & visitor) +void LobbyRestartGame::visitTyped(ICPackVisitor & visitor) { - visitor.visitLobbyEndGame(*this); + visitor.visitLobbyRestartGame(*this); } void LobbyStartGame::visitTyped(ICPackVisitor & visitor) @@ -720,6 +718,11 @@ void LobbyStartGame::visitTyped(ICPackVisitor & visitor) visitor.visitLobbyStartGame(*this); } +void LobbyPrepareStartGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyPrepareStartGame(*this); +} + void LobbyChangeHost::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbyChangeHost(*this); @@ -775,6 +778,11 @@ void LobbySetTurnTime::visitTyped(ICPackVisitor & visitor) visitor.visitLobbySetTurnTime(*this); } +void LobbySetExtraOptions::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetExtraOptions(*this); +} + void LobbySetDifficulty::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbySetDifficulty(*this); @@ -797,6 +805,7 @@ void SetResources::applyGs(CGameState * gs) const gs->getPlayerState(player)->resources = res; else gs->getPlayerState(player)->resources += res; + gs->getPlayerState(player)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); //just ensure that player resources are not negative //server is responsible to check if player can afford deal @@ -858,7 +867,7 @@ void AddQuest::applyGs(CGameState * gs) const void UpdateArtHandlerLists::applyGs(CGameState * gs) const { - VLC->arth->allocatedArtifacts = allocatedArtifacts; + gs->allocatedArtifacts = allocatedArtifacts; } void UpdateMapEvents::applyGs(CGameState * gs) const @@ -932,7 +941,7 @@ void SetMovePoints::applyGs(CGameState * gs) const void FoWChange::applyGs(CGameState *gs) { TeamState * team = gs->getPlayerTeam(player); - auto fogOfWarMap = team->fogOfWarMap; + auto & fogOfWarMap = team->fogOfWarMap; for(const int3 & t : tiles) (*fogOfWarMap)[t.z][t.x][t.y] = mode != ETileVisibility::HIDDEN; @@ -944,7 +953,7 @@ void FoWChange::applyGs(CGameState *gs) const CGObjectInstance *o = elem; if (o) { - switch(o->ID) + switch(o->ID.toEnum()) { case Obj::HERO: case Obj::MINE: @@ -963,7 +972,7 @@ void FoWChange::applyGs(CGameState *gs) void SetAvailableHero::applyGs(CGameState *gs) { - gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID); + gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID, replenishPoints); } void GiveBonus::applyGs(CGameState *gs) @@ -971,18 +980,15 @@ void GiveBonus::applyGs(CGameState *gs) CBonusSystemNode *cbsn = nullptr; switch(who) { - case ETarget::HERO: - cbsn = gs->getHero(ObjectInstanceID(id)); + case ETarget::OBJECT: + cbsn = dynamic_cast(gs->getObjInstance(id.as())); break; case ETarget::PLAYER: - cbsn = gs->getPlayerState(PlayerColor(id)); - break; - case ETarget::TOWN: - cbsn = gs->getTown(ObjectInstanceID(id)); + cbsn = gs->getPlayerState(id.as()); break; case ETarget::BATTLE: assert(Bonus::OneBattle(&bonus)); - cbsn = dynamic_cast(gs->getBattle(BattleID(id))); + cbsn = dynamic_cast(gs->getBattle(id.as())); break; } @@ -996,29 +1002,28 @@ void GiveBonus::applyGs(CGameState *gs) std::string &descr = b->description; - if(bdescr.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE)) - { - if (bonus.source == BonusSource::OBJECT_TYPE || bonus.source == BonusSource::OBJECT_INSTANCE) - { - descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" - } - else if(bonus.source == BonusSource::TOWN_STRUCTURE) - { - descr = bonus.description; - return; - } - else - { - descr = bdescr.toString(); - } - } - else + if(!bdescr.empty()) { descr = bdescr.toString(); } - // Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them - boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); - boost::replace_first(descr, "%s", std::to_string(std::abs(bonus.val))); + else if(!descr.empty()) + { + //use preseet description + } + else if((bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE) + && (bonus.source == BonusSource::OBJECT_TYPE || bonus.source == BonusSource::OBJECT_INSTANCE)) + { + //no description, use generic + //?could use allways when Type == BonusDuration::Type::ONE_BATTLE + descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" + } + else + { + logGlobal->debug("Empty bonus decription. Type=%d", (int) bonus.type); + } + // Some of(?) versions of H3 use " %s" here instead of %d. Try to replace both of them + boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); // " +/-%d Temporary until next battle + boost::replace_first(descr, " %s", boost::str(boost::format(" %+d") % bonus.val)); // " %s" in arraytxt.69, fountian of fortune } void ChangeObjPos::applyGs(CGameState *gs) @@ -1065,6 +1070,12 @@ void ChangeObjectVisitors::applyGs(CGameState * gs) const } break; + case VISITOR_GLOBAL: + { + CGObjectInstance * objectPtr = gs->getObjInstance(object); + gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjectsGlobal.insert({objectPtr->ID, objectPtr->subID}); + break; + } case VISITOR_REMOVE: gs->getHero(hero)->visitedObjects.erase(object); break; @@ -1114,11 +1125,20 @@ void PlayerReinitInterface::applyGs(CGameState *gs) void RemoveBonus::applyGs(CGameState *gs) { - CBonusSystemNode * node = nullptr; - if (who == GiveBonus::ETarget::HERO) - node = gs->getHero(ObjectInstanceID(whoID)); - else - node = gs->getPlayerState(PlayerColor(whoID)); + CBonusSystemNode *node = nullptr; + switch(who) + { + case GiveBonus::ETarget::OBJECT: + node = dynamic_cast(gs->getObjInstance(whoID.as())); + break; + case GiveBonus::ETarget::PLAYER: + node = gs->getPlayerState(whoID.as()); + break; + case GiveBonus::ETarget::BATTLE: + assert(Bonus::OneBattle(&bonus)); + node = dynamic_cast(gs->getBattle(whoID.as())); + break; + } BonusList &bonuses = node->getExportedBonusList(); @@ -1141,6 +1161,9 @@ void RemoveObject::applyGs(CGameState *gs) //unblock tiles gs->map->removeBlockVisTiles(obj); + if (initiator.isValidPlayer()) + gs->getPlayerState(initiator)->destroyedObjects.insert(objectID); + if(obj->ID == Obj::HERO) //remove beaten hero { auto * beatenHero = dynamic_cast(obj); @@ -1206,27 +1229,6 @@ void RemoveObject::applyGs(CGameState *gs) } } - for (TriggeredEvent & event : gs->map->triggeredEvents) - { - auto patcher = [&](EventCondition cond) -> EventExpression::Variant - { - if (cond.object == obj) - { - if (cond.condition == EventCondition::DESTROY || cond.condition == EventCondition::DESTROY_0) - { - cond.condition = EventCondition::CONST_VALUE; - cond.value = 1; // destroyed object, from now on always fulfilled - } - else if (cond.condition == EventCondition::CONTROL || cond.condition == EventCondition::HAVE_0) - { - cond.condition = EventCondition::CONST_VALUE; - cond.value = 0; // destroyed object, from now on can not be fulfilled - } - } - return cond; - }; - event.trigger = event.trigger.morph(patcher); - } gs->map->instanceNames.erase(obj->instanceName); gs->map->objects[objectID.getNum()].dellNull(); gs->map->calculateGuardingGreaturePositions(); @@ -1321,7 +1323,7 @@ void TryMoveHero::applyGs(CGameState *gs) gs->map->addBlockVisTiles(h); } - auto fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; + auto & fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; for(const int3 & t : fowRevealed) (*fogOfWarMap)[t.z][t.x][t.y] = 1; } @@ -1485,8 +1487,9 @@ void NewObject::applyGs(CGameState *gs) auto handler = VLC->objtypeh->getHandlerFor(ID, subID); - CGObjectInstance * o = handler->create(); + CGObjectInstance * o = handler->create(gs->callback, nullptr); handler->configureObject(o, gs->getRandomGenerator()); + assert(o->ID == this->ID); if (ID == Obj::MONSTER) //probably more options will be needed { @@ -1498,13 +1501,13 @@ void NewObject::applyGs(CGameState *gs) cre->character = 2; cre->gainedArtifact = ArtifactID::NONE; cre->identifier = -1; - cre->addToSlot(SlotID(0), new CStackInstance(CreatureID(subID), -1)); //add placeholder stack + cre->addToSlot(SlotID(0), new CStackInstance(subID.getNum(), -1)); //add placeholder stack } assert(!handler->getTemplates(terrainType).empty()); if (handler->getTemplates().empty()) { - logGlobal->error("Attempt to create object (%d %d) with no templates!", ID, subID); + logGlobal->error("Attempt to create object (%d %d) with no templates!", ID, subID.getNum()); return; } @@ -1514,8 +1517,6 @@ void NewObject::applyGs(CGameState *gs) o->appearance = handler->getTemplates().front(); o->id = ObjectInstanceID(static_cast(gs->map->objects.size())); - o->ID = ID; - o->subID = subID; o->pos = targetPos + o->getVisitableOffset(); gs->map->objects.emplace_back(o); @@ -1574,67 +1575,6 @@ struct GetBase } }; - -void ArtifactLocation::removeArtifact() -{ - CArtifactInstance *a = getArt(); - assert(a); - a->removeFrom(*this); -} - -const CArmedInstance * ArtifactLocation::relatedObj() const -{ - return std::visit(ObjectRetriever(), artHolder); -} - -PlayerColor ArtifactLocation::owningPlayer() const -{ - const auto * obj = relatedObj(); - return obj ? obj->tempOwner : PlayerColor::NEUTRAL; -} - -CArtifactSet *ArtifactLocation::getHolderArtSet() -{ - return std::visit(GetBase(), artHolder); -} - -CBonusSystemNode *ArtifactLocation::getHolderNode() -{ - return std::visit(GetBase(), artHolder); -} - -const CArtifactInstance *ArtifactLocation::getArt() const -{ - const auto * s = getSlot(); - if(s) - return s->getArt(); - else - return nullptr; -} - -CArtifactSet * ArtifactLocation::getHolderArtSet() const -{ - auto * t = const_cast(this); - return t->getHolderArtSet(); -} - -const CBonusSystemNode * ArtifactLocation::getHolderNode() const -{ - auto * t = const_cast(this); - return t->getHolderNode(); -} - -CArtifactInstance *ArtifactLocation::getArt() -{ - const ArtifactLocation *t = this; - return const_cast(t->getArt()); -} - -const ArtSlotInfo *ArtifactLocation::getSlot() const -{ - return getHolderArtSet()->getSlot(slot); -} - void ChangeStackCount::applyGs(CGameState * gs) { auto * srcObj = gs->getArmyInstance(army); @@ -1709,39 +1649,40 @@ void RebalanceStacks::applyGs(CGameState * gs) if(srcCount == count) //moving whole stack { - [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); + const auto c = dst.army->getCreature(dst.slot); if(c) //stack at dest -> merge { assert(c == srcType); - auto alHere = ArtifactLocation (src.getStack(), ArtifactPosition::CREATURE_SLOT); - auto alDest = ArtifactLocation (dst.getStack(), ArtifactPosition::CREATURE_SLOT); - auto * artHere = alHere.getArt(); - auto * artDest = alDest.getArt(); - if (artHere) + + const auto srcHero = dynamic_cast(src.army.get()); + const auto dstHero = dynamic_cast(dst.army.get()); + auto srcStack = const_cast(src.getStack()); + auto dstStack = const_cast(dst.getStack()); + if(auto srcArt = srcStack->getArt(ArtifactPosition::CREATURE_SLOT)) { - if (alDest.getArt()) + if(auto dstArt = dstStack->getArt(ArtifactPosition::CREATURE_SLOT)) { - auto * hero = dynamic_cast(src.army.get()); - auto dstSlot = ArtifactUtils::getArtBackpackPosition(hero, alDest.getArt()->getTypeId()); - if(hero && dstSlot != ArtifactPosition::PRE_FIRST) + auto dstSlot = ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId()); + if(srcHero && dstSlot != ArtifactPosition::PRE_FIRST) { - artDest->move (alDest, ArtifactLocation (hero, dstSlot)); + dstArt->move(*dstStack, ArtifactPosition::CREATURE_SLOT, *srcHero, dstSlot); } //else - artifact cna be lost :/ else { EraseArtifact ea; - ea.al = alDest; + ea.al = ArtifactLocation(dstHero->id, ArtifactPosition::CREATURE_SLOT); + ea.al.creature = dst.slot; ea.applyGs(gs); logNetwork->warn("Cannot move artifact! No free slots"); } - artHere->move (alHere, alDest); + srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); //TODO: choose from dialog } else //just move to the other slot before stack gets erased { - artHere->move (alHere, alDest); + srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); } } if (stackExp) @@ -1811,53 +1752,57 @@ void BulkSmartRebalanceStacks::applyGs(CGameState * gs) void PutArtifact::applyGs(CGameState *gs) { - assert(art->canBePutAt(al)); // Ensure that artifact has been correctly added via NewArtifact pack assert(vstd::contains(gs->map->artInstances, art)); assert(!art->getParentNodes().empty()); - art->putAt(al); + auto hero = gs->getHero(al.artHolder); + assert(hero); + assert(art && art->canBePutAt(hero, al.slot)); + art->putAt(*hero, al.slot); } void EraseArtifact::applyGs(CGameState *gs) { - const auto * slot = al.getSlot(); + const auto artSet = gs->getArtSet(al.artHolder); + assert(artSet); + const auto slot = artSet->getSlot(al.slot); if(slot->locked) { logGlobal->debug("Erasing locked artifact: %s", slot->artifact->artType->getNameTranslated()); DisassembledArtifact dis; dis.al.artHolder = al.artHolder; - auto * aset = al.getHolderArtSet(); - #ifndef NDEBUG - bool found = false; - #endif - for(auto& p : aset->artifactsWorn) + + for(auto & slotInfo : artSet->artifactsWorn) { - auto art = p.second.artifact; + auto art = slotInfo.second.artifact; if(art->isCombined() && art->isPart(slot->artifact)) { - dis.al.slot = aset->getArtPos(art); - #ifndef NDEBUG - found = true; - #endif + dis.al.slot = artSet->getArtPos(art); break; } } - assert(found && "Failed to determine the assembly this locked artifact belongs to"); - logGlobal->debug("Found the corresponding assembly: %s", dis.al.getSlot()->artifact->artType->getNameTranslated()); + assert((dis.al.slot != ArtifactPosition::PRE_FIRST) && "Failed to determine the assembly this locked artifact belongs to"); + logGlobal->debug("Found the corresponding assembly: %s", artSet->getArt(dis.al.slot)->artType->getNameTranslated()); dis.applyGs(gs); } else { logGlobal->debug("Erasing artifact %s", slot->artifact->artType->getNameTranslated()); } - al.removeArtifact(); + auto art = artSet->getArt(al.slot); + assert(art); + art->removeFrom(*artSet, al.slot); } void MoveArtifact::applyGs(CGameState * gs) { - CArtifactInstance * art = src.getArt(); - assert(!ArtifactUtils::isSlotEquipment(dst.slot) || !dst.getArt()); - art->move(src, dst); + auto srcHero = gs->getArtSet(src); + auto dstHero = gs->getArtSet(dst); + assert(srcHero); + assert(dstHero); + auto art = srcHero->getArt(src.slot); + assert(art && art->canBePutAt(dstHero, dst.slot)); + art->move(*srcHero, src.slot, *dstHero, dst.slot); } void BulkMoveArtifacts::applyGs(CGameState * gs) @@ -1869,8 +1814,8 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) BULK_PUT }; - auto bulkArtsOperation = [this](std::vector & artsPack, - CArtifactSet * artSet, EBulkArtsOp operation) -> void + auto bulkArtsOperation = [this, gs](std::vector & artsPack, + CArtifactSet & artSet, EBulkArtsOp operation) -> void { int numBackpackArtifactsMoved = 0; for(auto & slot : artsPack) @@ -1883,21 +1828,18 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) { srcPos = ArtifactPosition(srcPos.num - numBackpackArtifactsMoved); } - const auto * slotInfo = artSet->getSlot(srcPos); - assert(slotInfo); - auto * art = const_cast(slotInfo->getArt()); + auto * art = artSet.getArt(srcPos); assert(art); switch(operation) { case EBulkArtsOp::BULK_MOVE: - const_cast(art)->move( - ArtifactLocation(srcArtHolder, srcPos), ArtifactLocation(dstArtHolder, slot.dstPos)); + art->move(artSet, srcPos, *gs->getArtSet(ArtifactLocation(dstArtHolder, dstCreature)), slot.dstPos); break; case EBulkArtsOp::BULK_REMOVE: - art->removeFrom(ArtifactLocation(dstArtHolder, srcPos)); + art->removeFrom(artSet, srcPos); break; case EBulkArtsOp::BULK_PUT: - art->putAt(ArtifactLocation(srcArtHolder, slot.dstPos)); + art->putAt(*gs->getArtSet(ArtifactLocation(srcArtHolder, srcCreature)), slot.dstPos); break; default: break; @@ -1910,37 +1852,38 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) } }; + auto * leftSet = gs->getArtSet(ArtifactLocation(srcArtHolder, srcCreature)); if(swap) { // Swap - auto * leftSet = getSrcHolderArtSet(); - auto * rightSet = getDstHolderArtSet(); + auto * rightSet = gs->getArtSet(ArtifactLocation(dstArtHolder, dstCreature)); CArtifactFittingSet artFittingSet(leftSet->bearerType()); artFittingSet.artifactsWorn = rightSet->artifactsWorn; artFittingSet.artifactsInBackpack = rightSet->artifactsInBackpack; - bulkArtsOperation(artsPack1, rightSet, EBulkArtsOp::BULK_REMOVE); - bulkArtsOperation(artsPack0, leftSet, EBulkArtsOp::BULK_MOVE); - bulkArtsOperation(artsPack1, &artFittingSet, EBulkArtsOp::BULK_PUT); + bulkArtsOperation(artsPack1, *rightSet, EBulkArtsOp::BULK_REMOVE); + bulkArtsOperation(artsPack0, *leftSet, EBulkArtsOp::BULK_MOVE); + bulkArtsOperation(artsPack1, artFittingSet, EBulkArtsOp::BULK_PUT); } else { - bulkArtsOperation(artsPack0, getSrcHolderArtSet(), EBulkArtsOp::BULK_MOVE); + bulkArtsOperation(artsPack0, *leftSet, EBulkArtsOp::BULK_MOVE); } } void AssembledArtifact::applyGs(CGameState *gs) { - CArtifactSet * artSet = al.getHolderArtSet(); - const CArtifactInstance * transformedArt = al.getArt(); + auto hero = gs->getHero(al.artHolder); + assert(hero); + const auto transformedArt = hero->getArt(al.slot); assert(transformedArt); - assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->getTypeId()), [=](const CArtifact * art)->bool + assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(hero, transformedArt->getTypeId()), [=](const CArtifact * art)->bool { return art->getId() == builtArt->getId(); })); - const auto transformedArtSlot = artSet->getSlotByInstance(transformedArt); + const auto transformedArtSlot = hero->getSlotByInstance(transformedArt); auto * combinedArt = new CArtifactInstance(builtArt); gs->map->addNewArtifactInstance(combinedArt); @@ -1952,7 +1895,7 @@ void AssembledArtifact::applyGs(CGameState *gs) if(transformedArt->getTypeId() == constituent->getId()) slot = transformedArtSlot; else - slot = artSet->getArtPos(constituent->getId(), false, false); + slot = hero->getArtPos(constituent->getId(), false, false); assert(slot != ArtifactPosition::PRE_FIRST); slotsInvolved.emplace_back(slot); @@ -1972,8 +1915,8 @@ void AssembledArtifact::applyGs(CGameState *gs) break; } - if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), al.slot) - && vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), slot)) + if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(hero->bearerType()), al.slot) + && vstd::contains(combinedArt->artType->getPossibleSlots().at(hero->bearerType()), slot)) al.slot = slot; } else @@ -1986,8 +1929,8 @@ void AssembledArtifact::applyGs(CGameState *gs) // Delete parts from hero for(const auto slot : slotsInvolved) { - const auto constituentInstance = artSet->getArt(slot); - constituentInstance->removeFrom(ArtifactLocation(al.artHolder, slot)); + const auto constituentInstance = hero->getArt(slot); + constituentInstance->removeFrom(*hero, slot); if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot) combinedArt->addPart(constituentInstance, slot); @@ -1996,25 +1939,26 @@ void AssembledArtifact::applyGs(CGameState *gs) } // Put new combined artifacts - combinedArt->putAt(al); + combinedArt->putAt(*hero, al.slot); } void DisassembledArtifact::applyGs(CGameState *gs) { - auto * disassembled = al.getArt(); - assert(disassembled); + auto hero = gs->getHero(al.artHolder); + assert(hero); + auto disassembledArt = hero->getArt(al.slot); + assert(disassembledArt); - auto parts = disassembled->getPartsInfo(); - disassembled->removeFrom(al); + auto parts = disassembledArt->getPartsInfo(); + disassembledArt->removeFrom(*hero, al.slot); for(auto & part : parts) { - ArtifactLocation partLoc = al; // ArtifactPosition::PRE_FIRST is value of main part slot -> it'll replace combined artifact in its pos - partLoc.slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); - disassembled->detachFrom(*part.art); - part.art->putAt(partLoc); + auto slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); + disassembledArt->detachFrom(*part.art); + part.art->putAt(*hero, slot); } - gs->map->eraseArtifactInstance(disassembled); + gs->map->eraseArtifactInstance(disassembledArt); } void HeroVisit::applyGs(CGameState *gs) @@ -2023,9 +1967,9 @@ void HeroVisit::applyGs(CGameState *gs) void SetAvailableArtifacts::applyGs(CGameState * gs) const { - if(id >= 0) + if(id != ObjectInstanceID::NONE) { - if(auto * bm = dynamic_cast(gs->map->objects[id].get())) + if(auto * bm = dynamic_cast(gs->getObjInstance(id))) { bm->artifacts = arts; } @@ -2036,7 +1980,7 @@ void SetAvailableArtifacts::applyGs(CGameState * gs) const } else { - CGTownInstance::merchantArtifacts = arts; + gs->map->townMerchantArtifacts = arts; } } @@ -2069,6 +2013,7 @@ void NewTurn::applyGs(CGameState *gs) { assert(re.first.isValidPlayer()); gs->getPlayerState(re.first)->resources = re.second; + gs->getPlayerState(re.first)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); } for(const auto & creatureSet : cres) //set available creatures in towns @@ -2107,9 +2052,9 @@ void SetObjectProperty::applyGs(CGameState * gs) const if(state->towns.empty()) state->daysWithoutCastle = 0; } - if(PlayerColor(val).isValidPlayer()) + if(identifier.as().isValidPlayer()) { - PlayerState * p = gs->getPlayerState(PlayerColor(val)); + PlayerState * p = gs->getPlayerState(identifier.as()); p->towns.emplace_back(t); //reset counter before NewTurn to avoid no town message if game loaded at turn when one already captured @@ -2120,29 +2065,12 @@ void SetObjectProperty::applyGs(CGameState * gs) const CBonusSystemNode & nodeToMove = cai->whatShouldBeAttached(); nodeToMove.detachFrom(cai->whereShouldBeAttached(gs)); - obj->setProperty(what,val); + obj->setProperty(what, identifier); nodeToMove.attachTo(cai->whereShouldBeAttached(gs)); } else //not an armed instance { - obj->setProperty(what,val); - } -} - -void PrepareHeroLevelUp::applyGs(CGameState * gs) -{ - auto * hero = gs->getHero(heroId); - assert(hero); - - auto proposedSkills = hero->getLevelUpProposedSecondarySkills(); - - if(skills.size() == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically - { - skills.push_back(*RandomGeneratorUtil::nextItem(proposedSkills, hero->skillsInfo.rand)); - } - else - { - skills = proposedSkills; + obj->setProperty(what, identifier); } } @@ -2171,7 +2099,7 @@ void BattleStart::applyGs(CGameState * gs) const info->battleID = gs->nextBattleID; info->localInit(); - gs->nextBattleID = vstd::next(gs->nextBattleID, 1); + gs->nextBattleID = BattleID(gs->nextBattleID.getNum() + 1); } void BattleNextRound::applyGs(CGameState * gs) const @@ -2206,7 +2134,7 @@ void BattleTriggerEffect::applyGs(CGameState * gs) const } case BonusType::POISON: { - auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID(SpellID::POISON)) + auto b = st->getLocalBonus(Selector::source(BonusSource::SPELL_EFFECT, SpellID(SpellID::POISON)) .And(Selector::type()(BonusType::STACK_HEALTH))); if (b) b->val = val; @@ -2327,6 +2255,9 @@ void BattleAttack::applyGs(CGameState * gs) stackAttacked.applyGs(gs); attacker->removeBonusesRecursive(Bonus::UntilAttack); + + if(!this->counter()) + attacker->removeBonusesRecursive(Bonus::UntilOwnAttack); } void StartAction::applyGs(CGameState *gs) @@ -2580,13 +2511,6 @@ void TurnTimeUpdate::applyGs(CGameState *gs) const playerState.turnTimer = turnTimer; } -Component::Component(const CStackBasicDescriptor & stack) - : id(EComponentType::CREATURE) - , subtype(stack.type->getId()) - , val(stack.count) -{ -} - void EntitiesChanged::applyGs(CGameState * gs) { for(const auto & change : changes) @@ -2603,14 +2527,4 @@ const CArtifactInstance * ArtSlotInfo::getArt() const return artifact; } -CArtifactSet * BulkMoveArtifacts::getSrcHolderArtSet() -{ - return std::visit(GetBase(), srcArtHolder); -} - -CArtifactSet * BulkMoveArtifacts::getDstHolderArtSet() -{ - return std::visit(GetBase(), dstArtHolder); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/ObjProperty.h b/lib/networkPacks/ObjProperty.h new file mode 100644 index 000000000..67029d375 --- /dev/null +++ b/lib/networkPacks/ObjProperty.h @@ -0,0 +1,72 @@ +/* + * ObjProperty.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../constants/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +enum class ObjProperty : int8_t +{ + INVALID, + OWNER, + BLOCKVIS, + PRIMARY_STACK_COUNT, + VISITORS, + VISITED, + ID, + AVAILABLE_CREATURE, + MONSTER_COUNT, + MONSTER_POWER, + MONSTER_EXP, + MONSTER_RESTORE_TYPE, + MONSTER_REFUSED_JOIN, + + //town-specific + STRUCTURE_ADD_VISITING_HERO, + STRUCTURE_CLEAR_VISITORS, + STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state + BONUS_VALUE_FIRST, + BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) + + SEERHUT_VISITED, + SEERHUT_COMPLETE, + OBELISK_VISITED, + + //creature-bank specific + BANK_DAYCOUNTER, + BANK_RESET, + BANK_CLEAR, + + //object with reward + REWARD_RANDOMIZE, + REWARD_SELECT, + REWARD_CLEARED +}; + +class NumericID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + static si32 decode(const std::string & identifier) + { + return std::stoi(identifier); + } + static std::string encode(const si32 index) + { + return std::to_string(index); + } +}; + +using ObjPropertyID = VariantIdentifier; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 31bc4752e..85e07e1bd 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -15,6 +15,7 @@ #include "EOpenWindowMode.h" #include "EntityChanges.h" #include "NetPacksBase.h" +#include "ObjProperty.h" #include "../CCreatureSet.h" #include "../MetaString.h" @@ -61,7 +62,7 @@ struct DLL_LINKAGE PackageApplied : public CPackForClient ui32 requestID = 0; //an ID given by client to the request that was applied PlayerColor player; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & result; h & packType; @@ -82,7 +83,7 @@ struct DLL_LINKAGE SystemMessage : public CPackForClient std::string text; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & text; } @@ -99,7 +100,7 @@ struct DLL_LINKAGE PlayerBlocked : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & reason; h & startOrEnd; @@ -117,7 +118,7 @@ struct DLL_LINKAGE PlayerCheated : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & losingCheatCode; @@ -132,7 +133,7 @@ struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient PlayerColor player; TurnTimerInfo turnTimer; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & turnTimer; @@ -147,7 +148,7 @@ struct DLL_LINKAGE PlayerStartsTurn : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & player; @@ -163,7 +164,7 @@ struct DLL_LINKAGE DaysWithoutTown : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & daysWithoutCastle; @@ -178,7 +179,7 @@ struct DLL_LINKAGE EntitiesChanged : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & changes; } @@ -194,7 +195,7 @@ struct DLL_LINKAGE SetResources : public CPackForClient PlayerColor player; ResourceSet res; //res[resid] => res amount - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & abs; h & player; @@ -213,7 +214,7 @@ struct DLL_LINKAGE SetPrimSkill : public CPackForClient PrimarySkill which = PrimarySkill::ATTACK; si64 val = 0; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & abs; h & id; @@ -233,7 +234,7 @@ struct DLL_LINKAGE SetSecSkill : public CPackForClient SecondarySkill which; ui16 val = 0; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & abs; h & id; @@ -249,14 +250,15 @@ struct DLL_LINKAGE HeroVisitCastle : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; ui8 flags = 0; //1 - start - ObjectInstanceID tid, hid; + ObjectInstanceID tid; + ObjectInstanceID hid; bool start() const //if hero is entering castle (if false - leaving) { return flags & 1; } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & flags; h & tid; @@ -274,7 +276,7 @@ struct DLL_LINKAGE ChangeSpells : public CPackForClient ObjectInstanceID hid; std::set spells; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & learn; h & hid; @@ -292,7 +294,7 @@ struct DLL_LINKAGE SetMana : public CPackForClient si32 val = 0; bool absolute = true; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & val; h & hid; @@ -310,7 +312,7 @@ struct DLL_LINKAGE SetMovePoints : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & val; h & hid; @@ -329,7 +331,7 @@ struct DLL_LINKAGE FoWChange : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & tiles; h & player; @@ -351,44 +353,46 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient PlayerColor player; HeroTypeID hid; //HeroTypeID::NONE if no hero CSimpleArmy army; + bool replenishPoints; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & slotID; h & roleID; h & player; h & hid; h & army; + h & replenishPoints; } }; struct DLL_LINKAGE GiveBonus : public CPackForClient { - enum class ETarget : ui8 { HERO, PLAYER, TOWN, BATTLE }; + enum class ETarget : int8_t { OBJECT, PLAYER, BATTLE }; - explicit GiveBonus(ETarget Who = ETarget::HERO) + explicit GiveBonus(ETarget Who = ETarget::OBJECT) :who(Who) { } void applyGs(CGameState * gs); - ETarget who = ETarget::HERO; //who receives bonus - si32 id = 0; //hero. town or player id - whoever receives it + ETarget who = ETarget::OBJECT; + VariantIdentifier id; Bonus bonus; MetaString bdescr; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & bonus; h & id; h & bdescr; h & who; - assert(id != -1); + assert(id.getNum() != -1); } }; @@ -405,7 +409,7 @@ struct DLL_LINKAGE ChangeObjPos : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & objid; h & nPos; @@ -421,7 +425,7 @@ struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; } @@ -436,7 +440,7 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & victoryLossCheckResult; @@ -452,7 +456,7 @@ struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & players; h & playerConnectionId; @@ -461,7 +465,7 @@ struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient struct DLL_LINKAGE RemoveBonus : public CPackForClient { - explicit RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::HERO) + explicit RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::OBJECT) :who(Who) { } @@ -469,7 +473,7 @@ struct DLL_LINKAGE RemoveBonus : public CPackForClient void applyGs(CGameState * gs); GiveBonus::ETarget who; //who receives bonus - ui32 whoID = 0; //hero, town or player id - whoever loses bonus + VariantIdentifier whoID; //vars to identify bonus: its source BonusSource source; @@ -480,7 +484,7 @@ struct DLL_LINKAGE RemoveBonus : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & source; h & id; @@ -504,7 +508,7 @@ struct DLL_LINKAGE SetCommanderProperty : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & heroid; h & which; @@ -523,7 +527,7 @@ struct DLL_LINKAGE AddQuest : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & quest; @@ -537,7 +541,7 @@ struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient void applyGs(CGameState * gs) const; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & allocatedArtifacts; } @@ -550,7 +554,7 @@ struct DLL_LINKAGE UpdateMapEvents : public CPackForClient void applyGs(CGameState * gs) const; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & events; } @@ -564,7 +568,7 @@ struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient void applyGs(CGameState * gs) const; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & town; h & events; @@ -574,12 +578,12 @@ struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient struct DLL_LINKAGE ChangeFormation : public CPackForClient { ObjectInstanceID hid; - ui8 formation = 0; + EArmyFormation formation{}; void applyGs(CGameState * gs) const; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & hid; h & formation; @@ -604,7 +608,7 @@ struct DLL_LINKAGE RemoveObject : public CPackForClient /// Player that initiated this action, if any PlayerColor initiator; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & objectID; h & initiator; @@ -628,7 +632,8 @@ struct DLL_LINKAGE TryMoveHero : public CPackForClient ObjectInstanceID id; ui32 movePoints = 0; EResult result = FAILED; //uses EResult - int3 start, end; //h3m format + int3 start; //h3m format + int3 end; std::unordered_set fowRevealed; //revealed tiles std::optional attackedFrom; // Set when stepping into endangered tile. @@ -639,7 +644,7 @@ struct DLL_LINKAGE TryMoveHero : public CPackForClient return result != SUCCESS && result != EMBARK && result != DISEMBARK && result != TELEPORTATION; } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; h & result; @@ -661,7 +666,7 @@ struct DLL_LINKAGE NewStructures : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & tid; h & bid; @@ -679,7 +684,7 @@ struct DLL_LINKAGE RazeStructures : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & tid; h & bid; @@ -696,7 +701,7 @@ struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & tid; h & creatures; @@ -707,11 +712,13 @@ struct DLL_LINKAGE SetHeroesInTown : public CPackForClient { void applyGs(CGameState * gs) const; - ObjectInstanceID tid, visiting, garrison; //id of town, visiting hero, hero in garrison + ObjectInstanceID tid; //id of town + ObjectInstanceID visiting; //id of visiting hero + ObjectInstanceID garrison; //id of hero in garrison void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & tid; h & visiting; @@ -731,7 +738,7 @@ struct DLL_LINKAGE HeroRecruited : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & hid; h & tid; @@ -751,7 +758,7 @@ struct DLL_LINKAGE GiveHero : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; h & boatId; @@ -767,7 +774,7 @@ struct DLL_LINKAGE OpenWindow : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & window; @@ -781,9 +788,9 @@ struct DLL_LINKAGE NewObject : public CPackForClient void applyGs(CGameState * gs); /// Object ID to create - Obj ID; + MapObjectID ID; /// Object secondary ID to create - ui32 subID = 0; + MapObjectSubID subID; /// Position of visitable tile of created object int3 targetPos; /// Which player initiated creation of this object @@ -793,10 +800,10 @@ struct DLL_LINKAGE NewObject : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & ID; - h & subID; + subID.serializeIdentifier(h, ID); h & targetPos; h & initiator; } @@ -806,12 +813,13 @@ struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient { void applyGs(CGameState * gs) const; - si32 id = 0; //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) + //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) + ObjectInstanceID id; std::vector arts; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; h & arts; @@ -833,7 +841,7 @@ struct DLL_LINKAGE ChangeStackCount : CGarrisonOperationPack void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & army; h & slot; @@ -852,7 +860,7 @@ struct DLL_LINKAGE SetStackType : CGarrisonOperationPack void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & army; h & slot; @@ -868,7 +876,7 @@ struct DLL_LINKAGE EraseStack : CGarrisonOperationPack void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & army; h & slot; @@ -885,7 +893,7 @@ struct DLL_LINKAGE SwapStacks : CGarrisonOperationPack void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & srcArmy; h & dstArmy; @@ -904,7 +912,7 @@ struct DLL_LINKAGE InsertNewStack : CGarrisonOperationPack void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & army; h & slot; @@ -926,7 +934,7 @@ struct DLL_LINKAGE RebalanceStacks : CGarrisonOperationPack void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & srcArmy; h & dstArmy; @@ -944,7 +952,7 @@ struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack void visitTyped(ICPackVisitor & visitor) override; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & moves; } @@ -959,7 +967,7 @@ struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack void visitTyped(ICPackVisitor & visitor) override; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & moves; h & changes; @@ -973,19 +981,19 @@ struct DLL_LINKAGE CArtifactOperationPack : CPackForClient struct DLL_LINKAGE PutArtifact : CArtifactOperationPack { PutArtifact() = default; - explicit PutArtifact(ArtifactLocation * dst, bool askAssemble = true) - : al(*dst), askAssemble(askAssemble) + explicit PutArtifact(ArtifactLocation & dst, bool askAssemble = true) + : al(dst), askAssemble(askAssemble) { } ArtifactLocation al; - bool askAssemble = false; + bool askAssemble; ConstTransitivePtr art; void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & al; h & askAssemble; @@ -1000,7 +1008,7 @@ struct DLL_LINKAGE NewArtifact : public CArtifactOperationPack void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & art; } @@ -1013,7 +1021,7 @@ struct DLL_LINKAGE EraseArtifact : CArtifactOperationPack void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & al; } @@ -1026,13 +1034,14 @@ struct DLL_LINKAGE MoveArtifact : CArtifactOperationPack : src(*src), dst(*dst), askAssemble(askAssemble) { } - ArtifactLocation src, dst; + ArtifactLocation src; + ArtifactLocation dst; bool askAssemble = true; void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & src; h & dst; @@ -1053,24 +1062,34 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack , dstPos(dstPos) { } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & srcPos; h & dstPos; } }; - TArtHolder srcArtHolder; - TArtHolder dstArtHolder; + ObjectInstanceID srcArtHolder; + ObjectInstanceID dstArtHolder; + std::optional srcCreature; + std::optional dstCreature; BulkMoveArtifacts() - : swap(false) + : srcArtHolder(ObjectInstanceID::NONE) + , dstArtHolder(ObjectInstanceID::NONE) + , swap(false) + , askAssemble(false) + , srcCreature(std::nullopt) + , dstCreature(std::nullopt) { } - BulkMoveArtifacts(TArtHolder srcArtHolder, TArtHolder dstArtHolder, bool swap) + BulkMoveArtifacts(const ObjectInstanceID srcArtHolder, const ObjectInstanceID dstArtHolder, bool swap) : srcArtHolder(std::move(srcArtHolder)) , dstArtHolder(std::move(dstArtHolder)) , swap(swap) + , askAssemble(false) + , srcCreature(std::nullopt) + , dstCreature(std::nullopt) { } @@ -1079,31 +1098,33 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack std::vector artsPack0; std::vector artsPack1; bool swap; - CArtifactSet * getSrcHolderArtSet(); - CArtifactSet * getDstHolderArtSet(); + bool askAssemble; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & artsPack0; h & artsPack1; h & srcArtHolder; h & dstArtHolder; + h & srcCreature; + h & dstCreature; h & swap; + h & askAssemble; } }; struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack { ArtifactLocation al; //where assembly will be put - CArtifact * builtArt; + const CArtifact * builtArt; void applyGs(CGameState * gs); void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & al; h & builtArt; @@ -1118,7 +1139,7 @@ struct DLL_LINKAGE DisassembledArtifact : CArtifactOperationPack void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & al; } @@ -1136,7 +1157,7 @@ struct DLL_LINKAGE HeroVisit : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & heroId; @@ -1155,9 +1176,10 @@ struct DLL_LINKAGE NewTurn : public CPackForClient struct Hero { - ObjectInstanceID id; - ui32 move, mana; //id is a general serial id - template void serialize(Handler & h, const int version) + ObjectInstanceID id; //id is a general serial id + ui32 move; + ui32 mana; + template void serialize(Handler & h) { h & id; h & move; @@ -1175,7 +1197,7 @@ struct DLL_LINKAGE NewTurn : public CPackForClient NewTurn() = default; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & heroes; h & cres; @@ -1196,7 +1218,7 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple i void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & type; h & text; @@ -1207,46 +1229,23 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple i InfoWindow() = default; }; -namespace ObjProperty -{ - enum - { - OWNER = 1, BLOCKVIS = 2, PRIMARY_STACK_COUNT = 3, VISITORS = 4, VISITED = 5, ID = 6, AVAILABLE_CREATURE = 7, SUBID = 8, - MONSTER_COUNT = 10, MONSTER_POWER = 11, MONSTER_EXP = 12, MONSTER_RESTORE_TYPE = 13, MONSTER_REFUSED_JOIN, - - //town-specific - STRUCTURE_ADD_VISITING_HERO, STRUCTURE_CLEAR_VISITORS, STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state - BONUS_VALUE_FIRST, BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) - - //creature-bank specific - BANK_DAYCOUNTER, BANK_RESET, BANK_CLEAR, - - //object with reward - REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED - }; -} - struct DLL_LINKAGE SetObjectProperty : public CPackForClient { void applyGs(CGameState * gs) const; ObjectInstanceID id; - ui8 what = 0; // see ObjProperty enum - ui32 val = 0; + ObjProperty what{}; + + ObjPropertyID identifier; + SetObjectProperty() = default; - SetObjectProperty(const ObjectInstanceID & ID, ui8 What, ui32 Val) - : id(ID) - , what(What) - , val(Val) - { - } void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & id; h & what; - h & val; + h & identifier; } }; @@ -1256,10 +1255,11 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient { VISITOR_ADD, // mark hero as one that have visited this object VISITOR_ADD_TEAM, // mark team as one that have visited this object + VISITOR_GLOBAL, // mark player as one that have visited object of this type VISITOR_REMOVE, // unmark visitor, reversed to ADD VISITOR_CLEAR // clear all visitors from this object (object reset) }; - ui32 mode = VISITOR_CLEAR; // uses VisitMode enum + VisitMode mode = VISITOR_CLEAR; // uses VisitMode enum ObjectInstanceID object; ObjectInstanceID hero; // note: hero owner will be also marked as "visited" this object @@ -1269,14 +1269,14 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient ChangeObjectVisitors() = default; - ChangeObjectVisitors(ui32 mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) + ChangeObjectVisitors(VisitMode mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) : mode(mode) , object(object) , hero(heroID) { } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & object; h & hero; @@ -1284,23 +1284,6 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient } }; -struct DLL_LINKAGE PrepareHeroLevelUp : public CPackForClient -{ - ObjectInstanceID heroId; - - /// Do not serialize, used by server only - std::vector skills; - - void applyGs(CGameState * gs); - - void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & heroId; - } -}; - struct DLL_LINKAGE HeroLevelUp : public Query { PlayerColor player; @@ -1313,7 +1296,7 @@ struct DLL_LINKAGE HeroLevelUp : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & player; @@ -1334,7 +1317,7 @@ struct DLL_LINKAGE CommanderLevelUp : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & player; @@ -1373,7 +1356,7 @@ struct DLL_LINKAGE BlockingDialog : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & text; @@ -1386,12 +1369,13 @@ struct DLL_LINKAGE BlockingDialog : public Query struct DLL_LINKAGE GarrisonDialog : public Query { - ObjectInstanceID objid, hid; + ObjectInstanceID objid; + ObjectInstanceID hid; bool removableUnits = false; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & objid; @@ -1409,7 +1393,7 @@ struct DLL_LINKAGE ExchangeDialog : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & player; @@ -1434,7 +1418,7 @@ struct DLL_LINKAGE TeleportDialog : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & hero; @@ -1454,7 +1438,7 @@ struct DLL_LINKAGE MapObjectSelectDialog : public Query void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & queryID; h & player; @@ -1469,7 +1453,7 @@ struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient { ObjectInstanceID casterID; SpellID spellID; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & casterID; h & spellID; @@ -1486,7 +1470,7 @@ struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient std::vector objectPositions; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & showTerrain; @@ -1510,7 +1494,7 @@ struct DLL_LINKAGE PlayerMessageClient : public CPackForClient PlayerColor player; std::string text; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & player; h & text; @@ -1525,7 +1509,7 @@ struct DLL_LINKAGE CenterView : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & pos; h & player; diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h index ad0878fe9..239a248da 100644 --- a/lib/networkPacks/PacksForClientBattle.h +++ b/lib/networkPacks/PacksForClientBattle.h @@ -32,7 +32,7 @@ struct DLL_LINKAGE BattleStart : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & info; @@ -48,7 +48,7 @@ struct DLL_LINKAGE BattleNextRound : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; assert(battleID != BattleID::NONE); @@ -65,7 +65,7 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & stack; @@ -80,7 +80,7 @@ struct DLL_LINKAGE BattleCancelled: public CPackForClient BattleID battleID = BattleID::NONE; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; assert(battleID != BattleID::NONE); @@ -100,7 +100,7 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient CArmedInstance * army; TExpType exp; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & hero; h & army; @@ -112,7 +112,7 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient std::array heroResult; ui8 winnerSide; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & heroResult; @@ -128,13 +128,13 @@ struct DLL_LINKAGE BattleResult : public Query BattleID battleID = BattleID::NONE; EBattleResult result = EBattleResult::NORMAL; ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] - std::map casualties[2]; //first => casualties of attackers - map crid => number + std::map casualties[2]; //first => casualties of attackers - map crid => number TExpType exp[2] = {0, 0}; //exp for attacker and defender std::set artifacts; //artifacts taken from loser to winner - currently unused void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & queryID; @@ -158,7 +158,7 @@ struct DLL_LINKAGE BattleLogMessage : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & lines; @@ -179,7 +179,7 @@ struct DLL_LINKAGE BattleStackMoved : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & stack; @@ -200,7 +200,7 @@ struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & changedStacks; @@ -248,7 +248,7 @@ struct BattleStackAttacked return flags & FIRE_SHIELD; } - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & stackAttacked; @@ -315,7 +315,7 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & bsa; @@ -343,7 +343,7 @@ struct DLL_LINKAGE StartAction : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & ba; @@ -357,7 +357,7 @@ struct DLL_LINKAGE EndAction : public CPackForClient BattleID battleID = BattleID::NONE; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; } @@ -381,7 +381,7 @@ struct DLL_LINKAGE BattleSpellCast : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & side; @@ -408,7 +408,7 @@ struct DLL_LINKAGE StacksInjured : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & stacks; @@ -422,7 +422,7 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient PlayerColor player1, player2; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & player1; @@ -441,7 +441,7 @@ struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & changes; @@ -457,7 +457,7 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient EWallPart attackedPart; ui8 damageDealt; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & destinationTile; h & attackedPart; @@ -477,7 +477,7 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & attackedParts; @@ -498,7 +498,7 @@ struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient int val = 0; int absolute = 0; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & stackID; @@ -523,7 +523,7 @@ struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient int val = 0; int additionalInfo = 0; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & stackID; @@ -543,7 +543,7 @@ struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient BattleID battleID = BattleID::NONE; EGateState state = EGateState::NONE; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & state; diff --git a/lib/networkPacks/PacksForLobby.h b/lib/networkPacks/PacksForLobby.h index ef55fe054..ae0cf6635 100644 --- a/lib/networkPacks/PacksForLobby.h +++ b/lib/networkPacks/PacksForLobby.h @@ -29,7 +29,7 @@ struct DLL_LINKAGE CLobbyPackToPropagate : public CPackForLobby struct DLL_LINKAGE CLobbyPackToServer : public CPackForLobby { - virtual bool isForServer() const override; + bool isForServer() const override; }; struct DLL_LINKAGE LobbyClientConnected : public CLobbyPackToPropagate @@ -37,14 +37,14 @@ struct DLL_LINKAGE LobbyClientConnected : public CLobbyPackToPropagate // Set by client before sending pack to server std::string uuid; std::vector names; - StartInfo::EMode mode = StartInfo::INVALID; + EStartMode mode = EStartMode::INVALID; // Changed by server before announcing pack int clientId = -1; int hostClientId = -1; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & uuid; h & names; @@ -63,7 +63,7 @@ struct DLL_LINKAGE LobbyClientDisconnected : public CLobbyPackToPropagate void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & clientId; h & shutdownServer; @@ -72,11 +72,12 @@ struct DLL_LINKAGE LobbyClientDisconnected : public CLobbyPackToPropagate struct DLL_LINKAGE LobbyChatMessage : public CLobbyPackToPropagate { - std::string playerName, message; + std::string playerName; + std::string message; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & playerName; h & message; @@ -86,13 +87,13 @@ struct DLL_LINKAGE LobbyChatMessage : public CLobbyPackToPropagate struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate { enum EAction : ui8 { - NONE, NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS + NONE, NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS, OPEN_TURN_OPTIONS, OPEN_EXTRA_OPTIONS } action = NONE; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & action; } @@ -104,22 +105,27 @@ struct DLL_LINKAGE LobbyLoadProgress : public CLobbyPackToPropagate void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & progress; } }; -struct DLL_LINKAGE LobbyEndGame : public CLobbyPackToPropagate +struct DLL_LINKAGE LobbyRestartGame : public CLobbyPackToPropagate { - bool closeConnection = false, restart = false; - void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) + { + } +}; + +struct DLL_LINKAGE LobbyPrepareStartGame : public CLobbyPackToPropagate +{ + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h) { - h & closeConnection; - h & restart; } }; @@ -132,7 +138,7 @@ struct DLL_LINKAGE LobbyStartGame : public CLobbyPackToPropagate void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & clientId; h & initializedStartInfo; @@ -149,7 +155,7 @@ struct DLL_LINKAGE LobbyChangeHost : public CLobbyPackToPropagate void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & newHostConnectionId; } @@ -162,7 +168,7 @@ struct DLL_LINKAGE LobbyUpdateState : public CLobbyPackToPropagate void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & state; } @@ -177,7 +183,7 @@ struct DLL_LINKAGE LobbySetMap : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & mapInfo; h & mapGenOpts; @@ -190,7 +196,7 @@ struct DLL_LINKAGE LobbySetCampaign : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & ourCampaign; } @@ -202,7 +208,7 @@ struct DLL_LINKAGE LobbySetCampaignMap : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & mapId; } @@ -214,7 +220,7 @@ struct DLL_LINKAGE LobbySetCampaignBonus : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & bonusId; } @@ -229,7 +235,7 @@ struct DLL_LINKAGE LobbyChangePlayerOption : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & what; h & value; @@ -243,7 +249,7 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & clickedColor; } @@ -256,7 +262,7 @@ struct DLL_LINKAGE LobbySetPlayerName : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & color; h & name; @@ -269,7 +275,7 @@ struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & simturnsInfo; } @@ -281,19 +287,31 @@ struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & turnTimerInfo; } }; +struct DLL_LINKAGE LobbySetExtraOptions : public CLobbyPackToServer +{ + ExtraOptionsInfo extraOptionsInfo; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h) + { + h & extraOptionsInfo; + } +}; + struct DLL_LINKAGE LobbySetDifficulty : public CLobbyPackToServer { ui8 difficulty = 0; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & difficulty; } @@ -306,7 +324,7 @@ struct DLL_LINKAGE LobbyForceSetPlayer : public CLobbyPackToServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & targetConnectedPlayer; h & targetPlayerColor; @@ -319,7 +337,7 @@ struct DLL_LINKAGE LobbyShowMessage : public CLobbyPackToPropagate void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & message; } diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h index 9911d1ba8..14a60cce3 100644 --- a/lib/networkPacks/PacksForServer.h +++ b/lib/networkPacks/PacksForServer.h @@ -11,6 +11,7 @@ #include "ArtifactLocation.h" #include "NetPacksBase.h" +#include "TradeItem.h" #include "../int3.h" #include "../battle/BattleAction.h" @@ -21,7 +22,7 @@ struct DLL_LINKAGE GamePause : public CPackForServer { void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); } @@ -31,7 +32,7 @@ struct DLL_LINKAGE EndTurn : public CPackForServer { void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); } @@ -48,7 +49,7 @@ struct DLL_LINKAGE DismissHero : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & hid; @@ -70,7 +71,7 @@ struct DLL_LINKAGE MoveHero : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & dest; @@ -94,7 +95,7 @@ struct DLL_LINKAGE CastleTeleportHero : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & dest; @@ -122,7 +123,7 @@ struct DLL_LINKAGE ArrangeStacks : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & what; @@ -152,7 +153,7 @@ struct DLL_LINKAGE BulkMoveArmy : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & static_cast(*this); h & srcSlot; @@ -179,7 +180,7 @@ struct DLL_LINKAGE BulkSplitStack : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & static_cast(*this); h & src; @@ -204,7 +205,7 @@ struct DLL_LINKAGE BulkMergeStacks : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & static_cast(*this); h & src; @@ -228,7 +229,7 @@ struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & static_cast(*this); h & src; @@ -249,7 +250,7 @@ struct DLL_LINKAGE DisbandCreature : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & pos; @@ -270,7 +271,7 @@ struct DLL_LINKAGE BuildStructure : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & tid; @@ -302,7 +303,7 @@ struct DLL_LINKAGE RecruitCreatures : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & tid; @@ -328,7 +329,7 @@ struct DLL_LINKAGE UpgradeCreature : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & pos; @@ -348,7 +349,7 @@ struct DLL_LINKAGE GarrisonHeroSwap : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & tid; @@ -361,7 +362,7 @@ struct DLL_LINKAGE ExchangeArtifacts : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & src; @@ -389,7 +390,7 @@ struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & srcHero; @@ -417,7 +418,7 @@ struct DLL_LINKAGE AssembleArtifacts : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & heroID; @@ -438,7 +439,7 @@ struct DLL_LINKAGE EraseArtifactByClient : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & al; @@ -458,7 +459,7 @@ struct DLL_LINKAGE BuyArtifact : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & hid; @@ -472,12 +473,13 @@ struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer ObjectInstanceID heroId; EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; - std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] + std::vector r1; + std::vector r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] std::vector val; //units of sold resource void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & marketId; @@ -493,17 +495,17 @@ struct DLL_LINKAGE SetFormation : public CPackForServer { SetFormation() = default; ; - SetFormation(const ObjectInstanceID & HID, ui8 Formation) + SetFormation(const ObjectInstanceID & HID, EArmyFormation Formation) : hid(HID) , formation(Formation) { } ObjectInstanceID hid; - ui8 formation = 0; + EArmyFormation formation{}; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & hid; @@ -514,21 +516,24 @@ struct DLL_LINKAGE SetFormation : public CPackForServer struct DLL_LINKAGE HireHero : public CPackForServer { HireHero() = default; - HireHero(HeroTypeID HID, const ObjectInstanceID & TID) + HireHero(HeroTypeID HID, const ObjectInstanceID & TID, const HeroTypeID & NHID) : hid(HID) , tid(TID) + , nhid(NHID) { } HeroTypeID hid; //available hero serial + HeroTypeID nhid; //next hero ObjectInstanceID tid; //town (tavern) id PlayerColor player; void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & hid; + h & nhid; h & tid; h & player; } @@ -540,7 +545,7 @@ struct DLL_LINKAGE BuildBoat : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & objid; @@ -561,7 +566,7 @@ struct DLL_LINKAGE QueryReply : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & qid; @@ -582,7 +587,7 @@ struct DLL_LINKAGE MakeAction : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & ba; @@ -596,7 +601,7 @@ struct DLL_LINKAGE DigWithHero : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & id; @@ -611,7 +616,7 @@ struct DLL_LINKAGE CastAdvSpell : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & hid; @@ -635,7 +640,7 @@ struct DLL_LINKAGE SaveGame : public CPackForServer void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & fname; @@ -658,7 +663,7 @@ struct DLL_LINKAGE PlayerMessage : public CPackForServer std::string text; ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & static_cast(*this); h & text; diff --git a/lib/networkPacks/SetStackEffect.h b/lib/networkPacks/SetStackEffect.h index f53b4cfe2..af6196353 100644 --- a/lib/networkPacks/SetStackEffect.h +++ b/lib/networkPacks/SetStackEffect.h @@ -29,7 +29,7 @@ struct DLL_LINKAGE SetStackEffect : public CPackForClient void visitTyped(ICPackVisitor & visitor) override; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & battleID; h & toAdd; diff --git a/lib/networkPacks/StackLocation.h b/lib/networkPacks/StackLocation.h index abb922d0f..a53255b8b 100644 --- a/lib/networkPacks/StackLocation.h +++ b/lib/networkPacks/StackLocation.h @@ -30,7 +30,7 @@ struct StackLocation } DLL_LINKAGE const CStackInstance * getStack(); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & army; h & slot; diff --git a/lib/networkPacks/TradeItem.h b/lib/networkPacks/TradeItem.h new file mode 100644 index 000000000..fba54e63e --- /dev/null +++ b/lib/networkPacks/TradeItem.h @@ -0,0 +1,20 @@ +/* + * TradeItem.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../constants/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TradeItemSell = VariantIdentifier; +using TradeItemBuy = VariantIdentifier; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h index ad6e90383..52e8e7d82 100644 --- a/lib/pathfinder/CGPathNode.h +++ b/lib/pathfinder/CGPathNode.h @@ -102,7 +102,7 @@ struct DLL_LINKAGE CGPathNode STRONG_INLINE void setCost(float value) { - if(value == cost) + if(vstd::isAlmostEqual(value, cost)) return; bool getUpNode = value < cost; @@ -197,7 +197,7 @@ struct DLL_LINKAGE CPathsInfo STRONG_INLINE CGPathNode * getNode(const int3 & coord, const ELayer layer) { - return &nodes[layer][coord.z][coord.x][coord.y]; + return &nodes[layer.getNum()][coord.z][coord.x][coord.y]; } }; @@ -233,7 +233,7 @@ struct DLL_LINKAGE CDestinationNodeInfo : public PathNodeInfo CDestinationNodeInfo(); - virtual void setNode(CGameState * gs, CGPathNode * n) override; + void setNode(CGameState * gs, CGPathNode * n) override; virtual bool isBetterWay() const; }; diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index ee237bfe5..4dcf8c5ed 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -19,16 +19,44 @@ #include "../CPlayerState.h" #include "../TerrainHandler.h" #include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/MiscObjects.h" #include "../mapping/CMap.h" #include "spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN +bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const +{ + // we can always make the first step, even when standing on object + if(source.node->theNodeBefore == nullptr) + return true; + + if (!source.nodeObject) + return true; + + if (!source.isNodeObjectVisitable()) + return true; + + // we can always move from visitable object if hero has teleported here (e.g. went through monolith) + if (source.node->isTeleportAction()) + return true; + + // we can not go through teleporters since moving onto a teleport will teleport hero and may invalidate path (e.g. one-way teleport or enemy hero on other side) + if (dynamic_cast(source.nodeObject) != nullptr) + return false; + + return true; +} + std::vector CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const { std::vector neighbourTiles; - neighbourTiles.reserve(8); + if (!canMoveFromNode(source)) + return neighbourTiles; + + neighbourTiles.reserve(8); getNeighbours( *source.tile, source.node->coord, @@ -38,7 +66,7 @@ std::vector CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & sour if(source.isNodeObjectVisitable()) { - vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool + vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool { return !canMoveBetween(tile, source.nodeObject->visitablePos()); }); @@ -260,15 +288,18 @@ std::vector CPathfinderHelper::getTeleportExits(const PathNodeInfo & sourc teleportationExits.push_back(exit); } } - else if(options.useCastleGate - && (source.nodeObject->ID == Obj::TOWN && source.nodeObject->subID == ETownType::INFERNO - && source.objectRelations != PlayerRelations::ENEMIES)) + else if(options.useCastleGate && source.nodeObject->ID == Obj::TOWN && source.objectRelations != PlayerRelations::ENEMIES) { - /// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo - /// This may be handy if we allow to use teleportation to friendly towns - for(const auto & exit : getCastleGates(source)) + auto * town = dynamic_cast(source.nodeObject); + assert(town); + if (town && town->getFaction() == FactionID::INFERNO) { - teleportationExits.push_back(exit); + /// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo + /// This may be handy if we allow to use teleportation to friendly towns + for(const auto & exit : getCastleGates(source)) + { + teleportationExits.push_back(exit); + } } } @@ -299,7 +330,7 @@ bool CPathfinder::isLayerTransitionPossible() const if(source.node->action == EPathNodeAction::BATTLE) return false; - switch(source.node->layer) + switch(source.node->layer.toEnum()) { case ELayer::LAND: if(destLayer == ELayer::AIR) @@ -452,7 +483,7 @@ bool CPathfinderHelper::passOneTurnLimitCheck(const PathNodeInfo & source) const return false; if(source.node->layer == EPathfindingLayer::AIR) { - return options.originalMovementRules && source.node->accessible == EPathAccessibility::ACCESSIBLE; + return options.originalFlyRules && source.node->accessible == EPathAccessibility::ACCESSIBLE; } return true; @@ -502,7 +533,7 @@ void CPathfinderHelper::updateTurnInfo(const int Turn) bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const { - switch(layer) + switch(layer.toEnum()) { case EPathfindingLayer::AIR: if(!options.useFlying) diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index af12ca56a..4203c8ba1 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -79,6 +79,7 @@ public: virtual ~CPathfinderHelper(); void initializePatrol(); bool isHeroPatrolLocked() const; + bool canMoveFromNode(const PathNodeInfo & source) const; bool isPatrolMovementAllowed(const int3 & dst) const; void updateTurnInfo(const int turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; diff --git a/lib/pathfinder/NodeStorage.cpp b/lib/pathfinder/NodeStorage.cpp index 38936dcf5..c24c10f1a 100644 --- a/lib/pathfinder/NodeStorage.cpp +++ b/lib/pathfinder/NodeStorage.cpp @@ -16,6 +16,7 @@ #include "../CPlayerState.h" #include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/MiscObjects.h" #include "../mapping/CMap.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,7 +28,7 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState int3 pos; const PlayerColor player = out.hero->tempOwner; const int3 sizes = gs->getMapSize(); - const auto fow = static_cast(gs)->getPlayerTeam(player)->fogOfWarMap; + const auto & fow = static_cast(gs)->getPlayerTeam(player)->fogOfWarMap; //make 200% sure that these are loop invariants (also a bit shorter code), let compiler do the rest(loop unswitching) const bool useFlying = options.useFlying; @@ -100,6 +101,12 @@ std::vector NodeStorage::calculateTeleportations( { auto * node = getNode(neighbour, source.node->layer); + if(!node->coord.valid()) + { + logAi->debug("Teleportation exit is blocked " + neighbour.toString()); + continue; + } + neighbours.push_back(node); } diff --git a/lib/pathfinder/NodeStorage.h b/lib/pathfinder/NodeStorage.h index eddb23201..e8a70a63b 100644 --- a/lib/pathfinder/NodeStorage.h +++ b/lib/pathfinder/NodeStorage.h @@ -34,7 +34,7 @@ public: void initialize(const PathfinderOptions & options, const CGameState * gs) override; virtual ~NodeStorage() = default; - virtual std::vector getInitialNodes() override; + std::vector getInitialNodes() override; virtual std::vector calculateNeighbours( const PathNodeInfo & source, @@ -46,7 +46,7 @@ public: const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) override; - virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; + void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 4c83acafc..4b16b63f3 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -21,15 +21,16 @@ VCMI_LIB_NAMESPACE_BEGIN PathfinderOptions::PathfinderOptions() : useFlying(true) , useWaterWalking(true) + , ignoreGuards(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_IGNORE_GUARDS)) , useEmbarkAndDisembark(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_BOAT)) , useTeleportTwoWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY)) , useTeleportOneWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE)) , useTeleportOneWayRandom(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM)) , useTeleportWhirlpool(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_WHIRLPOOL)) + , originalFlyRules(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_ORIGINAL_FLY_RULES)) , useCastleGate(false) , lightweightFlyingMode(false) , oneTurnSpecialLayersLimit(true) - , originalMovementRules(false) , turnLimit(std::numeric_limits::max()) , canUseCast(false) { diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index 96d75cb2a..c4cb6ef44 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -24,6 +24,7 @@ struct DLL_LINKAGE PathfinderOptions { bool useFlying; bool useWaterWalking; + bool ignoreGuards; bool useEmbarkAndDisembark; bool useTeleportTwoWay; // Two-way monoliths and Subterranean Gate bool useTeleportOneWay; // One-way monoliths with one known exit only @@ -66,7 +67,9 @@ struct DLL_LINKAGE PathfinderOptions /// - Option should also allow same tile land <-> air layer transitions. /// Current implementation only allow go into (from) air layer only to neighbour tiles. /// I find it's reasonable limitation, but it's will make some movements more expensive than in H3. - bool originalMovementRules; + /// Further work can also be done to mimic SoD quirks if needed + /// (such as picking unoptimal paths on purpose when targeting guards or being interrupted on guarded resource tile when picking it during diagonal u-turn) + bool originalFlyRules; /// Max number of turns to compute. Default = infinite uint8_t turnLimit; @@ -103,7 +106,7 @@ public: SingleHeroPathfinderConfig(CPathsInfo & out, CGameState * gs, const CGHeroInstance * hero); virtual ~SingleHeroPathfinderConfig(); - virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; + CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override; static std::vector> buildRuleSet(); }; diff --git a/lib/pathfinder/PathfinderUtil.h b/lib/pathfinder/PathfinderUtil.h index 49efd7833..749cc513e 100644 --- a/lib/pathfinder/PathfinderUtil.h +++ b/lib/pathfinder/PathfinderUtil.h @@ -19,11 +19,11 @@ VCMI_LIB_NAMESPACE_BEGIN namespace PathfinderUtil { - using FoW = std::shared_ptr>; + using FoW = std::unique_ptr>; using ELayer = EPathfindingLayer; template - EPathAccessibility evaluateAccessibility(const int3 & pos, const TerrainTile & tinfo, FoW fow, const PlayerColor player, const CGameState * gs) + EPathAccessibility evaluateAccessibility(const int3 & pos, const TerrainTile & tinfo, const FoW & fow, const PlayerColor player, const CGameState * gs) { if(!(*fow)[pos.z][pos.x][pos.y]) return EPathAccessibility::BLOCKED; diff --git a/lib/pathfinder/PathfindingRules.cpp b/lib/pathfinder/PathfindingRules.cpp index 48aaf7814..ca8e3df46 100644 --- a/lib/pathfinder/PathfindingRules.cpp +++ b/lib/pathfinder/PathfindingRules.cpp @@ -123,7 +123,7 @@ void DestinationActionRule::process( EPathNodeAction action = EPathNodeAction::NORMAL; const auto * hero = pathfinderHelper->hero; - switch(destination.node->layer) + switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(source.node->layer == EPathfindingLayer::SAIL) @@ -249,9 +249,7 @@ PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlocking } case EPathNodeAction::BLOCKING_VISIT: - return destination.guarded - ? BlockingReason::DESTINATION_GUARDED - : BlockingReason::DESTINATION_BLOCKVIS; + return BlockingReason::DESTINATION_BLOCKVIS; case EPathNodeAction::NORMAL: return BlockingReason::NONE; @@ -271,7 +269,12 @@ PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlocking case EPathNodeAction::BATTLE: /// Movement after BATTLE action only possible from guarded tile to guardian tile if(destination.guarded) - return BlockingReason::DESTINATION_GUARDED; + { + if (pathfinderHelper->options.ignoreGuards) + return BlockingReason::NONE; + else + return BlockingReason::DESTINATION_GUARDED; + } break; } @@ -290,7 +293,7 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(destination.node->accessible == EPathAccessibility::BLOCKED) return BlockingReason::DESTINATION_BLOCKED; - switch(destination.node->layer) + switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord)) @@ -298,7 +301,8 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(source.guarded) { - if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) + if(!(pathfinderConfig->options.originalFlyRules && source.node->layer == EPathfindingLayer::AIR) + && !pathfinderConfig->options.ignoreGuards && (!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard { return BlockingReason::SOURCE_GUARDED; @@ -359,7 +363,7 @@ void LayerTransitionRule::process( if(source.node->layer == destination.node->layer) return; - switch(source.node->layer) + switch(source.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(destination.node->layer == EPathfindingLayer::SAIL) @@ -382,14 +386,22 @@ void LayerTransitionRule::process( break; case EPathfindingLayer::AIR: - if(pathfinderConfig->options.originalMovementRules) + if(pathfinderConfig->options.originalFlyRules) { - if((source.node->accessible != EPathAccessibility::ACCESSIBLE && - source.node->accessible != EPathAccessibility::VISITABLE) && - (destination.node->accessible != EPathAccessibility::VISITABLE && - destination.node->accessible != EPathAccessibility::ACCESSIBLE)) + if(source.node->accessible != EPathAccessibility::ACCESSIBLE && + source.node->accessible != EPathAccessibility::VISITABLE && + destination.node->accessible != EPathAccessibility::VISITABLE && + destination.node->accessible != EPathAccessibility::ACCESSIBLE) { - destination.blocked = true; + if(destination.node->accessible == EPathAccessibility::BLOCKVIS) + { + if(source.nodeObject || (source.tile->blocked && destination.tile->blocked)) + { + destination.blocked = true; + } + } + else + destination.blocked = true; } } else if(destination.node->accessible != EPathAccessibility::ACCESSIBLE) diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 01ba846ed..1f32139a6 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -23,7 +23,8 @@ TurnInfo::BonusCache::BonusCache(const TConstBonusListPtr & bl) for(const auto & terrain : VLC->terrainTypeHandler->objects) { auto selector = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(terrain->getId())); - noTerrainPenalty.push_back(static_cast(bl->getFirst(selector))); + if (bl->getFirst(selector)) + noTerrainPenalty.insert(terrain->getId()); } freeShipBoarding = static_cast(bl->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); @@ -47,7 +48,7 @@ TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn): bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const { - switch(layer) + switch(layer.toEnum()) { case EPathfindingLayer::AIR: if(hero && hero->boat && hero->boat->layer == EPathfindingLayer::AIR) @@ -87,7 +88,7 @@ bool TurnInfo::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const case BonusType::WATER_WALKING: return bonusCache->waterWalking; case BonusType::NO_TERRAIN_PENALTY: - return bonusCache->noTerrainPenalty[subtype.getNum()]; + return bonusCache->noTerrainPenalty.count(subtype.as()); } return static_cast( diff --git a/lib/pathfinder/TurnInfo.h b/lib/pathfinder/TurnInfo.h index 4390e13c6..6fff27e6f 100644 --- a/lib/pathfinder/TurnInfo.h +++ b/lib/pathfinder/TurnInfo.h @@ -21,7 +21,7 @@ struct DLL_LINKAGE TurnInfo /// This is certainly not the best design ever and certainly can be improved /// Unfortunately for pathfinder that do hundreds of thousands calls onus system add too big overhead struct BonusCache { - std::vector noTerrainPenalty; + std::set noTerrainPenalty; bool freeShipBoarding; bool flyingMovement; int flyingMovementVal; diff --git a/lib/registerTypes/RegisterTypes.cpp b/lib/registerTypes/RegisterTypes.cpp deleted file mode 100644 index 9408fa7ff..000000000 --- a/lib/registerTypes/RegisterTypes.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * RegisterTypes.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#define INSTANTIATE_REGISTER_TYPES_HERE -#include "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../spells/CSpellHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -// For reference: peak memory usage by gcc during compilation of register type templates -// registerTypesMapObjects: 1.9 Gb -// registerTypes2: 2.2 Gb -// registerTypesClientPacks1 1.6 Gb -// registerTypesClientPacks2 1.6 Gb -// registerTypesServerPacks: 1.3 Gb -// registerTypes4: 1.3 Gb - - -#define DEFINE_EXTERNAL_METHOD(METHODNAME) \ -extern template DLL_LINKAGE void METHODNAME(BinaryDeserializer & s); \ -extern template DLL_LINKAGE void METHODNAME(BinarySerializer & s); \ -extern template DLL_LINKAGE void METHODNAME(CTypeList & s); \ - -//DEFINE_EXTERNAL_METHOD(registerTypesMapObjects) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects1) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects2) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks1) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks2) -DEFINE_EXTERNAL_METHOD(registerTypesServerPacks) -DEFINE_EXTERNAL_METHOD(registerTypesLobbyPacks) - -template void registerTypes(BinaryDeserializer & s); -template void registerTypes(BinarySerializer & s); -template void registerTypes(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 907361a40..2c2284452 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -9,413 +9,20 @@ */ #pragma once -#include "../networkPacks/PacksForClient.h" -#include "../networkPacks/PacksForClientBattle.h" -#include "../networkPacks/PacksForServer.h" -#include "../networkPacks/PacksForLobby.h" -#include "../networkPacks/SetStackEffect.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CCreatureSet.h" -#include "../CPlayerState.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../mapObjectConstructors/CRewardableConstructor.h" -#include "../mapObjectConstructors/CommonConstructors.h" -#include "../mapObjectConstructors/CBankInstanceConstructor.h" -#include "../mapObjectConstructors/DwellingInstanceConstructor.h" -#include "../mapObjectConstructors/HillFortInstanceConstructor.h" -#include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjects/MapObjects.h" -#include "../mapObjects/CGCreature.h" -#include "../mapObjects/CGTownBuilding.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../battle/CObstacleInstance.h" -#include "../bonuses/CBonusSystemNode.h" -#include "../bonuses/Limiters.h" -#include "../bonuses/Updaters.h" -#include "../bonuses/Propagators.h" -#include "../CStack.h" +#include "RegisterTypesClientPacks.h" +#include "RegisterTypesLobbyPacks.h" +#include "RegisterTypesMapObjects.h" +#include "RegisterTypesServerPacks.h" VCMI_LIB_NAMESPACE_BEGIN -class BinarySerializer; -class BinaryDeserializer; -class CTypeList; - -template -void registerTypesMapObjects1(Serializer &s) -{ - ////////////////////////////////////////////////////////////////////////// - // Adventure map objects - ////////////////////////////////////////////////////////////////////////// - s.template registerType(); - - // Non-armed objects - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); s.template registerType(); s.template registerType(); - - // Armed objects - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); -} - -template -void registerTypesMapObjectTypes(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -#define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() - - REGISTER_GENERIC_HANDLER(CGObjectInstance); - REGISTER_GENERIC_HANDLER(CGArtifact); - REGISTER_GENERIC_HANDLER(CGBlackMarket); - REGISTER_GENERIC_HANDLER(CGBoat); - REGISTER_GENERIC_HANDLER(CGBorderGate); - REGISTER_GENERIC_HANDLER(CGBorderGuard); - REGISTER_GENERIC_HANDLER(CGCreature); - REGISTER_GENERIC_HANDLER(CGDenOfthieves); - REGISTER_GENERIC_HANDLER(CGDwelling); - REGISTER_GENERIC_HANDLER(CGEvent); - REGISTER_GENERIC_HANDLER(CGGarrison); - REGISTER_GENERIC_HANDLER(CGHeroPlaceholder); - REGISTER_GENERIC_HANDLER(CGHeroInstance); - REGISTER_GENERIC_HANDLER(CGKeymasterTent); - REGISTER_GENERIC_HANDLER(CGLighthouse); - REGISTER_GENERIC_HANDLER(CGTerrainPatch); - REGISTER_GENERIC_HANDLER(CGMagi); - REGISTER_GENERIC_HANDLER(CGMarket); - REGISTER_GENERIC_HANDLER(CGMine); - REGISTER_GENERIC_HANDLER(CGObelisk); - REGISTER_GENERIC_HANDLER(CGPandoraBox); - REGISTER_GENERIC_HANDLER(CGQuestGuard); - REGISTER_GENERIC_HANDLER(CGResource); - REGISTER_GENERIC_HANDLER(CGSeerHut); - REGISTER_GENERIC_HANDLER(CGShipyard); - REGISTER_GENERIC_HANDLER(CGSignBottle); - REGISTER_GENERIC_HANDLER(CGSirens); - REGISTER_GENERIC_HANDLER(CGMonolith); - REGISTER_GENERIC_HANDLER(CGSubterraneanGate); - REGISTER_GENERIC_HANDLER(CGWhirlpool); - REGISTER_GENERIC_HANDLER(CGTownInstance); - REGISTER_GENERIC_HANDLER(CGUniversity); - REGISTER_GENERIC_HANDLER(HillFort); - -#undef REGISTER_GENERIC_HANDLER - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - //new types (other than netpacks) must register here - //order of type registration is critical for loading old savegames -} - -template -void registerTypesMapObjects2(Serializer &s) -{ - //Other object-related - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - - s.template registerType(); - s.template registerType(); - - //s.template registerType(); - //s.template registerType(); - - //end of objects - - ////////////////////////////////////////////////////////////////////////// - // Bonus system - ////////////////////////////////////////////////////////////////////////// - //s.template registerType(); - s.template registerType(); - - // Limiters - //s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -// s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); //TODO - //s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); - s.template registerType(); - - //s.template registerType(); - s.template registerType(); -} -template -void registerTypesClientPacks1(Serializer &s) -{ - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesClientPacks2(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesServerPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesLobbyPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - - // Any client can sent - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only host client send - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only server send - s.template registerType(); - s.template registerType(); - - // For client with permissions - s.template registerType(); - // Only for host client - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - template void registerTypes(Serializer &s) { - registerTypesMapObjects1(s); - registerTypesMapObjects2(s); - registerTypesMapObjectTypes(s); - registerTypesClientPacks1(s); - registerTypesClientPacks2(s); + registerTypesMapObjects(s); + registerTypesClientPacks(s); registerTypesServerPacks(s); registerTypesLobbyPacks(s); } -#ifndef INSTANTIATE_REGISTER_TYPES_HERE - -extern template DLL_LINKAGE void registerTypes(BinaryDeserializer & s); -extern template DLL_LINKAGE void registerTypes(BinarySerializer & s); -extern template DLL_LINKAGE void registerTypes(CTypeList & s); - -#endif - - VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesClientPacks.h b/lib/registerTypes/RegisterTypesClientPacks.h new file mode 100644 index 000000000..5ba7c3165 --- /dev/null +++ b/lib/registerTypes/RegisterTypesClientPacks.h @@ -0,0 +1,124 @@ +/* + * RegisterTypesClientPacks.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/SetStackEffect.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template +void registerTypesClientPacks(Serializer &s) +{ + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesLobbyPacks.h b/lib/registerTypes/RegisterTypesLobbyPacks.h new file mode 100644 index 000000000..3eade0382 --- /dev/null +++ b/lib/registerTypes/RegisterTypesLobbyPacks.h @@ -0,0 +1,64 @@ +/* + * RegisterTypesLobbyPacks.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../networkPacks/PacksForLobby.h" +#include "../gameState/CGameState.h" +#include "../campaign/CampaignState.h" +#include "../mapping/CMapInfo.h" +#include "../rmg/CMapGenOptions.h" +#include "../gameState/TavernHeroesPool.h" +#include "../gameState/CGameStateCampaign.h" +#include "../mapping/CMap.h" +#include "../TerrainHandler.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template +void registerTypesLobbyPacks(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + + // Any client can sent + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only host client send + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only server send + s.template registerType(); + s.template registerType(); + + // For client with permissions + s.template registerType(); + // Only for host client + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesMapObjects.h b/lib/registerTypes/RegisterTypesMapObjects.h new file mode 100644 index 000000000..5e8d96f32 --- /dev/null +++ b/lib/registerTypes/RegisterTypesMapObjects.h @@ -0,0 +1,139 @@ +/* + * RegisterTypesMapObjects.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../mapObjectConstructors/CBankInstanceConstructor.h" +#include "../mapObjects/MapObjects.h" +#include "../mapObjects/CGCreature.h" +#include "../mapObjects/CGTownBuilding.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../battle/BattleInfo.h" +#include "../battle/CObstacleInstance.h" +#include "../bonuses/Limiters.h" +#include "../bonuses/Updaters.h" +#include "../bonuses/Propagators.h" +#include "../CPlayerState.h" +#include "../CStack.h" +#include "../CHeroHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template +void registerTypesMapObjects(Serializer &s) +{ + ////////////////////////////////////////////////////////////////////////// + // Adventure map objects + ////////////////////////////////////////////////////////////////////////// + s.template registerType(); + + // Non-armed objects + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); s.template registerType(); s.template registerType(); + + // Armed objects + s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + //new types (other than netpacks) must register here + //order of type registration is critical for loading old savegames + + //Other object-related + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + + s.template registerType(); + s.template registerType(); + + //end of objects + + ////////////////////////////////////////////////////////////////////////// + // Bonus system + ////////////////////////////////////////////////////////////////////////// + //s.template registerType(); + s.template registerType(); + + // Limiters + //s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + +// s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + //s.template registerType(); //TODO + //s.template registerType(); + s.template registerType(); + s.template registerType(); + //s.template registerType(); + s.template registerType(); + + //s.template registerType(); + s.template registerType(); + + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesServerPacks.h b/lib/registerTypes/RegisterTypesServerPacks.h new file mode 100644 index 000000000..f5eed0156 --- /dev/null +++ b/lib/registerTypes/RegisterTypesServerPacks.h @@ -0,0 +1,56 @@ +/* + * RegisterTypesServerPacks.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../networkPacks/PacksForServer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BinarySerializer; +class BinaryDeserializer; +class CTypeList; + +template +void registerTypesServerPacks(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks1.cpp b/lib/registerTypes/TypesClientPacks1.cpp deleted file mode 100644 index c13b78c83..000000000 --- a/lib/registerTypes/TypesClientPacks1.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * TypesClientPacks1.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks1(BinaryDeserializer & s); -template void registerTypesClientPacks1(BinarySerializer & s); -template void registerTypesClientPacks1(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks2.cpp b/lib/registerTypes/TypesClientPacks2.cpp deleted file mode 100644 index bf68ca257..000000000 --- a/lib/registerTypes/TypesClientPacks2.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * TypesClientPacks2.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks2(BinaryDeserializer & s); -template void registerTypesClientPacks2(BinarySerializer & s); -template void registerTypesClientPacks2(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesLobbyPacks.cpp b/lib/registerTypes/TypesLobbyPacks.cpp deleted file mode 100644 index fe3838d05..000000000 --- a/lib/registerTypes/TypesLobbyPacks.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * TypesLobbyPacks.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../gameState/CGameState.h" -#include "../gameState/CGameStateCampaign.h" -#include "../gameState/TavernHeroesPool.h" -#include "../mapping/CMap.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../RoadHandler.h" -#include "../RiverHandler.h" -#include "../TerrainHandler.h" -#include "../campaign/CampaignState.h" -#include "../rmg/CMapGenOptions.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesLobbyPacks(BinaryDeserializer & s); -template void registerTypesLobbyPacks(BinarySerializer & s); -template void registerTypesLobbyPacks(CTypeList & s); - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects1.cpp b/lib/registerTypes/TypesMapObjects1.cpp deleted file mode 100644 index 1e383a581..000000000 --- a/lib/registerTypes/TypesMapObjects1.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* - * TypesMapObjects1.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesMapObjects1(BinaryDeserializer & s); -template void registerTypesMapObjects1(BinarySerializer & s); -template void registerTypesMapObjects1(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects2.cpp b/lib/registerTypes/TypesMapObjects2.cpp deleted file mode 100644 index ab7d4f1a1..000000000 --- a/lib/registerTypes/TypesMapObjects2.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * TypesMapObjects2.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesMapObjects2(BinaryDeserializer & s); -template void registerTypesMapObjects2(BinarySerializer & s); -template void registerTypesMapObjects2(CTypeList & s); - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects3.cpp b/lib/registerTypes/TypesMapObjects3.cpp deleted file mode 100644 index 3833d0dbf..000000000 --- a/lib/registerTypes/TypesMapObjects3.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * TypesMapObjects3.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesMapObjectTypes(BinaryDeserializer & s); -template void registerTypesMapObjectTypes(BinarySerializer & s); -template void registerTypesMapObjectTypes(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesServerPacks.cpp b/lib/registerTypes/TypesServerPacks.cpp deleted file mode 100644 index 3d37cbc92..000000000 --- a/lib/registerTypes/TypesServerPacks.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * TypesServerPacks.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesServerPacks(BinaryDeserializer & s); -template void registerTypesServerPacks(BinarySerializer & s); -template void registerTypesServerPacks(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 4cab55235..9eb95649a 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -34,14 +34,16 @@ std::optional Rewardable::Configuration::getVariable(const std::string & ca return std::nullopt; } -JsonNode Rewardable::Configuration::getPresetVariable(const std::string & category, const std::string & name) const +const JsonNode & Rewardable::Configuration::getPresetVariable(const std::string & category, const std::string & name) const { + static const JsonNode emptyNode; + std::string variableID = category + '@' + name; if (variables.preset.count(variableID)) return variables.preset.at(variableID); else - return JsonNode(); + return emptyNode; } void Rewardable::Configuration::presetVariable(const std::string & category, const std::string & name, const JsonNode & value) diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 602142343..8815c4640 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -69,7 +69,7 @@ struct DLL_LINKAGE ResetInfo void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & period; h & visitors; @@ -94,7 +94,7 @@ struct DLL_LINKAGE VisitInfo void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & limiter; h & reward; @@ -114,7 +114,7 @@ struct DLL_LINKAGE Variables void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & values; h & preset; @@ -168,13 +168,13 @@ struct DLL_LINKAGE Configuration ui16 getResetDuration() const; std::optional getVariable(const std::string & category, const std::string & name) const; - JsonNode getPresetVariable(const std::string & category, const std::string & name) const; + const JsonNode & getPresetVariable(const std::string & category, const std::string & name) const; void presetVariable(const std::string & category, const std::string & name, const JsonNode & value); void initVariable(const std::string & category, const std::string & name, int value); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & onSelect; h & description; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index aadf30285..14b73fa6c 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -17,9 +17,10 @@ #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" -#include "../JsonRandom.h" +#include "../json/JsonRandom.h" #include "../mapObjects/IObjectInterface.h" #include "../modding/IdentifierStorage.h" +#include "../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -74,7 +75,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){ if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@') - VLC->generaltexth->registerString(entry.meta, textID, entry.String()); + VLC->generaltexth->registerString(entry.getModScope(), textID, entry.String()); }; parameters = objectConfig; @@ -105,14 +106,14 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o loadString(parameters["onEmptyMessage"], TextIdentifier(objectName, "onEmpty")); } -Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const +Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const { Rewardable::LimitersList result; for (const auto & input : source.Vector()) { auto newLimiter = std::make_shared(); - configureLimiter(object, rng, *newLimiter, input); + configureLimiter(object, rng, cb, *newLimiter, input); result.push_back(newLimiter); } @@ -120,65 +121,67 @@ Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Conf return result; } -void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const +void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Limiter & limiter, const JsonNode & source) const { auto const & variables = object.variables.values; + JsonRandom randomizer(cb); - limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng, variables); - limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng, variables); - limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); - limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); + limiter.dayOfWeek = randomizer.loadValue(source["dayOfWeek"], rng, variables); + limiter.daysPassed = randomizer.loadValue(source["daysPassed"], rng, variables); + limiter.heroExperience = randomizer.loadValue(source["heroExperience"], rng, variables); + limiter.heroLevel = randomizer.loadValue(source["heroLevel"], rng, variables); limiter.canLearnSkills = source["canLearnSkills"].Bool(); - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables); - limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng, variables); + limiter.manaPercentage = randomizer.loadValue(source["manaPercentage"], rng, variables); + limiter.manaPoints = randomizer.loadValue(source["manaPoints"], rng, variables); - limiter.resources = JsonRandom::loadResources(source["resources"], rng, variables); + limiter.resources = randomizer.loadResources(source["resources"], rng, variables); - limiter.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); - limiter.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); - limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); - limiter.spells = JsonRandom::loadSpells(source["spells"], rng, variables); - limiter.canLearnSpells = JsonRandom::loadSpells(source["canLearnSpells"], rng, variables); - limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); + limiter.primary = randomizer.loadPrimaries(source["primary"], rng, variables); + limiter.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables); + limiter.artifacts = randomizer.loadArtifacts(source["artifacts"], rng, variables); + limiter.spells = randomizer.loadSpells(source["spells"], rng, variables); + limiter.canLearnSpells = randomizer.loadSpells(source["canLearnSpells"], rng, variables); + limiter.creatures = randomizer.loadCreatures(source["creatures"], rng, variables); - limiter.players = JsonRandom::loadColors(source["colors"], rng, variables); - limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng); - limiter.heroClasses = JsonRandom::loadHeroClasses(source["heroClasses"], rng); + limiter.players = randomizer.loadColors(source["colors"], rng, variables); + limiter.heroes = randomizer.loadHeroes(source["heroes"], rng); + limiter.heroClasses = randomizer.loadHeroClasses(source["heroClasses"], rng); - limiter.allOf = configureSublimiters(object, rng, source["allOf"] ); - limiter.anyOf = configureSublimiters(object, rng, source["anyOf"] ); - limiter.noneOf = configureSublimiters(object, rng, source["noneOf"] ); + limiter.allOf = configureSublimiters(object, rng, cb, source["allOf"] ); + limiter.anyOf = configureSublimiters(object, rng, cb, source["anyOf"] ); + limiter.noneOf = configureSublimiters(object, rng, cb, source["noneOf"] ); } -void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Reward & reward, const JsonNode & source) const +void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Reward & reward, const JsonNode & source) const { auto const & variables = object.variables.values; + JsonRandom randomizer(cb); - reward.resources = JsonRandom::loadResources(source["resources"], rng, variables); + reward.resources = randomizer.loadResources(source["resources"], rng, variables); - reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); - reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); + reward.heroExperience = randomizer.loadValue(source["heroExperience"], rng, variables); + reward.heroLevel = randomizer.loadValue(source["heroLevel"], rng, variables); - reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng, variables); - reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng, variables); - reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables, -1); + reward.manaDiff = randomizer.loadValue(source["manaPoints"], rng, variables); + reward.manaOverflowFactor = randomizer.loadValue(source["manaOverflowFactor"], rng, variables); + reward.manaPercentage = randomizer.loadValue(source["manaPercentage"], rng, variables, -1); - reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng, variables); - reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, variables, -1); + reward.movePoints = randomizer.loadValue(source["movePoints"], rng, variables); + reward.movePercentage = randomizer.loadValue(source["movePercentage"], rng, variables, -1); reward.removeObject = source["removeObject"].Bool(); - reward.bonuses = JsonRandom::loadBonuses(source["bonuses"]); + reward.bonuses = randomizer.loadBonuses(source["bonuses"]); - reward.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); - reward.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); + reward.primary = randomizer.loadPrimaries(source["primary"], rng, variables); + reward.secondary = randomizer.loadSecondaries(source["secondary"], rng, variables); - reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); - reward.spells = JsonRandom::loadSpells(source["spells"], rng, variables); - reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); + reward.artifacts = randomizer.loadArtifacts(source["artifacts"], rng, variables); + reward.spells = randomizer.loadSpells(source["spells"], rng, variables); + reward.creatures = randomizer.loadCreatures(source["creatures"], rng, variables); if(!source["spellCast"].isNull() && source["spellCast"].isStruct()) { - reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng, variables); + reward.spellCast.first = randomizer.loadSpell(source["spellCast"]["spell"], rng, variables); reward.spellCast.second = source["spellCast"]["schoolLevel"].Integer(); } @@ -187,21 +190,21 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand auto const & entry = source["revealTiles"]; reward.revealTiles = RewardRevealTiles(); - reward.revealTiles->radius = JsonRandom::loadValue(entry["radius"], rng, variables); + reward.revealTiles->radius = randomizer.loadValue(entry["radius"], rng, variables); reward.revealTiles->hide = entry["hide"].Bool(); - reward.revealTiles->scoreSurface = JsonRandom::loadValue(entry["surface"], rng, variables); - reward.revealTiles->scoreSubterra = JsonRandom::loadValue(entry["subterra"], rng, variables); - reward.revealTiles->scoreWater = JsonRandom::loadValue(entry["water"], rng, variables); - reward.revealTiles->scoreRock = JsonRandom::loadValue(entry["rock"], rng, variables); + reward.revealTiles->scoreSurface = randomizer.loadValue(entry["surface"], rng, variables); + reward.revealTiles->scoreSubterra = randomizer.loadValue(entry["subterra"], rng, variables); + reward.revealTiles->scoreWater = randomizer.loadValue(entry["water"], rng, variables); + reward.revealTiles->scoreRock = randomizer.loadValue(entry["rock"], rng, variables); } for ( auto node : source["changeCreatures"].Struct() ) { - CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value()); - CreatureID dest(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.second.String()).value()); + CreatureID from(VLC->identifiers()->getIdentifier(node.second.getModScope(), "creature", node.first).value()); + CreatureID dest(VLC->identifiers()->getIdentifier(node.second.getModScope(), "creature", node.second.String()).value()); - reward.extraComponents.emplace_back(Component::EComponentType::CREATURE, dest.getNum(), 0, 0); + reward.extraComponents.emplace_back(ComponentType::CREATURE, dest); reward.creaturesChange[from] = dest; } @@ -214,8 +217,10 @@ void Rewardable::Info::configureResetInfo(Rewardable::Configuration & object, CR resetParameters.rewards = source["rewards"].Bool(); } -void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const +void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const { + JsonRandom randomizer(cb); + for(const auto & category : source.Struct()) { for(const auto & entry : category.second.Struct()) @@ -225,19 +230,19 @@ void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CR int32_t value = -1; if (category.first == "number") - value = JsonRandom::loadValue(input, rng, object.variables.values); + value = randomizer.loadValue(input, rng, object.variables.values); if (category.first == "artifact") - value = JsonRandom::loadArtifact(input, rng, object.variables.values).getNum(); + value = randomizer.loadArtifact(input, rng, object.variables.values).getNum(); if (category.first == "spell") - value = JsonRandom::loadSpell(input, rng, object.variables.values).getNum(); + value = randomizer.loadSpell(input, rng, object.variables.values).getNum(); if (category.first == "primarySkill") - value = static_cast(JsonRandom::loadPrimary(input, rng, object.variables.values)); + value = randomizer.loadPrimary(input, rng, object.variables.values).getNum(); if (category.first == "secondarySkill") - value = JsonRandom::loadSecondary(input, rng, object.variables.values).getNum(); + value = randomizer.loadSecondary(input, rng, object.variables.values).getNum(); object.initVariable(category.first, entry.first, value); } @@ -249,41 +254,42 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab for (const auto & variable : variables.values ) { if( boost::algorithm::starts_with(variable.first, "spell")) - target.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + target.replaceName(SpellID(variable.second)); if( boost::algorithm::starts_with(variable.first, "secondarySkill")) - target.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + target.replaceName(SecondarySkill(variable.second)); } } void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const { for (const auto & artifact : info.reward.artifacts ) - target.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); + target.replaceName(artifact); - for (const auto & artifact : info.reward.spells ) - target.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + for (const auto & spell : info.reward.spells ) + target.replaceName(spell); for (const auto & secondary : info.reward.secondary ) - target.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); + target.replaceName(secondary.first); replaceTextPlaceholders(target, variables); } void Rewardable::Info::configureRewards( Rewardable::Configuration & object, - CRandomGenerator & rng, const - JsonNode & source, + CRandomGenerator & rng, + IGameCallback * cb, + const JsonNode & source, Rewardable::EEventType event, const std::string & modeName) const { for(size_t i = 0; i < source.Vector().size(); ++i) { - const JsonNode reward = source.Vector()[i]; + const JsonNode & reward = source.Vector().at(i); if (!reward["appearChance"].isNull()) { - JsonNode chance = reward["appearChance"]; + const JsonNode & chance = reward["appearChance"]; std::string diceID = std::to_string(chance["dice"].Integer()); auto diceValue = object.getVariable("dice", diceID); @@ -315,8 +321,8 @@ void Rewardable::Info::configureRewards( } Rewardable::VisitInfo info; - configureLimiter(object, rng, info.limiter, reward["limiter"]); - configureReward(object, rng, info.reward, reward); + configureLimiter(object, rng, cb, info.limiter, reward["limiter"]); + configureReward(object, rng, cb, info.reward, reward); info.visitType = event; info.message = loadMessage(reward["message"], TextIdentifier(objectTextID, modeName, i)); @@ -329,15 +335,15 @@ void Rewardable::Info::configureRewards( } } -void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRandomGenerator & rng) const +void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb) const { object.info.clear(); - configureVariables(object, rng, parameters["variables"]); + configureVariables(object, rng, cb, parameters["variables"]); - configureRewards(object, rng, parameters["rewards"], Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); - configureRewards(object, rng, parameters["onVisited"], Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); - configureRewards(object, rng, parameters["onEmpty"], Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); + configureRewards(object, rng, cb, parameters["rewards"], Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); + configureRewards(object, rng, cb, parameters["onVisited"], Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); + configureRewards(object, rng, cb, parameters["onEmpty"], Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); object.description = loadMessage(parameters["description"], TextIdentifier(objectTextID, "description")); @@ -401,7 +407,7 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand } if (object.visitMode == Rewardable::VISIT_LIMITER) - configureLimiter(object, rng, object.visitLimiter, parameters["visitLimiter"]); + configureLimiter(object, rng, cb, object.visitLimiter, parameters["visitLimiter"]); } diff --git a/lib/rewardable/Info.h b/lib/rewardable/Info.h index 5ad96ca40..e2bb4322f 100644 --- a/lib/rewardable/Info.h +++ b/lib/rewardable/Info.h @@ -10,13 +10,14 @@ #pragma once -#include "../JsonNode.h" +#include "../json/JsonNode.h" #include "../mapObjectConstructors/IObjectInfo.h" VCMI_LIB_NAMESPACE_BEGIN class CRandomGenerator; class MetaString; +class IGameCallback; namespace Rewardable { @@ -38,13 +39,13 @@ class DLL_LINKAGE Info : public IObjectInfo void replaceTextPlaceholders(MetaString & target, const Variables & variables) const; void replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const; - void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; - void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; + void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const; + void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; - void configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const; - Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; + void configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Limiter & limiter, const JsonNode & source) const; + Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, const JsonNode & source) const; - void configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Reward & info, const JsonNode & source) const; + void configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb, Rewardable::Reward & info, const JsonNode & source) const; void configureResetInfo(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::ResetInfo & info, const JsonNode & source) const; public: const JsonNode & getParameters() const; @@ -64,11 +65,11 @@ public: bool givesBonuses() const override; - void configureObject(Rewardable::Configuration & object, CRandomGenerator & rng) const; + void configureObject(Rewardable::Configuration & object, CRandomGenerator & rng, IGameCallback * cb) const; void init(const JsonNode & objectConfig, const std::string & objectTextID); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & parameters; } diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index da7f86520..d7358efb9 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -35,7 +35,7 @@ std::vector Rewardable::Interface::getAvailableRewards(const CGHeroInstanc { const Rewardable::VisitInfo & visit = configuration.info[i]; - if(event == visit.visitType && visit.limiter.heroAllowed(hero)) + if(event == visit.visitType && (!hero || visit.limiter.heroAllowed(hero))) { logGlobal->trace("Reward %d is allowed", i); ret.push_back(static_cast(i)); @@ -117,7 +117,7 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R for(int i=0; i< info.reward.primary.size(); i++) cb->changePrimSkill(hero, static_cast(i), info.reward.primary[i], false); - si64 expToGive = 0; + TExpType expToGive = 0; if (info.reward.heroLevel > 0) expToGive += VLC->heroh->reqExp(hero->level+info.reward.heroLevel) - VLC->heroh->reqExp(hero->level); @@ -126,7 +126,7 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R expToGive += hero->calculateXp(info.reward.heroExperience); if(expToGive) - cb->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive); + cb->giveExperience(hero, expToGive); } void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Rewardable::VisitInfo & info, const CArmedInstance * army, const CGHeroInstance * hero) const @@ -150,14 +150,14 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re for(const Bonus & bonus : info.reward.bonuses) { GiveBonus gb; - gb.who = GiveBonus::ETarget::HERO; + gb.who = GiveBonus::ETarget::OBJECT; gb.bonus = bonus; - gb.id = hero->id.getNum(); + gb.id = hero->id; cb->giveHeroBonus(&gb); } for(const ArtifactID & art : info.reward.artifacts) - cb->giveHeroNewArtifact(hero, VLC->arth->objects[art],ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(hero, art.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); if(!info.reward.spells.empty()) { diff --git a/lib/rewardable/Interface.h b/lib/rewardable/Interface.h index ac85e6b72..3ff6980c9 100644 --- a/lib/rewardable/Interface.h +++ b/lib/rewardable/Interface.h @@ -44,7 +44,7 @@ public: void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & configuration; } diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index ae6602231..339f45415 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -30,6 +30,7 @@ Rewardable::Limiter::Limiter() , heroLevel(-1) , manaPercentage(0) , manaPoints(0) + , canLearnSkills(false) , primary(GameConstants::PRIMARY_SKILLS, 0) { } @@ -45,6 +46,7 @@ bool operator==(const Rewardable::Limiter & l, const Rewardable::Limiter & r) && l.manaPoints == r.manaPoints && l.manaPercentage == r.manaPercentage && l.secondary == r.secondary + && l.canLearnSkills == r.canLearnSkills && l.creatures == r.creatures && l.spells == r.spells && l.artifacts == r.artifacts @@ -67,13 +69,13 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const { if(dayOfWeek != 0) { - if (IObjectInterface::cb->getDate(Date::DAY_OF_WEEK) != dayOfWeek) + if (hero->cb->getDate(Date::DAY_OF_WEEK) != dayOfWeek) return false; } if(daysPassed != 0) { - if (IObjectInterface::cb->getDate(Date::DAY) < daysPassed) + if (hero->cb->getDate(Date::DAY) < daysPassed) return false; } @@ -90,7 +92,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } - if(!IObjectInterface::cb->getPlayerState(hero->tempOwner)->resources.canAfford(resources)) + if(!hero->cb->getPlayerState(hero->tempOwner)->resources.canAfford(resources)) return false; if(heroLevel > static_cast(hero->level)) @@ -128,7 +130,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const for(const auto & spell : canLearnSpells) { - if (!hero->canLearnSpell(spell.toSpell(VLC->spells()), true)) + if (!hero->canLearnSpell(spell.toEntity(VLC), true)) return false; } @@ -187,49 +189,45 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, const CGHeroInstance * h) const { if (heroExperience) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); + comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h ? h->calculateXp(heroExperience) : heroExperience)); if (heroLevel > 0) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); + comps.emplace_back(ComponentType::EXPERIENCE, heroLevel); if (manaPoints || manaPercentage > 0) { - int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); + int absoluteMana = (h && h->manaLimit()) ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + comps.emplace_back(ComponentType::MANA, absoluteMana + manaPoints); } for (size_t i=0; i(i), primary[i], 0); + comps.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill(i), primary[i]); } for(const auto & entry : secondary) - comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0); + comps.emplace_back(ComponentType::SEC_SKILL, entry.first, entry.second); for(const auto & entry : artifacts) - comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0); + comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0); + comps.emplace_back(ComponentType::SPELL, entry); for(const auto & entry : creatures) - comps.emplace_back(Component::EComponentType::CREATURE, entry.type->getId(), entry.count, 0); + comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); for(const auto & entry : players) - comps.emplace_back(Component::EComponentType::FLAG, entry, 0, 0); + comps.emplace_back(ComponentType::FLAG, entry); - //FIXME: portrait may not match hero, if custom portrait was set in map editor for(const auto & entry : heroes) - comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroTypes()->getById(entry)->getIconIndex(), 0, 0); + comps.emplace_back(ComponentType::HERO_PORTRAIT, entry); - for(const auto & entry : heroClasses) - comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroClasses()->getById(entry)->getIconIndex(), 0, 0); - for (size_t i=0; i(i), resources[i], 0); + comps.emplace_back(ComponentType::RESOURCE, GameResID(i), resources[i]); } } @@ -253,7 +251,7 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) std::vector> fieldValue(secondary.begin(), secondary.end()); a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) { - h.serializeId("skill", e.first, SecondarySkill{}, VLC->skillh->decodeSkill, VLC->skillh->encodeSkill); + h.serializeId("skill", e.first, SecondarySkill(SecondarySkill::NONE)); h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); }); a.syncSize(fieldValue); diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 407db0f24..969bcd576 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -92,7 +92,7 @@ struct DLL_LINKAGE Limiter final void loadComponents(std::vector & comps, const CGHeroInstance * h) const; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & dayOfWeek; h & daysPassed; diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index cb080adca..8b774f2c6 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -33,6 +33,7 @@ Rewardable::Reward::Reward() , heroLevel(0) , manaDiff(0) , manaPercentage(-1) + , manaOverflowFactor(0) , movePoints(0) , movePercentage(-1) , primary(4, 0) @@ -74,42 +75,42 @@ void Rewardable::Reward::loadComponents(std::vector & comps, const CG for (auto & bonus : bonuses) { if (bonus.type == BonusType::MORALE) - comps.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0); + comps.emplace_back(ComponentType::MORALE, bonus.val); if (bonus.type == BonusType::LUCK) - comps.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0); + comps.emplace_back(ComponentType::LUCK, bonus.val); } if (heroExperience) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h ? h->calculateXp(heroExperience) : heroExperience), 0); + comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h ? h->calculateXp(heroExperience) : heroExperience)); if (heroLevel) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); + comps.emplace_back(ComponentType::LEVEL, heroLevel); if (manaDiff || manaPercentage >= 0) - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, h ? (calculateManaPoints(h) - h->mana) : manaDiff, 0); + comps.emplace_back(ComponentType::MANA, h ? (calculateManaPoints(h) - h->mana) : manaDiff); for (size_t i=0; i(i), primary[i], 0); + comps.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill(i), primary[i]); } for(const auto & entry : secondary) - comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0); + comps.emplace_back(ComponentType::SEC_SKILL, entry.first, entry.second); for(const auto & entry : artifacts) - comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0); + comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0); + comps.emplace_back(ComponentType::SPELL, entry); for(const auto & entry : creatures) - comps.emplace_back(Component::EComponentType::CREATURE, entry.type->getId(), entry.count, 0); + comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); for (size_t i=0; i(i), resources[i], 0); + comps.emplace_back(ComponentType::RESOURCE, GameResID(i), resources[i]); } } @@ -133,7 +134,7 @@ void Rewardable::Reward::serializeJson(JsonSerializeFormat & handler) std::vector> fieldValue(secondary.begin(), secondary.end()); a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) { - h.serializeId("skill", e.first, SecondarySkill{}, VLC->skillh->decodeSkill, VLC->skillh->encodeSkill); + h.serializeId("skill", e.first); h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); }); a.syncSize(fieldValue); diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 5c4fd84a2..a7410a241 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -45,7 +45,7 @@ struct RewardRevealTiles void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & radius; h & scoreSurface; @@ -119,7 +119,7 @@ struct DLL_LINKAGE Reward final Reward(); ~Reward(); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & resources; h & extraComponents; diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 1a0265327..fe233bf99 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -22,10 +22,11 @@ VCMI_LIB_NAMESPACE_BEGIN CMapGenOptions::CMapGenOptions() : width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), hasTwoLevels(true), - playerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), - waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr) + humanOrCpuPlayerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), + waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr), + customizedPlayers(false) { - resetPlayersMap(); + initPlayersMap(); setRoadEnabled(RoadId(Road::DIRT_ROAD), true); setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true); setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true); @@ -63,23 +64,91 @@ void CMapGenOptions::setHasTwoLevels(bool value) hasTwoLevels = value; } -si8 CMapGenOptions::getPlayerCount() const +si8 CMapGenOptions::getHumanOrCpuPlayerCount() const { - return playerCount; + return humanOrCpuPlayerCount; } -void CMapGenOptions::setPlayerCount(si8 value) +void CMapGenOptions::setHumanOrCpuPlayerCount(si8 value) { assert((value >= 1 && value <= PlayerColor::PLAYER_LIMIT_I) || value == RANDOM_SIZE); - playerCount = value; + humanOrCpuPlayerCount = value; - auto possibleCompPlayersCount = PlayerColor::PLAYER_LIMIT_I - value; + // Use template player limit, if any + auto playerLimit = getPlayerLimit(); + auto possibleCompPlayersCount = playerLimit - std::max(0, humanOrCpuPlayerCount); if (compOnlyPlayerCount > possibleCompPlayersCount) + { setCompOnlyPlayerCount(possibleCompPlayersCount); + } resetPlayersMap(); } +si8 CMapGenOptions::getMinPlayersCount(bool withTemplateLimit) const +{ + auto totalPlayers = 0; + si8 humans = getHumanOrCpuPlayerCount(); + si8 cpus = getCompOnlyPlayerCount(); + + if (humans == RANDOM_SIZE && cpus == RANDOM_SIZE) + { + totalPlayers = 2; + } + else if (humans == RANDOM_SIZE) + { + totalPlayers = cpus + 1; // Must add at least 1 player + } + else if (cpus == RANDOM_SIZE) + { + totalPlayers = humans; + } + else + { + totalPlayers = humans + cpus; + } + + if (withTemplateLimit && mapTemplate) + { + auto playersRange = mapTemplate->getPlayers(); + + //New template can also impose higher limit than current settings + vstd::amax(totalPlayers, playersRange.minValue()); + } + + // Can't play without at least 2 players + vstd::amax(totalPlayers, 2); + return totalPlayers; +} + +si8 CMapGenOptions::getMaxPlayersCount(bool withTemplateLimit) const +{ + // Max number of players possible with current settings + auto totalPlayers = 0; + si8 humans = getHumanOrCpuPlayerCount(); + si8 cpus = getCompOnlyPlayerCount(); + if (humans == RANDOM_SIZE || cpus == RANDOM_SIZE) + { + totalPlayers = PlayerColor::PLAYER_LIMIT_I; + } + else + { + totalPlayers = humans + cpus; + } + + if (withTemplateLimit && mapTemplate) + { + auto playersRange = mapTemplate->getPlayers(); + + //New template can also impose higher limit than current settings + vstd::amin(totalPlayers, playersRange.maxValue()); + } + + assert (totalPlayers <= PlayerColor::PLAYER_LIMIT_I); + assert (totalPlayers >= 2); + return totalPlayers; +} + si8 CMapGenOptions::getTeamCount() const { return teamCount; @@ -87,7 +156,7 @@ si8 CMapGenOptions::getTeamCount() const void CMapGenOptions::setTeamCount(si8 value) { - assert(getPlayerCount() == RANDOM_SIZE || (value >= 0 && value < getPlayerCount()) || value == RANDOM_SIZE); + assert(getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value < getHumanOrCpuPlayerCount()) || value == RANDOM_SIZE); teamCount = value; } @@ -96,9 +165,20 @@ si8 CMapGenOptions::getCompOnlyPlayerCount() const return compOnlyPlayerCount; } +si8 CMapGenOptions::getPlayerLimit() const +{ + //How many players could we set with current template, ignoring other settings + si8 playerLimit = PlayerColor::PLAYER_LIMIT_I; + if (auto temp = getMapTemplate()) + { + playerLimit = static_cast(temp->getPlayers().maxValue()); + } + return playerLimit; +} + void CMapGenOptions::setCompOnlyPlayerCount(si8 value) { - assert(value == RANDOM_SIZE || (getPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= PlayerColor::PLAYER_LIMIT_I - getPlayerCount()))); + assert(value == RANDOM_SIZE || (getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= getPlayerLimit() - getHumanOrCpuPlayerCount()))); compOnlyPlayerCount = value; resetPlayersMap(); @@ -135,7 +215,7 @@ void CMapGenOptions::setMonsterStrength(EMonsterStrength::EMonsterStrength value monsterStrength = value; } -void CMapGenOptions::resetPlayersMap() +void CMapGenOptions::initPlayersMap() { std::map rememberTownTypes; @@ -144,42 +224,156 @@ void CMapGenOptions::resetPlayersMap() for(const auto & p : players) { auto town = p.second.getStartingTown(); - if (town != RANDOM_SIZE) + if (town != FactionID::RANDOM) rememberTownTypes[p.first] = FactionID(town); rememberTeam[p.first] = p.second.getTeam(); } players.clear(); - int realPlayersCnt = playerCount; - int realCompOnlyPlayersCnt = (compOnlyPlayerCount == RANDOM_SIZE) ? (PlayerColor::PLAYER_LIMIT_I - realPlayersCnt) : compOnlyPlayerCount; - int totalPlayersLimit = realPlayersCnt + realCompOnlyPlayersCnt; - if (getPlayerCount() == RANDOM_SIZE || compOnlyPlayerCount == RANDOM_SIZE) - totalPlayersLimit = static_cast(PlayerColor::PLAYER_LIMIT_I); + int realPlayersCnt = getHumanOrCpuPlayerCount(); - //FIXME: what happens with human players here? - for(int color = 0; color < totalPlayersLimit; ++color) + // Initialize settings for all color even if not present + for(int color = 0; color < getPlayerLimit(); ++color) { CPlayerSettings player; auto pc = PlayerColor(color); player.setColor(pc); + auto playerType = EPlayerType::AI; - if (getPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) + // Color doesn't have to be continuous. Player colors can later be changed manually + if (getHumanOrCpuPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) { playerType = EPlayerType::HUMAN; } - else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) - || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) + else if((getHumanOrCpuPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) + || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I - compOnlyPlayerCount))) { playerType = EPlayerType::COMP_ONLY; } player.setPlayerType(playerType); - player.setTeam(rememberTeam[pc]); - players[pc] = player; - if (vstd::contains(rememberTownTypes, pc)) - players[pc].setStartingTown(rememberTownTypes[pc]); + players[pc] = player; } + savePlayersMap(); +} + +void CMapGenOptions::resetPlayersMap() +{ + // Called when number of player changes + // But do not update info about already made selections + + savePlayersMap(); + + int realPlayersCnt = getMaxPlayersCount(); + + //Trim the number of AI players, then CPU-only players, finally human players + auto eraseLastPlayer = [this](EPlayerType playerType) -> bool + { + for (auto it = players.rbegin(); it != players.rend(); ++it) + { + if (it->second.getPlayerType() == playerType) + { + players.erase(it->first); + return true; + } + } + return false; //Can't earse any player of this type + }; + + while (players.size() > realPlayersCnt) + { + while (eraseLastPlayer(EPlayerType::AI)); + while (eraseLastPlayer(EPlayerType::COMP_ONLY)); + while (eraseLastPlayer(EPlayerType::HUMAN)); + } + + //First colors from the list are assigned to human players, then to CPU players + std::vector availableColors; + for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; color++) + { + availableColors.push_back(PlayerColor(color)); + } + + auto removeUsedColors = [this, &availableColors](EPlayerType playerType) + { + for (auto& player : players) + { + if (player.second.getPlayerType() == playerType) + { + vstd::erase(availableColors, player.second.getColor()); + } + } + }; + removeUsedColors(EPlayerType::HUMAN); + removeUsedColors(EPlayerType::COMP_ONLY); + //removeUsedColors(EPlayerType::AI); + + //Assign unused colors to remaining AI players + while (players.size() < realPlayersCnt && !availableColors.empty()) + { + auto color = availableColors.front(); + players[color].setColor(color); + setPlayerTypeForStandardPlayer(color, EPlayerType::AI); + availableColors.erase(availableColors.begin()); + + if (vstd::contains(savedPlayerSettings, color)) + { + setPlayerTeam(color, savedPlayerSettings.at(color).getTeam()); + // TODO: setter + players[color].setStartingTown(savedPlayerSettings.at(color).getStartingTown()); + } + else + { + logGlobal->warn("Adding settings for player %s", color); + // Usually, all players should be initialized in initPlayersMap() + CPlayerSettings settings; + players[color] = settings; + } + } + + std::set occupiedTeams; + for(auto & player : players) + { + auto team = player.second.getTeam(); + if (team != TeamID::NO_TEAM) + { + occupiedTeams.insert(team); + } + } + // TODO: Handle situation when we remove a player and remaining players belong to only one team + + for(auto & player : players) + { + if (player.second.getTeam() == TeamID::NO_TEAM) + { + //Find first unused team + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + TeamID team(i); + if(!occupiedTeams.count(team)) + { + player.second.setTeam(team); + occupiedTeams.insert(team); + break; + } + } + } + } +} + +void CMapGenOptions::savePlayersMap() +{ + //Only save already configured players + for (const auto& player : players) + { + savedPlayerSettings[player.first] = player.second; + } +} + +const std::map & CMapGenOptions::getSavedPlayersMap() const +{ + return savedPlayerSettings; } const std::map & CMapGenOptions::getPlayersSettings() const @@ -187,19 +381,28 @@ const std::map & CMapGenOptions::g return players; } -void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, si32 town) +void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, FactionID town) { auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setStartingTown(town); } +void CMapGenOptions::setStartingHeroForPlayer(const PlayerColor & color, HeroTypeID hero) +{ + auto it = players.find(color); + assert(it != players.end()); + it->second.setStartingHero(hero); +} + void CMapGenOptions::setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType) { + // FIXME: Why actually not set it to COMP_ONLY? Ie. when swapping human to another color? assert(playerType != EPlayerType::COMP_ONLY); auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setPlayerType(playerType); + customizedPlayers = true; } const CRmgTemplate * CMapGenOptions::getMapTemplate() const @@ -209,6 +412,12 @@ const CRmgTemplate * CMapGenOptions::getMapTemplate() const void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) { + if (mapTemplate == value) + { + //Does not trigger during deserialization + return; + } + mapTemplate = value; //validate & adapt options according to template if(mapTemplate) @@ -220,13 +429,21 @@ void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) setHeight(sizes.first.y); setHasTwoLevels(sizes.first.z - 1); } - - if(!mapTemplate->getPlayers().isInRange(getPlayerCount())) - setPlayerCount(RANDOM_SIZE); - if(!mapTemplate->getCpuPlayers().isInRange(getCompOnlyPlayerCount())) + + si8 maxPlayerCount = getMaxPlayersCount(false); + si8 minPlayerCount = getMinPlayersCount(false); + + // Neither setting can fit within the template range + if(!mapTemplate->getPlayers().isInRange(minPlayerCount) && + !mapTemplate->getPlayers().isInRange(maxPlayerCount)) + { + setHumanOrCpuPlayerCount(RANDOM_SIZE); setCompOnlyPlayerCount(RANDOM_SIZE); + } if(!mapTemplate->getWaterContentAllowed().count(getWaterContent())) setWaterContent(EWaterContent::RANDOM); + + resetPlayersMap(); // Update teams and player count } } @@ -261,15 +478,16 @@ bool CMapGenOptions::isRoadEnabled() const void CMapGenOptions::setPlayerTeam(const PlayerColor & color, const TeamID & team) { auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setTeam(team); + customizedPlayers = true; } void CMapGenOptions::finalize(CRandomGenerator & rand) { logGlobal->info("RMG map: %dx%d, %s underground", getWidth(), getHeight(), getHasTwoLevels() ? "WITH" : "NO"); logGlobal->info("RMG settings: players %d, teams %d, computer players %d, computer teams %d, water %d, monsters %d", - static_cast(getPlayerCount()), static_cast(getTeamCount()), static_cast(getCompOnlyPlayerCount()), + static_cast(getHumanOrCpuPlayerCount()), static_cast(getTeamCount()), static_cast(getCompOnlyPlayerCount()), static_cast(getCompOnlyTeamCount()), static_cast(getWaterContent()), static_cast(getMonsterStrength())); if(!mapTemplate) @@ -280,25 +498,44 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) logGlobal->info("RMG template name: %s", mapTemplate->getName()); - if (getPlayerCount() == RANDOM_SIZE) + auto maxPlayers = getMaxPlayersCount(); + if (getHumanOrCpuPlayerCount() == RANDOM_SIZE) { auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); //ignore all non-randomized players, make sure these players will not be missing after roll possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(countHumanPlayers() + countCompOnlyPlayers())); + + vstd::erase_if(possiblePlayers, [maxPlayers](int i) + { + return i > maxPlayers; + }); assert(!possiblePlayers.empty()); - setPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); + setHumanOrCpuPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); updatePlayers(); } if(teamCount == RANDOM_SIZE) { - teamCount = rand.nextInt(getPlayerCount() - 1); + teamCount = rand.nextInt(getHumanOrCpuPlayerCount() - 1); if (teamCount == 1) teamCount = 0; } if(compOnlyPlayerCount == RANDOM_SIZE) { - auto possiblePlayers = mapTemplate->getCpuPlayers().getNumbers(); - compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); + // Use remaining range + auto presentPlayers = getHumanOrCpuPlayerCount(); + auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); + vstd::erase_if(possiblePlayers, [maxPlayers, presentPlayers](int i) + { + return i > (maxPlayers - presentPlayers); + }); + if (possiblePlayers.empty()) + { + compOnlyPlayerCount = 0; + } + else + { + compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); + } updateCompOnlyPlayers(); } if(compOnlyTeamCount == RANDOM_SIZE) @@ -328,11 +565,6 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) assert (vstd::iswithin(waterContent, EWaterContent::NONE, EWaterContent::ISLANDS)); assert (vstd::iswithin(monsterStrength, EMonsterStrength::GLOBAL_WEAK, EMonsterStrength::GLOBAL_STRONG)); - - //rectangular maps are the future of gaming - //setHeight(20); - //setWidth(50); - logGlobal->trace("Player config:"); int cpuOnlyPlayers = 0; for(const auto & player : players) @@ -355,19 +587,18 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) } logGlobal->trace("Player %d: %s", player.second.getColor(), playerType); } - setCompOnlyPlayerCount(cpuOnlyPlayers); //human players are set automaticlaly (?) - logGlobal->info("Final player config: %d total, %d cpu-only", players.size(), static_cast(getCompOnlyPlayerCount())); + logGlobal->info("Final player config: %d total, %d cpu-only", players.size(), cpuOnlyPlayers); } void CMapGenOptions::updatePlayers() { - // Remove AI players only from the end of the players map if necessary + // Remove non-human players only from the end of the players map if necessary for(auto itrev = players.end(); itrev != players.begin();) { auto it = itrev; --it; - if (players.size() == getPlayerCount()) break; - if(it->second.getPlayerType() == EPlayerType::AI) + if (players.size() == getHumanOrCpuPlayerCount()) break; + if(it->second.getPlayerType() != EPlayerType::HUMAN) { players.erase(it); } @@ -385,7 +616,7 @@ void CMapGenOptions::updateCompOnlyPlayers() { auto it = itrev; --it; - if (players.size() <= getPlayerCount()) break; + if (players.size() <= getHumanOrCpuPlayerCount()) break; if(it->second.getPlayerType() == EPlayerType::COMP_ONLY) { players.erase(it); @@ -397,11 +628,11 @@ void CMapGenOptions::updateCompOnlyPlayers() } // Add some comp only players if necessary - int compOnlyPlayersToAdd = static_cast(getPlayerCount() - players.size()); + int compOnlyPlayersToAdd = static_cast(getHumanOrCpuPlayerCount() - players.size()); if (compOnlyPlayersToAdd < 0) { - logGlobal->error("Incorrect number of players to add. Requested players %d, current players %d", playerCount, players.size()); + logGlobal->error("Incorrect number of players to add. Requested players %d, current players %d", humanOrCpuPlayerCount, players.size()); assert (compOnlyPlayersToAdd < 0); } for(int i = 0; i < compOnlyPlayersToAdd; ++i) @@ -457,6 +688,11 @@ bool CMapGenOptions::checkOptions() const } } +bool CMapGenOptions::arePlayersCustomized() const +{ + return customizedPlayers; +} + std::vector CMapGenOptions::getPossibleTemplates() const { int3 tplSize(width, height, (hasTwoLevels ? 2 : 1)); @@ -472,21 +708,32 @@ std::vector CMapGenOptions::getPossibleTemplates() const if(!tmpl->isWaterContentAllowed(getWaterContent())) return true; - if(getPlayerCount() != -1) + auto humanOrCpuPlayerCount = getHumanOrCpuPlayerCount(); + auto compOnlyPlayerCount = getCompOnlyPlayerCount(); + // Check if total number of players fall inside given range + + if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE && compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) { - if (!tmpl->getPlayers().isInRange(getPlayerCount())) + if (!tmpl->getPlayers().isInRange(humanOrCpuPlayerCount + compOnlyPlayerCount)) + return true; + + } + else if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE) + { + // We can always add any number CPU players, but not subtract + if (!(humanOrCpuPlayerCount <= tmpl->getPlayers().maxValue())) + return true; + } + else if(compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) + { + //We must fit at least one more human player, but can add any number + if (!(compOnlyPlayerCount < tmpl->getPlayers().maxValue())) return true; } else { // Human players shouldn't be banned when playing with random player count - if(humanPlayers > *boost::min_element(tmpl->getPlayers().getNumbers())) - return true; - } - - if(compOnlyPlayerCount != -1) - { - if (!tmpl->getCpuPlayers().isInRange(compOnlyPlayerCount)) + if(humanPlayers > tmpl->getPlayers().minValue()) return true; } @@ -506,7 +753,7 @@ const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand return *RandomGeneratorUtil::nextItem(templates, rand); } -CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) +CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), startingHero(HeroTypeID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) { } @@ -522,22 +769,33 @@ void CMapGenOptions::CPlayerSettings::setColor(const PlayerColor & value) color = value; } -si32 CMapGenOptions::CPlayerSettings::getStartingTown() const +FactionID CMapGenOptions::CPlayerSettings::getStartingTown() const { return startingTown; } -void CMapGenOptions::CPlayerSettings::setStartingTown(si32 value) +void CMapGenOptions::CPlayerSettings::setStartingTown(FactionID value) { - assert(value >= -1); - if(value >= 0) + assert(value >= FactionID::RANDOM); + if(value != FactionID::RANDOM) { - assert(value < static_cast(VLC->townh->size())); + assert(value < FactionID(VLC->townh->size())); assert((*VLC->townh)[value]->town != nullptr); } startingTown = value; } +HeroTypeID CMapGenOptions::CPlayerSettings::getStartingHero() const +{ + return startingHero; +} + +void CMapGenOptions::CPlayerSettings::setStartingHero(HeroTypeID value) +{ + assert(value == HeroTypeID::RANDOM || value.toEntity(VLC) != nullptr); + startingHero = value; +} + EPlayerType CMapGenOptions::CPlayerSettings::getPlayerType() const { return playerType; diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index 2e25fd671..47e33c797 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -42,8 +42,13 @@ public: /// The starting town of the player ranging from 0 to town max count or RANDOM_TOWN. /// The default value is RANDOM_TOWN. - si32 getStartingTown() const; - void setStartingTown(si32 value); + FactionID getStartingTown() const; + void setStartingTown(FactionID value); + + /// The starting hero of the player ranging from 0 to hero max count or RANDOM_HERO. + /// The default value is RANDOM_HERO + HeroTypeID getStartingHero() const; + void setStartingHero(HeroTypeID value); /// The default value is EPlayerType::AI. EPlayerType getPlayerType() const; @@ -55,19 +60,23 @@ public: private: PlayerColor color; - si32 startingTown; + FactionID startingTown; + HeroTypeID startingHero; EPlayerType playerType; TeamID team; public: template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & color; h & startingTown; h & playerType; - if(version >= 806) - h & team; + h & team; + if (h.version >= Handler::Version::RELEASE_143) + h & startingHero; + else + startingHero = HeroTypeID::RANDOM; } }; @@ -85,8 +94,12 @@ public: /// The count of all (human or computer) players ranging from 1 to PlayerColor::PLAYER_LIMIT or RANDOM_SIZE for random. If you call /// this method, all player settings are reset to default settings. - si8 getPlayerCount() const; - void setPlayerCount(si8 value); + si8 getHumanOrCpuPlayerCount() const; + void setHumanOrCpuPlayerCount(si8 value); + + si8 getMinPlayersCount(bool withTemplateLimit = true) const; + si8 getMaxPlayersCount(bool withTemplateLimit = true) const; + si8 getPlayerLimit() const; /// The count of the teams ranging from 0 to or RANDOM_SIZE for random. si8 getTeamCount() const; @@ -114,7 +127,9 @@ public: /// The first player colors belong to standard players and the last player colors belong to comp only players. /// All standard players are by default of type EPlayerType::AI. const std::map & getPlayersSettings() const; - void setStartingTownForPlayer(const PlayerColor & color, si32 town); + const std::map & getSavedPlayersMap() const; + void setStartingTownForPlayer(const PlayerColor & color, FactionID town); + void setStartingHeroForPlayer(const PlayerColor & color, HeroTypeID hero); /// Sets a player type for a standard player. A standard player is the opposite of a computer only player. The /// values which can be chosen for the player type are EPlayerType::AI or EPlayerType::HUMAN. void setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType); @@ -136,11 +151,15 @@ public: /// Returns false if there is no template available which fits to the currently selected options. bool checkOptions() const; + /// Returns true if player colors or teams were set in game GUI + bool arePlayersCustomized() const; static const si8 RANDOM_SIZE = -1; private: + void initPlayersMap(); void resetPlayersMap(); + void savePlayersMap(); int countHumanPlayers() const; int countCompOnlyPlayers() const; PlayerColor getNextPlayerColor() const; @@ -148,24 +167,30 @@ private: void updatePlayers(); const CRmgTemplate * getPossibleTemplate(CRandomGenerator & rand) const; - si32 width, height; + si32 width; + si32 height; bool hasTwoLevels; - si8 playerCount, teamCount, compOnlyPlayerCount, compOnlyTeamCount; + si8 humanOrCpuPlayerCount; + si8 teamCount; + si8 compOnlyPlayerCount; + si8 compOnlyTeamCount; EWaterContent::EWaterContent waterContent; EMonsterStrength::EMonsterStrength monsterStrength; std::map players; + std::map savedPlayerSettings; std::set enabledRoads; + bool customizedPlayers; const CRmgTemplate * mapTemplate; public: template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & width; h & height; h & hasTwoLevels; - h & playerCount; + h & humanOrCpuPlayerCount; h & teamCount; h & compOnlyPlayerCount; h & compOnlyTeamCount; @@ -177,16 +202,13 @@ public: { templateName = mapTemplate->getId(); } - if(version >= 806) + h & templateName; + if(!h.saving) { - h & templateName; - if(!h.saving) - { - setMapTemplate(templateName); - } - - h & enabledRoads; + setMapTemplate(templateName); } + + h & enabledRoads; } }; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 4a3e919d8..02543f967 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -17,6 +17,7 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapping/CMapEditManager.h" +#include "../CArtHandler.h" #include "../CTownHandler.h" #include "../CHeroHandler.h" #include "../constants/StringConstants.h" @@ -33,14 +34,14 @@ VCMI_LIB_NAMESPACE_BEGIN -CMapGenerator::CMapGenerator(CMapGenOptions& mapGenOptions, int RandomSeed) : +CMapGenerator::CMapGenerator(CMapGenOptions& mapGenOptions, IGameCallback * cb, int RandomSeed) : mapGenOptions(mapGenOptions), randomSeed(RandomSeed), - allowedPrisons(0), monolithIndex(0) + monolithIndex(0) { loadConfig(); rand.setSeed(this->randomSeed); mapGenOptions.finalize(rand); - map = std::make_unique(mapGenOptions); + map = std::make_unique(mapGenOptions, cb); placer = std::make_shared(*map); } @@ -96,17 +97,6 @@ const CMapGenOptions& CMapGenerator::getMapGenOptions() const return mapGenOptions; } -void CMapGenerator::initPrisonsRemaining() -{ - allowedPrisons = 0; - for (auto isAllowed : map->getMap(this).allowedHeroes) - { - if (isAllowed) - allowedPrisons++; - } - allowedPrisons = std::max (0, allowedPrisons - 16 * mapGenOptions.getPlayerCount()); //so at least 16 heroes will be available for every player -} - void CMapGenerator::initQuestArtsRemaining() { //TODO: Move to QuestArtifactPlacer? @@ -127,7 +117,6 @@ std::unique_ptr CMapGenerator::generate() addHeaderInfo(); map->initTiles(*this, rand); Load::Progress::step(); - initPrisonsRemaining(); initQuestArtsRemaining(); genZones(); Load::Progress::step(); @@ -162,7 +151,7 @@ std::string CMapGenerator::getMapDescription() const std::stringstream ss; ss << boost::str(boost::format(std::string("Map created by the Random Map Generator.\nTemplate was %s, size %dx%d") + ", levels %d, players %d, computers %d, water %s, monster %s, VCMI map") % mapTemplate->getName() % - map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getPlayerCount()) % + map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getHumanOrCpuPlayerCount()) % static_cast(mapGenOptions.getCompOnlyPlayerCount()) % waterContentStr[mapGenOptions.getWaterContent()] % monsterStrengthStr[monsterStrengthIndex]); @@ -185,84 +174,120 @@ std::string CMapGenerator::getMapDescription() const void CMapGenerator::addPlayerInfo() { - // Calculate which team numbers exist + // Teams are already configured in CMapGenOptions. However, it's not the case when it comes to map editor - enum ETeams {CPHUMAN = 0, CPUONLY = 1, AFTER_LAST = 2}; // Used as a kind of a local named array index, so left as enum, not enum class - std::array, 2> teamNumbers; - std::set teamsTotal; + std::set teamsTotal; - int teamOffset = 0; - int playerCount = 0; - int teamCount = 0; - - for (int i = CPHUMAN; i < AFTER_LAST; ++i) + if (mapGenOptions.arePlayersCustomized()) { - if (i == CPHUMAN) - { - playerCount = mapGenOptions.getPlayerCount(); - teamCount = mapGenOptions.getTeamCount(); - } - else - { - playerCount = mapGenOptions.getCompOnlyPlayerCount(); - teamCount = mapGenOptions.getCompOnlyTeamCount(); - } + // Simply copy existing settings set in GUI - if(playerCount == 0) + for (const auto & player : mapGenOptions.getPlayersSettings()) { - continue; + PlayerInfo playerInfo; + playerInfo.team = player.second.getTeam(); + if (player.second.getPlayerType() == EPlayerType::COMP_ONLY) + { + playerInfo.canHumanPlay = false; + } + else + { + playerInfo.canHumanPlay = true; + } + map->getMap(this).players[player.first.getNum()] = playerInfo; + teamsTotal.insert(player.second.getTeam()); } - int playersPerTeam = playerCount / (teamCount == 0 ? playerCount : teamCount); - int teamCountNorm = teamCount; - if(teamCountNorm == 0) + } + else + { + // Assign standard teams (in map editor) + + // Calculate which team numbers exist + + enum ETeams {CPHUMAN = 0, CPUONLY = 1, AFTER_LAST = 2}; // Used as a kind of a local named array index, so left as enum, not enum class + std::array, 2> teamNumbers; + + int teamOffset = 0; + int playerCount = 0; + int teamCount = 0; + + // FIXME: Player can be any color, not just 0 + for (int i = CPHUMAN; i < AFTER_LAST; ++i) { - teamCountNorm = playerCount; - } - for(int j = 0; j < teamCountNorm; ++j) - { - for(int k = 0; k < playersPerTeam; ++k) + if (i == CPHUMAN) + { + playerCount = mapGenOptions.getHumanOrCpuPlayerCount(); + teamCount = mapGenOptions.getTeamCount(); + } + else + { + playerCount = mapGenOptions.getCompOnlyPlayerCount(); + teamCount = mapGenOptions.getCompOnlyTeamCount(); + } + + if(playerCount == 0) + { + continue; + } + int playersPerTeam = playerCount / (teamCount == 0 ? playerCount : teamCount); + int teamCountNorm = teamCount; + if(teamCountNorm == 0) + { + teamCountNorm = playerCount; + } + for(int j = 0; j < teamCountNorm; ++j) + { + for(int k = 0; k < playersPerTeam; ++k) + { + teamNumbers[i].push_back(j + teamOffset); + } + } + for(int j = 0; j < playerCount - teamCountNorm * playersPerTeam; ++j) { teamNumbers[i].push_back(j + teamOffset); } + teamOffset += teamCountNorm; } - for(int j = 0; j < playerCount - teamCountNorm * playersPerTeam; ++j) - { - teamNumbers[i].push_back(j + teamOffset); - } - teamOffset += teamCountNorm; - } + logGlobal->info("Current player settings size: %d", mapGenOptions.getPlayersSettings().size()); - // Team numbers are assigned randomly to every player - //TODO: allow customize teams in rmg template - for(const auto & pair : mapGenOptions.getPlayersSettings()) - { - const auto & pSettings = pair.second; - PlayerInfo player; - player.canComputerPlay = true; - int j = (pSettings.getPlayerType() == EPlayerType::COMP_ONLY) ? CPUONLY : CPHUMAN; - if (j == CPHUMAN) + // Team numbers are assigned randomly to every player + //TODO: allow to customize teams in rmg template + for(const auto & pair : mapGenOptions.getPlayersSettings()) { - player.canHumanPlay = true; - } - - if(pSettings.getTeam() != TeamID::NO_TEAM) - { - player.team = pSettings.getTeam(); - } - else - { - if (teamNumbers[j].empty()) + const auto & pSettings = pair.second; + PlayerInfo player; + player.canComputerPlay = true; + int j = (pSettings.getPlayerType() == EPlayerType::COMP_ONLY) ? CPUONLY : CPHUMAN; + if (j == CPHUMAN) { - logGlobal->error("Not enough places in team for %s player", ((j == CPUONLY) ? "CPU" : "CPU or human")); - assert (teamNumbers[j].size()); + player.canHumanPlay = true; } - auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], rand); - player.team = TeamID(*itTeam); - teamNumbers[j].erase(itTeam); + + if(pSettings.getTeam() != TeamID::NO_TEAM) + { + player.team = pSettings.getTeam(); + } + else + { + if (teamNumbers[j].empty()) + { + logGlobal->error("Not enough places in team for %s player", ((j == CPUONLY) ? "CPU" : "CPU or human")); + assert (teamNumbers[j].size()); + } + auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], rand); + player.team = TeamID(*itTeam); + teamNumbers[j].erase(itTeam); + } + teamsTotal.insert(player.team); + map->getMap(this).players[pSettings.getColor().getNum()] = player; } - teamsTotal.insert(player.team.getNum()); - map->getMap(this).players[pSettings.getColor().getNum()] = player; + + logGlobal->info("Current team count: %d", teamsTotal.size()); + } + // FIXME: 0 + // Can't find info for player 0 (starting zone) + // Can't find info for player 1 (starting zone) map->getMap(this).howManyTeams = teamsTotal.size(); } @@ -409,7 +434,7 @@ void CMapGenerator::addHeaderInfo() m.twoLevel = mapGenOptions.getHasTwoLevels(); m.name.appendLocalString(EMetaText::GENERAL_TXT, 740); m.description.appendRawString(getMapDescription()); - m.difficulty = 1; + m.difficulty = EMapDifficulty::NORMAL; addPlayerInfo(); m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE); m.banWaterContent(); @@ -437,11 +462,6 @@ int CMapGenerator::getNextMonlithIndex() } } -int CMapGenerator::getPrisonsRemaning() const -{ - return allowedPrisons; -} - std::shared_ptr CMapGenerator::getZonePlacer() const { return placer; @@ -457,34 +477,42 @@ const std::vector CMapGenerator::getAllPossibleHeroes() const auto isWaterMap = map->getMap(this).isWaterMap(); //Skip heroes that were banned, including the ones placed in prisons std::vector ret; - for (int j = 0; j < map->getMap(this).allowedHeroes.size(); j++) + + for (HeroTypeID hero : map->getMap(this).allowedHeroes) { - if (map->getMap(this).allowedHeroes[j]) + auto * h = dynamic_cast(VLC->heroTypes()->getById(hero)); + if(h->onlyOnWaterMap && !isWaterMap) + continue; + + if(h->onlyOnMapWithoutWater && isWaterMap) + continue; + + bool heroUsedAsStarting = false; + for (auto const & player : map->getMapGenOptions().getPlayersSettings()) { - auto * h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); - if ((h->onlyOnWaterMap && !isWaterMap) || (h->onlyOnMapWithoutWater && isWaterMap)) + if (player.second.getStartingHero() == hero) { - continue; - } - else - { - ret.push_back(HeroTypeID(j)); + heroUsedAsStarting = true; + break; } } + + if (heroUsedAsStarting) + continue; + + ret.push_back(hero); } return ret; } void CMapGenerator::banQuestArt(const ArtifactID & id) { - //TODO: Protect with mutex - map->getMap(this).allowedArtifact[id] = false; + map->getMap(this).allowedArtifact.erase(id); } -void CMapGenerator::banHero(const HeroTypeID & id) +void CMapGenerator::unbanQuestArt(const ArtifactID & id) { - //TODO: Protect with mutex - map->getMap(this).banHero(id); + map->getMap(this).allowedArtifact.insert(id); } Zone * CMapGenerator::getZoneWater() const diff --git a/lib/rmg/CMapGenerator.h b/lib/rmg/CMapGenerator.h index dd18108c5..e75d9fe3a 100644 --- a/lib/rmg/CMapGenerator.h +++ b/lib/rmg/CMapGenerator.h @@ -26,6 +26,7 @@ class RmgMap; class CMap; class Zone; class CZonePlacer; +class IGameCallback; using JsonVector = std::vector; @@ -42,15 +43,21 @@ public: std::string defaultRoadType; std::string secondaryRoadType; int treasureValueLimit; - std::vector prisonExperience, prisonValues; + std::vector prisonExperience; + std::vector prisonValues; std::vector scrollValues; - int pandoraMultiplierGold, pandoraMultiplierExperience, pandoraMultiplierSpells, pandoraSpellSchool, pandoraSpell60; + int pandoraMultiplierGold; + int pandoraMultiplierExperience; + int pandoraMultiplierSpells; + int pandoraSpellSchool; + int pandoraSpell60; std::vector pandoraCreatureValues; - std::vector questValues, questRewardValues; + std::vector questValues; + std::vector questRewardValues; bool singleThread; }; - explicit CMapGenerator(CMapGenOptions& mapGenOptions, int RandomSeed = std::time(nullptr)); + explicit CMapGenerator(CMapGenOptions& mapGenOptions, IGameCallback * cb, int RandomSeed); ~CMapGenerator(); // required due to std::unique_ptr const Config & getConfig() const; @@ -65,8 +72,7 @@ public: const std::vector & getAllPossibleQuestArtifacts() const; const std::vector getAllPossibleHeroes() const; void banQuestArt(const ArtifactID & id); - void banHero(const HeroTypeID& id); - + void unbanQuestArt(const ArtifactID & id); Zone * getZoneWater() const; void addWaterTreasuresInfo(); @@ -82,7 +88,6 @@ private: std::vector connectionsLeft; - int allowedPrisons; int monolithIndex; std::vector questArtifacts; diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 3d96df035..75467694f 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -188,13 +188,7 @@ std::set ZoneOptions::getDefaultTerrainTypes() const std::set ZoneOptions::getDefaultTownTypes() const { - std::set defaultTowns; - auto towns = VLC->townh->getDefaultAllowed(); - for(int i = 0; i < towns.size(); ++i) - { - if(towns[i]) defaultTowns.insert(FactionID(i)); - } - return defaultTowns; + return VLC->townh->getDefaultAllowed(); } const std::set ZoneOptions::getTownTypes() const @@ -547,6 +541,11 @@ const std::string & CRmgTemplate::getName() const return name; } +const std::string & CRmgTemplate::getDescription() const +{ + return description; +} + const std::string & CRmgTemplate::getId() const { return id; @@ -557,9 +556,9 @@ const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getPlayers() const return players; } -const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getCpuPlayers() const +const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getHumanPlayers() const { - return cpuPlayers; + return humanPlayers; } const CRmgTemplate::Zones & CRmgTemplate::getZones() const @@ -675,13 +674,24 @@ void CRmgTemplate::CPlayerCountRange::fromString(const std::string & value) } } +int CRmgTemplate::CPlayerCountRange::maxValue() const +{ + return *boost::max_element(getNumbers()); +} + +int CRmgTemplate::CPlayerCountRange::minValue() const +{ + return *boost::min_element(getNumbers()); +} + void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("name", name); + handler.serializeString("description", description); serializeSize(handler, minSize, "minSize"); serializeSize(handler, maxSize, "maxSize"); serializePlayers(handler, players, "players"); - serializePlayers(handler, cpuPlayers, "cpu"); + serializePlayers(handler, humanPlayers, "humans"); // TODO: Rename this parameter { auto connectionsData = handler.enterArray("connections"); diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index 3f5a91197..1d0382ad1 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -231,6 +231,9 @@ public: std::string toString() const; void fromString(const std::string & value); + int maxValue() const; + int minValue() const; + private: std::vector > range; }; @@ -245,9 +248,10 @@ public: void setName(const std::string & value); const std::string & getId() const; const std::string & getName() const; + const std::string & getDescription() const; const CPlayerCountRange & getPlayers() const; - const CPlayerCountRange & getCpuPlayers() const; + const CPlayerCountRange & getHumanPlayers() const; std::pair getMapSizes() const; const Zones & getZones() const; const std::vector & getConnectedZoneIds() const; @@ -260,8 +264,11 @@ public: private: std::string id; std::string name; - int3 minSize, maxSize; - CPlayerCountRange players, cpuPlayers; + std::string description; + int3 minSize; + int3 maxSize; + CPlayerCountRange players; + CPlayerCountRange humanPlayers; Zones zones; std::vector connectedZoneIds; std::set allowedWaterContent; diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index 738d65c1a..c340219fd 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -51,12 +51,6 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const } } -std::vector CRmgTemplateStorage::getDefaultAllowed() const -{ - //all templates are allowed - return std::vector(); -} - std::vector CRmgTemplateStorage::loadLegacyData() { return std::vector(); diff --git a/lib/rmg/CRmgTemplateStorage.h b/lib/rmg/CRmgTemplateStorage.h index 6ce5c1411..65b0c0f09 100644 --- a/lib/rmg/CRmgTemplateStorage.h +++ b/lib/rmg/CRmgTemplateStorage.h @@ -24,12 +24,11 @@ class DLL_LINKAGE CRmgTemplateStorage : public IHandlerBase public: CRmgTemplateStorage() = default; - std::vector getDefaultAllowed() const override; std::vector loadLegacyData() override; /// loads single object into game. Scope is namespace of this object, same as name of source mod - virtual void loadObject(std::string scope, std::string name, const JsonNode & data) override; - virtual void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; + void loadObject(std::string scope, std::string name, const JsonNode & data) override; + void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; void afterLoadFinalization() override; diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index 07c56688f..9765a7b4e 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -9,9 +9,10 @@ */ #include "StdInc.h" -#include -#include "../CRandomGenerator.h" #include "CZonePlacer.h" + +#include "../CRandomGenerator.h" +#include "../CTownHandler.h" #include "../TerrainHandler.h" #include "../mapping/CMap.h" #include "../mapping/CMapEditManager.h" @@ -19,13 +20,14 @@ #include "RmgMap.h" #include "Zone.h" #include "Functions.h" +#include "PenroseTiling.h" VCMI_LIB_NAMESPACE_BEGIN class CRandomGenerator; CZonePlacer::CZonePlacer(RmgMap & map) - : width(0), height(0), scaleX(0), scaleY(0), mapSize(0), + : width(0), height(0), mapSize(0), gravityConstant(1e-3f), stiffnessConstant(3e-3f), stifness(0), @@ -108,7 +110,8 @@ void CZonePlacer::placeOnGrid(CRandomGenerator* rand) //Place first zone auto firstZone = zonesVector[0].second; - size_t x = 0, y = 0; + size_t x = 0; + size_t y = 0; auto getRandomEdge = [rand, gridSize](size_t& x, size_t& y) { @@ -401,7 +404,7 @@ void CZonePlacer::placeZones(CRandomGenerator * rand) //TODO: Don't do this is fitness was improved moveOneZone(zones, totalForces, distances, overlaps); - improved |= evaluateSolution();; + improved |= evaluateSolution(); } if (!improved) @@ -442,11 +445,15 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const { auto player = PlayerColor(*owner - 1); auto playerSettings = map.getMapGenOptions().getPlayersSettings(); - si32 faction = FactionID::RANDOM; + FactionID faction = FactionID::RANDOM; if (vstd::contains(playerSettings, player)) + { faction = playerSettings[player].getStartingTown(); + } else - logGlobal->error("Can't find info for player %d (starting zone)", player.getNum()); + { + logGlobal->trace("Player %d (starting zone %d) does not participate in game", player.getNum(), zone.first); + } if (faction == FactionID::RANDOM) //TODO: check this after a town has already been randomized zonesToPlace.push_back(zone); @@ -518,7 +525,7 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const std::vector prescaler = { 0, 0 }; for (int i = 0; i < 2; i++) - prescaler[i] = std::sqrt((width * height) / (totalSize[i] * 3.14f)); + prescaler[i] = std::sqrt((width * height) / (totalSize[i] * PI_CONSTANT)); mapSize = static_cast(sqrt(width * height)); for(const auto & zone : zones) { @@ -796,18 +803,8 @@ void CZonePlacer::moveOneZone(TZoneMap& zones, TForceVector& totalForces, TDista float CZonePlacer::metric (const int3 &A, const int3 &B) const { - float dx = abs(A.x - B.x) * scaleX; - float dy = abs(A.y - B.y) * scaleY; + return A.dist2dSQ(B); - /* - 1. Normal euclidean distance - 2. Sinus for extra curves - 3. Nonlinear mess for fuzzy edges - */ - - return dx * dx + dy * dy + - 5 * std::sin(dx * dy / 10) + - 25 * std::sin (std::sqrt(A.x * B.x) * (A.y - B.y) / 100 * (scaleX * scaleY)); } void CZonePlacer::assignZones(CRandomGenerator * rand) @@ -817,9 +814,6 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) auto width = map.getMapGenOptions().getWidth(); auto height = map.getMapGenOptions().getHeight(); - //scale to Medium map to ensure smooth results - scaleX = 72.f / width; - scaleY = 72.f / height; auto zones = map.getZones(); vstd::erase_if(zones, [](const std::pair> & pr) @@ -839,7 +833,13 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) return lhs.second / lhs.first->getSize() < rhs.second / rhs.first->getSize(); }; - auto moveZoneToCenterOfMass = [](const std::shared_ptr & zone) -> void + auto simpleCompareByDistance = [](const Dpair & lhs, const Dpair & rhs) -> bool + { + //bigger zones have smaller distance + return lhs.second < rhs.second; + }; + + auto moveZoneToCenterOfMass = [width, height](const std::shared_ptr & zone) -> void { int3 total(0, 0, 0); auto tiles = zone->area().getTiles(); @@ -849,17 +849,17 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) } int size = static_cast(tiles.size()); assert(size); - zone->setPos(int3(total.x / size, total.y / size, total.z / size)); + auto newPos = int3(total.x / size, total.y / size, total.z / size); + zone->setPos(newPos); + zone->setCenter(float3(float(newPos.x) / width, float(newPos.y) / height, newPos.z)); }; int levels = map.levels(); - /* - 1. Create Voronoi diagram - 2. find current center of mass for each zone. Move zone to that center to balance zones sizes - */ + // Find current center of mass for each zone. Move zone to that center to balance zones sizes int3 pos; + for(pos.z = 0; pos.z < levels; pos.z++) { for(pos.x = 0; pos.x < width; pos.x++) @@ -887,11 +887,28 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) moveZoneToCenterOfMass(zone.second); } - //assign actual tiles to each zone using nonlinear norm for fine edges - for(const auto & zone : zones) zone.second->clearTiles(); //now populate them again + // Assign zones to closest Penrose vertex + PenroseTiling penrose; + auto vertices = penrose.generatePenroseTiling(zones.size() / map.levels(), rand); + + std::map, std::set> vertexMapping; + + for (const auto & vertex : vertices) + { + distances.clear(); + for(const auto & zone : zones) + { + distances.emplace_back(zone.second, zone.second->getCenter().dist2dSQ(float3(vertex.x(), vertex.y(), 0))); + } + auto closestZone = boost::min_element(distances, compareByDistance)->first; + + vertexMapping[closestZone].insert(int3(vertex.x() * width, vertex.y() * height, closestZone->getPos().z)); //Closest vertex belongs to zone + } + + //Assign actual tiles to each zone for (pos.z = 0; pos.z < levels; pos.z++) { for (pos.x = 0; pos.x < width; pos.x++) @@ -899,22 +916,32 @@ void CZonePlacer::assignZones(CRandomGenerator * rand) for (pos.y = 0; pos.y < height; pos.y++) { distances.clear(); - for(const auto & zone : zones) + for(const auto & zoneVertex : vertexMapping) { - if (zone.second->getPos().z == pos.z) - distances.emplace_back(zone.second, metric(pos, zone.second->getPos())); - else - distances.emplace_back(zone.second, std::numeric_limits::max()); + auto zone = zoneVertex.first; + for (const auto & vertex : zoneVertex.second) + { + if (zone->getCenter().z == pos.z) + distances.emplace_back(zone, metric(pos, vertex)); + else + distances.emplace_back(zone, std::numeric_limits::max()); + } } - auto zone = boost::min_element(distances, compareByDistance)->first; //closest tile belongs to zone - zone->area().add(pos); - map.setZoneID(pos, zone->getId()); + + //Tile closes to vertex belongs to zone + auto closestZone = boost::min_element(distances, simpleCompareByDistance)->first; + closestZone->area().add(pos); + map.setZoneID(pos, closestZone->getId()); } } } + //set position (town position) to center of mass of irregular zone for(const auto & zone : zones) { + if(zone.second->area().empty()) + throw rmgException("Empty zone after Penrose tiling"); + moveZoneToCenterOfMass(zone.second); //TODO: similiar for islands diff --git a/lib/rmg/CZonePlacer.h b/lib/rmg/CZonePlacer.h index 90b77b8f2..2eaf429ce 100644 --- a/lib/rmg/CZonePlacer.h +++ b/lib/rmg/CZonePlacer.h @@ -54,9 +54,7 @@ private: private: int width; int height; - //metric coeficients - float scaleX; - float scaleY; + //metric coeficient float mapSize; float gravityConstant; diff --git a/lib/rmg/Functions.h b/lib/rmg/Functions.h index 3a4885b5f..0bcaee0d7 100644 --- a/lib/rmg/Functions.h +++ b/lib/rmg/Functions.h @@ -28,12 +28,8 @@ public: explicit rmgException(const std::string& _Message) : msg(_Message) { } - - virtual ~rmgException() throw () - { - }; - - const char *what() const throw () override + + const char *what() const noexcept override { return msg.c_str(); } diff --git a/lib/rmg/PenroseTiling.cpp b/lib/rmg/PenroseTiling.cpp new file mode 100644 index 000000000..e6ee0e9f4 --- /dev/null +++ b/lib/rmg/PenroseTiling.cpp @@ -0,0 +1,165 @@ +/* + * PenroseTiling.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +// Adapted from https://github.com/mpizzzle/penrose by Michael Percival + +#include "StdInc.h" +#include "PenroseTiling.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +Point2D Point2D::operator * (float scale) const +{ + return Point2D(x() * scale, y() * scale); +} + +Point2D Point2D::operator + (const Point2D& other) const +{ + return Point2D(x() + other.x(), y() + other.y()); +} + +bool Point2D::operator < (const Point2D& other) const +{ + if (x() < other.x()) + { + return true; + } + else + { + return y() < other.y(); + } +} + +Triangle::Triangle(bool t_123, const TIndices & inds): + tiling(t_123), + indices(inds) +{} + +Triangle::~Triangle() +{ + for (auto * triangle : subTriangles) + { + if (triangle) + { + delete triangle; + triangle = nullptr; + } + } +} + +Point2D Point2D::rotated(float radians) const +{ + float cosAngle = cos(radians); + float sinAngle = sin(radians); + + // Apply rotation matrix transformation + float newX = x() * cosAngle - y() * sinAngle; + float newY = x() * sinAngle + y() * cosAngle; + + return Point2D(newX, newY); +} + +void PenroseTiling::split(Triangle& p, std::vector& points, + std::array, 5>& indices, uint32_t depth) +{ + uint32_t s = points.size(); + TIndices& i = p.indices; + + const auto p2 = P2; + + if (depth > 0) + { + if (p.tiling ^ !p2) + { + points.push_back(Point2D((points[i[0]] * (1.0f - PHI) ) + (points[i[2]]) * PHI)); + points.push_back(Point2D((points[i[p2]] * (1.0f - PHI)) + (points[i[!p2]] * PHI))); + + auto * t1 = new Triangle(p2, TIndices({ i[(!p2) + 1], p2 ? i[2] : s, p2 ? s : i[1] })); + auto * t2 = new Triangle(true, TIndices({ p2 ? i[1] : s, s + 1, p2 ? s : i[1] })); + auto * t3 = new Triangle(false, TIndices({ s, s + 1, i[0] })); + + p.subTriangles = { t1, t2, t3 }; + } + else + { + points.push_back(Point2D((points[i[p2 * 2]] * (1.0f - PHI)) + (points[i[!p2]]) * PHI)); + + auto * t1 = new Triangle(true, TIndices({ i[2], s, i[1] })); + auto * t2 = new Triangle(false, TIndices({ i[(!p2) + 1], s, i[0] })); + + p.subTriangles = { t1, t2 }; + } + + for (auto& t : p.subTriangles) + { + if (depth == 1) + { + for (uint32_t k = 0; k < 3; ++k) + { + if (k != (t->tiling ^ !p2 ? 2 : 1)) + { + indices[indices.size() - 1].push_back(t->indices[k]); + indices[indices.size() - 1].push_back(t->indices[((k + 1) % 3)]); + } + } + + indices[t->tiling + (p.tiling ? 0 : 2)].insert(indices[t->tiling + (p.tiling ? 0 : 2)].end(), t->indices.begin(), t->indices.end()); + } + + // Split recursively + split(*t, points, indices, depth - 1); + } + } + + return; +} + +std::set PenroseTiling::generatePenroseTiling(size_t numZones, CRandomGenerator * rand) +{ + float scale = 100.f / (numZones * 1.5f + 20); + float polyAngle = (2 * PI_CONSTANT) / POLY; + + float randomAngle = rand->nextDouble(0, 2 * PI_CONSTANT); + + std::vector points = { Point2D(0.0f, 0.0f), Point2D(0.0f, 1.0f).rotated(randomAngle) }; + std::array, 5> indices; + + for (uint32_t i = 1; i < POLY; ++i) + { + Point2D next = points[i].rotated(polyAngle); + points.push_back(next); + } + + for (auto& p : points) + { + p.x(p.x() * scale * BASE_SIZE); + } + + std::set finalPoints; + + for (uint32_t i = 0; i < POLY; i++) + { + std::array p = { (i % (POLY + 1)) + 1, ((i + 1) % POLY) + 1 }; + + Triangle t(true, TIndices({ 0, p[i & 1], p[!(i & 1)] })); + + split(t, points, indices, DEPTH); + } + + vstd::copy_if(points, vstd::set_inserter(finalPoints), [](const Point2D point) + { + return vstd::isbetween(point.x(), 0.f, 1.0f) && vstd::isbetween(point.y(), 0.f, 1.0f); + }); + + return finalPoints; +} + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/PenroseTiling.h b/lib/rmg/PenroseTiling.h new file mode 100644 index 000000000..bdda4c7c9 --- /dev/null +++ b/lib/rmg/PenroseTiling.h @@ -0,0 +1,71 @@ +/* + * PenroseTiling.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#pragma once + +#include "../GameConstants.h" +#include "../CRandomGenerator.h" +#include +#include + +VCMI_LIB_NAMESPACE_BEGIN + +using namespace boost::geometry; +typedef std::array TIndices; + +const float PI_CONSTANT = 3.141592f; + +class Point2D : public model::d2::point_xy +{ +public: + using point_xy::point_xy; + + Point2D operator * (float scale) const; + Point2D operator + (const Point2D& other) const; + Point2D rotated(float radians) const; + + bool operator < (const Point2D& other) const; +}; + +Point2D rotatePoint(const Point2D& point, double radians, const Point2D& origin); + +class Triangle +{ +public: + ~Triangle(); + + const bool tiling; + TIndices indices; + + std::vector subTriangles; + + Triangle(bool t_123, const TIndices & inds); +}; + +class PenroseTiling +{ + +public: + const float PHI = 1.0 / ((1.0 + std::sqrt(5.0)) / 2); + const uint32_t POLY = 10; // Number of symmetries? + + const float BASE_SIZE = 1.25f; + const uint32_t DEPTH = 7; //Recursion depth + + const bool P2 = false; // Tiling type + + std::set generatePenroseTiling(size_t numZones, CRandomGenerator * rand); + +private: + void split(Triangle& p, std::vector& points, std::array, 5>& indices, uint32_t depth); + +}; + +VCMI_LIB_NAMESPACE_END \ No newline at end of file diff --git a/lib/rmg/RmgArea.cpp b/lib/rmg/RmgArea.cpp index 1b6b6196e..05c45612d 100644 --- a/lib/rmg/RmgArea.cpp +++ b/lib/rmg/RmgArea.cpp @@ -19,12 +19,12 @@ namespace rmg void toAbsolute(Tileset & tiles, const int3 & position) { - Tileset temp; - for(const auto & tile : tiles) + std::vector vec(tiles.begin(), tiles.end()); + tiles.clear(); + std::transform(vec.begin(), vec.end(), vstd::set_inserter(tiles), [position](const int3 & tile) { - temp.insert(tile + position); - } - tiles = std::move(temp); + return tile + position; + }); } void toRelative(Tileset & tiles, const int3 & position) @@ -161,6 +161,7 @@ const Tileset & Area::getBorder() const return dBorderCache; //compute border cache + dBorderCache.reserve(dTiles.bucket_count()); for(const auto & t : dTiles) { for(auto & i : int3::getDirs()) @@ -182,6 +183,7 @@ const Tileset & Area::getBorderOutside() const return dBorderOutsideCache; //compute outside border cache + dBorderOutsideCache.reserve(dBorderCache.bucket_count() * 2); for(const auto & t : dTiles) { for(auto & i : int3::getDirs()) @@ -238,6 +240,7 @@ bool Area::contains(const Area & area) const bool Area::overlap(const std::vector & tiles) const { + // Important: Make sure that tiles.size < area.size for(const auto & t : tiles) { if(contains(t)) @@ -296,15 +299,15 @@ int3 Area::nearest(const Area & area) const Area Area::getSubarea(const std::function & filter) const { Area subset; - for(const auto & t : getTilesVector()) - if(filter(t)) - subset.add(t); + subset.dTiles.reserve(getTilesVector().size()); + vstd::copy_if(getTilesVector(), vstd::set_inserter(subset.dTiles), filter); return subset; } void Area::clear() { dTiles.clear(); + dTilesVectorCache.clear(); dTotalShiftCache = int3(); invalidate(); } @@ -329,15 +332,16 @@ void Area::erase(const int3 & tile) void Area::unite(const Area & area) { invalidate(); - for(const auto & t : area.getTilesVector()) - { - dTiles.insert(t); - } + const auto & vec = area.getTilesVector(); + dTiles.reserve(dTiles.size() + vec.size()); + dTiles.insert(vec.begin(), vec.end()); } + void Area::intersect(const Area & area) { invalidate(); Tileset result; + result.reserve(std::max(dTiles.size(), area.getTilesVector().size())); for(const auto & t : area.getTilesVector()) { if(dTiles.count(t)) @@ -359,10 +363,9 @@ void Area::translate(const int3 & shift) { dBorderCache.clear(); dBorderOutsideCache.clear(); - + if(dTilesVectorCache.empty()) { - getTiles(); getTilesVector(); } @@ -373,7 +376,6 @@ void Area::translate(const int3 & shift) { t += shift; } - //toAbsolute(dTiles, shift); } void Area::erase_if(std::function predicate) @@ -398,8 +400,12 @@ Area operator+ (const Area & l, const int3 & r) Area operator+ (const Area & l, const Area & r) { - Area result(l); - result.unite(r); + Area result; + const auto & lTiles = l.getTilesVector(); + const auto & rTiles = r.getTilesVector(); + result.dTiles.reserve(lTiles.size() + rTiles.size()); + result.dTiles.insert(lTiles.begin(), lTiles.end()); + result.dTiles.insert(rTiles.begin(), rTiles.end()); return result; } @@ -419,7 +425,7 @@ Area operator* (const Area & l, const Area & r) bool operator== (const Area & l, const Area & r) { - return l.getTiles() == r.getTiles(); + return l.getTilesVector() == r.getTilesVector(); } } diff --git a/lib/rmg/RmgArea.h b/lib/rmg/RmgArea.h index f07bcd5e0..b04d02809 100644 --- a/lib/rmg/RmgArea.h +++ b/lib/rmg/RmgArea.h @@ -20,7 +20,7 @@ namespace rmg static const std::array dirs4 = { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0) }; static const std::array dirsDiagonal= { int3(1,1,0),int3(1,-1,0),int3(-1,1,0),int3(-1,-1,0) }; - using Tileset = std::set; + using Tileset = std::unordered_set; using DistanceMap = std::map; void toAbsolute(Tileset & tiles, const int3 & position); void toRelative(Tileset & tiles, const int3 & position); diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index 85c0c8590..1e2d9ebc2 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -19,6 +19,7 @@ #include "modificators/ObjectManager.h" #include "modificators/RoadPlacer.h" #include "modificators/TreasurePlacer.h" +#include "modificators/PrisonHeroPlacer.h" #include "modificators/QuestArtifactPlacer.h" #include "modificators/ConnectionsPlacer.h" #include "modificators/TownPlacer.h" @@ -37,14 +38,19 @@ VCMI_LIB_NAMESPACE_BEGIN -RmgMap::RmgMap(const CMapGenOptions& mapGenOptions) : +RmgMap::RmgMap(const CMapGenOptions& mapGenOptions, IGameCallback * cb) : mapGenOptions(mapGenOptions), zonesTotal(0) { - mapInstance = std::make_unique(); + mapInstance = std::make_unique(cb); mapProxy = std::make_shared(*this); getEditManager()->getUndoManager().setUndoRedoLimit(0); } +int RmgMap::getDecorationsPercentage() const +{ + return 15; // arbitrary value to generate more readable map +} + void RmgMap::foreach_neighbour(const int3 & pos, const std::function & foo) const { for(const int3 &dir : int3::getDirs()) @@ -90,7 +96,7 @@ void RmgMap::initTiles(CMapGenerator & generator, CRandomGenerator & rand) getEditManager()->clearTerrain(&rand); getEditManager()->getTerrainSelection().selectRange(MapRect(int3(0, 0, 0), mapGenOptions.getWidth(), mapGenOptions.getHeight())); - getEditManager()->drawTerrain(ETerrainId::GRASS, &rand); + getEditManager()->drawTerrain(ETerrainId::GRASS, getDecorationsPercentage(), &rand); const auto * tmpl = mapGenOptions.getMapTemplate(); zones.clear(); @@ -122,6 +128,7 @@ void RmgMap::initTiles(CMapGenerator & generator, CRandomGenerator & rand) void RmgMap::addModificators() { bool hasObjectDistributor = false; + bool hasHeroPlacer = false; bool hasRockFiller = false; for(auto & z : getZones()) @@ -134,6 +141,11 @@ void RmgMap::addModificators() zone->addModificator(); hasObjectDistributor = true; } + if (!hasHeroPlacer) + { + zone->addModificator(); + hasHeroPlacer = true; + } zone->addModificator(); zone->addModificator(); zone->addModificator(); @@ -309,7 +321,7 @@ void RmgMap::setZoneID(const int3& tile, TRmgTemplateZoneId zid) zoneColouring[tile.x][tile.y][tile.z] = zid; } -void RmgMap::setNearestObjectDistance(int3 &tile, float value) +void RmgMap::setNearestObjectDistance(const int3 &tile, float value) { assertOnMap(tile); @@ -345,7 +357,7 @@ bool RmgMap::isAllowedSpell(const SpellID & sid) const assert(sid.getNum() >= 0); if (sid.getNum() < mapInstance->allowedSpells.size()) { - return mapInstance->allowedSpells[sid]; + return mapInstance->allowedSpells.count(sid); } else return false; diff --git a/lib/rmg/RmgMap.h b/lib/rmg/RmgMap.h index 11213d031..26079a9c1 100644 --- a/lib/rmg/RmgMap.h +++ b/lib/rmg/RmgMap.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CMap; class CMapEditManager; +class CRandomGenerator; class TileInfo; class CMapGenOptions; class Zone; @@ -27,11 +28,13 @@ class playerInfo; class RmgMap { public: + int getDecorationsPercentage() const; + mutable std::unique_ptr mapInstance; std::shared_ptr getMapProxy() const; CMap & getMap(const CMapGenerator *) const; //limited access - RmgMap(const CMapGenOptions& mapGenOptions); + RmgMap(const CMapGenOptions& mapGenOptions, IGameCallback * cb); ~RmgMap() = default; CMapEditManager* getEditManager() const; @@ -61,7 +64,7 @@ public: TerrainTile & getTile(const int3 & tile) const; float getNearestObjectDistance(const int3 &tile) const; - void setNearestObjectDistance(int3 &tile, float value); + void setNearestObjectDistance(const int3 &tile, float value); TRmgTemplateZoneId getZoneID(const int3& tile) const; void setZoneID(const int3& tile, TRmgTemplateZoneId zid); diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 9934602a5..7c6d8cf9e 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -17,6 +17,7 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjects/ObjectTemplate.h" +#include "../mapObjects/CGObjectInstance.h" #include "Functions.h" #include "../TerrainHandler.h" @@ -38,7 +39,8 @@ const Area & Object::Instance::getBlockedArea() const { if(dBlockedAreaCache.empty()) { - dBlockedAreaCache.assign(dObject.getBlockedPos()); + std::set blockedArea = dObject.getBlockedPos(); + dBlockedAreaCache.assign(rmg::Tileset(blockedArea.begin(), blockedArea.end())); if(dObject.isVisitable() || dBlockedAreaCache.empty()) dBlockedAreaCache.add(dObject.visitablePos()); } @@ -68,8 +70,10 @@ const rmg::Area & Object::Instance::getAccessibleArea() const if(dAccessibleAreaCache.empty()) { auto neighbours = rmg::Area({getVisitablePosition()}).getBorderOutside(); + // FIXME: Blocked area of removable object is also accessible area for neighbors rmg::Area visitable = rmg::Area(neighbours) - getBlockedArea(); - for(const auto & from : visitable.getTiles()) + // TODO: Add in one operation to avoid multiple invalidation + for(const auto & from : visitable.getTilesVector()) { if(isVisitableFrom(from)) dAccessibleAreaCache.add(from); @@ -85,9 +89,7 @@ void Object::Instance::setPosition(const int3 & position) dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } void Object::Instance::setPositionRaw(const int3 & position) @@ -97,9 +99,7 @@ void Object::Instance::setPositionRaw(const int3 & position) dObject.pos = dPosition + dParent.getPosition(); dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } auto shift = position + dParent.getPosition() - dObject.pos; @@ -113,9 +113,9 @@ void Object::Instance::setPositionRaw(const int3 & position) void Object::Instance::setAnyTemplate(CRandomGenerator & rng) { - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(); + auto templates = dObject.getObjectHandler()->getTemplates(); if(templates.empty()) - throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.subID)); + throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.getObjTypeIndex())); dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); dAccessibleAreaCache.clear(); @@ -124,11 +124,12 @@ void Object::Instance::setAnyTemplate(CRandomGenerator & rng) void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng) { - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrain); + auto templates = dObject.getObjectHandler()->getMostSpecificTemplates(terrain); + if (templates.empty()) { auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated(); - throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.subID % terrainName)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.getObjTypeIndex() % terrainName)); } dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); @@ -138,12 +139,13 @@ void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng) void Object::Instance::clear() { + if (onCleared) + onCleared(&dObject); + delete &dObject; dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } bool Object::Instance::isVisitableFrom(const int3 & position) const @@ -152,6 +154,16 @@ bool Object::Instance::isVisitableFrom(const int3 & position) const return dObject.appearance->isVisitableFrom(relPosition.x, relPosition.y); } +bool Object::Instance::isBlockedVisitable() const +{ + return dObject.isBlockedVisitable(); +} + +bool Object::Instance::isRemovable() const +{ + return dObject.isRemovable(); +} + CGObjectInstance & Object::Instance::object() { return dObject; @@ -175,7 +187,6 @@ Object::Object(CGObjectInstance & object): } Object::Object(const Object & object): - dStrength(object.dStrength), guarded(false) { for(const auto & i : object.dInstances) @@ -183,20 +194,24 @@ Object::Object(const Object & object): setPosition(object.getPosition()); } -std::list Object::instances() +std::list & Object::instances() { - std::list result; - for(auto & i : dInstances) - result.push_back(&i); - return result; + if (cachedInstanceList.empty()) + { + for(auto & i : dInstances) + cachedInstanceList.push_back(&i); + } + return cachedInstanceList; } -std::list Object::instances() const +std::list & Object::instances() const { - std::list result; - for(const auto & i : dInstances) - result.push_back(&i); - return result; + if (cachedInstanceConstList.empty()) + { + for(const auto & i : dInstances) + cachedInstanceConstList.push_back(&i); + } + return cachedInstanceConstList; } void Object::addInstance(Instance & object) @@ -204,20 +219,22 @@ void Object::addInstance(Instance & object) //assert(object.dParent == *this); setGuardedIfMonster(object); dInstances.push_back(object); + cachedInstanceList.push_back(&object); + cachedInstanceConstList.push_back(&object); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); + visibleTopOffset.reset(); } Object::Instance & Object::addInstance(CGObjectInstance & object) { dInstances.emplace_back(*this, object); setGuardedIfMonster(dInstances.back()); + cachedInstanceList.push_back(&dInstances.back()); + cachedInstanceConstList.push_back(&dInstances.back()); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); + visibleTopOffset.reset(); return dInstances.back(); } @@ -225,10 +242,11 @@ Object::Instance & Object::addInstance(CGObjectInstance & object, const int3 & p { dInstances.emplace_back(*this, object, position); setGuardedIfMonster(dInstances.back()); + cachedInstanceList.push_back(&dInstances.back()); + cachedInstanceConstList.push_back(&dInstances.back()); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); + visibleTopOffset.reset(); return dInstances.back(); } @@ -255,26 +273,89 @@ const rmg::Area & Object::getAccessibleArea(bool exceptLast) const return dAccessibleAreaCache; if(!exceptLast && !dAccessibleAreaFullCache.empty()) return dAccessibleAreaFullCache; - + + // FIXME: This clears tiles for every consecutive object for(auto i = dInstances.begin(); i != std::prev(dInstances.end()); ++i) dAccessibleAreaCache.unite(i->getAccessibleArea()); - + dAccessibleAreaFullCache = dAccessibleAreaCache; dAccessibleAreaFullCache.unite(dInstances.back().getAccessibleArea()); dAccessibleAreaCache.subtract(getArea()); dAccessibleAreaFullCache.subtract(getArea()); - + if(exceptLast) return dAccessibleAreaCache; else return dAccessibleAreaFullCache; } +const rmg::Area & Object::getBlockVisitableArea() const +{ + if(dBlockVisitableCache.empty()) + { + for(const auto & i : dInstances) + { + // FIXME: Account for blockvis objects with multiple visitable tiles + if (i.isBlockedVisitable()) + dBlockVisitableCache.add(i.getVisitablePosition()); + } + } + return dBlockVisitableCache; +} + +const rmg::Area & Object::getRemovableArea() const +{ + if(dRemovableAreaCache.empty()) + { + for(const auto & i : dInstances) + { + if (i.isRemovable()) + dRemovableAreaCache.unite(i.getBlockedArea()); + } + } + + return dRemovableAreaCache; +} + +const rmg::Area & Object::getVisitableArea() const +{ + if(dVisitableCache.empty()) + { + for(const auto & i : dInstances) + { + // FIXME: Account for bjects with multiple visitable tiles + dVisitableCache.add(i.getVisitablePosition()); + } + } + return dVisitableCache; +} + +const rmg::Area Object::getEntrableArea() const +{ + // Calculate Area that hero can freely pass + + // Do not use blockVisitTiles, unless they belong to removable objects (resources etc.) + // area = accessibleArea - (blockVisitableArea - removableArea) + + // FIXME: What does it do? AccessibleArea means area AROUND the object + rmg::Area entrableArea = getVisitableArea(); + rmg::Area blockVisitableArea = getBlockVisitableArea(); + blockVisitableArea.subtract(getRemovableArea()); + entrableArea.subtract(blockVisitableArea); + + return entrableArea; +} + void Object::setPosition(const int3 & position) { - dAccessibleAreaCache.translate(position - dPosition); - dAccessibleAreaFullCache.translate(position - dPosition); - dFullAreaCache.translate(position - dPosition); + auto shift = position - dPosition; + + dAccessibleAreaCache.translate(shift); + dAccessibleAreaFullCache.translate(shift); + dBlockVisitableCache.translate(shift); + dVisitableCache.translate(shift); + dRemovableAreaCache.translate(shift); + dFullAreaCache.translate(shift); dPosition = position; for(auto& i : dInstances) @@ -285,6 +366,8 @@ void Object::setTemplate(const TerrainId & terrain, CRandomGenerator & rng) { for(auto& i : dInstances) i.setTemplate(terrain, rng); + + visibleTopOffset.reset(); } const Area & Object::getArea() const @@ -302,15 +385,23 @@ const Area & Object::getArea() const const int3 Object::getVisibleTop() const { - int3 topTile(-1, 10000, -1); //Start at the bottom - for (const auto& i : dInstances) + if (visibleTopOffset) { - if (i.getTopTile().y < topTile.y) - { - topTile = i.getTopTile(); - } + return dPosition + visibleTopOffset.value(); + } + else + { + int3 topTile(-1, 10000, -1); //Start at the bottom + for (const auto& i : dInstances) + { + if (i.getTopTile().y < topTile.y) + { + topTile = i.getTopTile(); + } + } + visibleTopOffset = topTile - dPosition; + return topTile; } - return topTile; } bool rmg::Object::isGuarded() const @@ -335,10 +426,10 @@ void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) if (!dObject.appearance) { const auto * terrainType = map.getTile(getPosition(true)).terType; - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrainType->getId()); + auto templates = dObject.getObjectHandler()->getTemplates(terrainType->getId()); if (templates.empty()) { - throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.subID % getPosition(true).toString() % terrainType)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.getObjTypeIndex() % getPosition(true).toString() % terrainType)); } else { @@ -374,14 +465,26 @@ void Object::finalize(RmgMap & map, CRandomGenerator & rng) } } +void Object::clearCachedArea() const +{ + dFullAreaCache.clear(); + dAccessibleAreaCache.clear(); + dAccessibleAreaFullCache.clear(); + dBlockVisitableCache.clear(); + dVisitableCache.clear(); + dRemovableAreaCache.clear(); +} + void Object::clear() { for(auto & instance : dInstances) instance.clear(); dInstances.clear(); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + cachedInstanceList.clear(); + cachedInstanceConstList.clear(); + visibleTopOffset.reset(); + + clearCachedArea(); } diff --git a/lib/rmg/RmgObject.h b/lib/rmg/RmgObject.h index 2ba78a29a..3361dbbba 100644 --- a/lib/rmg/RmgObject.h +++ b/lib/rmg/RmgObject.h @@ -35,6 +35,8 @@ public: int3 getVisitablePosition() const; bool isVisitableFrom(const int3 & tile) const; + bool isBlockedVisitable() const; + bool isRemovable() const; const Area & getAccessibleArea() const; void setTemplate(TerrainId terrain, CRandomGenerator &); //cache invalidation void setAnyTemplate(CRandomGenerator &); //cache invalidation @@ -49,6 +51,7 @@ public: void finalize(RmgMap & map, CRandomGenerator &); //cache invalidation void clear(); + std::function onCleared; private: mutable Area dBlockedAreaCache; int3 dPosition; @@ -66,11 +69,15 @@ public: Instance & addInstance(CGObjectInstance & object); Instance & addInstance(CGObjectInstance & object, const int3 & position); - std::list instances(); - std::list instances() const; + std::list & instances(); + std::list & instances() const; int3 getVisitablePosition() const; const Area & getAccessibleArea(bool exceptLast = false) const; + const Area & getBlockVisitableArea() const; + const Area & getVisitableArea() const; + const Area & getRemovableArea() const; + const Area getEntrableArea() const; const int3 & getPosition() const; void setPosition(const int3 & position); @@ -83,14 +90,21 @@ public: void setGuardedIfMonster(const Instance & object); void finalize(RmgMap & map, CRandomGenerator &); + void clearCachedArea() const; void clear(); private: std::list dInstances; mutable Area dFullAreaCache; - mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache; + mutable Area dAccessibleAreaCache; + mutable Area dAccessibleAreaFullCache; + mutable Area dBlockVisitableCache; + mutable Area dVisitableCache; + mutable Area dRemovableAreaCache; int3 dPosition; - ui32 dStrength; + mutable std::optional visibleTopOffset; + mutable std::list cachedInstanceList; + mutable std::list cachedInstanceConstList; bool guarded; }; } diff --git a/lib/rmg/RmgPath.cpp b/lib/rmg/RmgPath.cpp index 6bd0ec6b3..134f08267 100644 --- a/lib/rmg/RmgPath.cpp +++ b/lib/rmg/RmgPath.cpp @@ -68,11 +68,12 @@ Path Path::search(const Tileset & dst, bool straight, std::function AREA_NO_FILTER = [](const int3 & t) +const std::function AREA_NO_FILTER = [](const int3 & t) { return true; }; @@ -115,6 +116,7 @@ void Zone::initFreeTiles() if(dAreaFree.empty()) { + // Fixme: This might fail fot water zone, which doesn't need to have a tile in its center of the mass dAreaPossible.erase(pos); dAreaFree.add(pos); //zone must have at least one free tile where other paths go - for instance in the center } @@ -127,10 +129,10 @@ rmg::Area & Zone::freePaths() FactionID Zone::getTownType() const { - return FactionID(townType); + return townType; } -void Zone::setTownType(si32 town) +void Zone::setTownType(FactionID town) { townType = town; } @@ -177,6 +179,38 @@ rmg::Path Zone::searchPath(const rmg::Area & src, bool onlyStraight, const std:: return resultPath; } +rmg::Path Zone::searchPath(const rmg::Area & src, bool onlyStraight, const rmg::Area & searchArea) const +///connect current tile to any other free tile within searchArea +{ + auto movementCost = [this](const int3 & s, const int3 & d) + { + if(map.isFree(d)) + return 1; + else if (map.isPossible(d)) + return 2; + return 3; + }; + + rmg::Path freePath(searchArea); + rmg::Path resultPath(searchArea); + freePath.connect(dAreaFree); + + //connect to all pieces + auto goals = connectedAreas(src, onlyStraight); + for(auto & goal : goals) + { + auto path = freePath.search(goal, onlyStraight, movementCost); + if(path.getPathArea().empty()) + return rmg::Path::invalid(); + + freePath.connect(path.getPathArea()); + resultPath.connect(path.getPathArea()); + } + + return resultPath; +} + + rmg::Path Zone::searchPath(const int3 & src, bool onlyStraight, const std::function & areafilter) const ///connect current tile to any other free tile within zone { @@ -204,33 +238,47 @@ void Zone::fractalize() rmg::Area tilesToIgnore; //will be erased in this iteration //Squared - float minDistance = 10 * 10; + float minDistance = 9 * 9; + float freeDistance = pos.z ? (10 * 10) : 6 * 6; float spanFactor = (pos.z ? 0.25 : 0.5f); //Narrower passages in the Underground + float marginFactor = 1.0f; int treasureValue = 0; int treasureDensity = 0; - for (auto t : treasureInfo) + for (const auto & t : treasureInfo) { treasureValue += ((t.min + t.max) / 2) * t.density / 1000.f; //Thousands treasureDensity += t.density; } - if (treasureValue > 200) + if (getType() == ETemplateZoneType::WATER) { - //Less obstacles - max span is 1 (no obstacles) - spanFactor = 1.0f - ((std::max(0, (1000 - treasureValue)) / (1000.f - 200)) * (1 - spanFactor)); + // Set very little obstacles on water + spanFactor = 0.2; } - else if (treasureValue < 100) + else //Scale with treasure density { - //Dense obstacles - spanFactor *= (treasureValue / 100.f); - vstd::amax(spanFactor, 0.2f); + if (treasureValue > 400) + { + // A quater at max density + marginFactor = (0.25f + ((std::max(0, (600 - treasureValue))) / (600.f - 400)) * 0.75f); + } + else if (treasureValue < 125) + { + //Dense obstacles + spanFactor *= (treasureValue / 125.f); + vstd::amax(spanFactor, 0.15f); + } + if (treasureDensity <= 10) + { + vstd::amin(spanFactor, 0.1f + 0.01f * treasureDensity); //Add extra obstacles to fill up space } - if (treasureDensity <= 10) - { - vstd::amin(spanFactor, 0.25f); //Add extra obstacles to fill up space } + float blockDistance = minDistance * spanFactor; //More obstacles in the Underground + freeDistance = freeDistance * marginFactor; + vstd::amax(freeDistance, 4 * 4); + logGlobal->info("Zone %d: treasureValue %d blockDistance: %2.f, freeDistance: %2.f", getId(), treasureValue, blockDistance, freeDistance); if(type != ETemplateZoneType::JUNCTION) { @@ -240,6 +288,16 @@ void Zone::fractalize() { //link tiles in random order std::vector tilesToMakePath = possibleTiles.getTilesVector(); + + // Do not fractalize tiles near the edge of the map to avoid paths adjacent to map edge + const auto h = map.height(); + const auto w = map.width(); + const size_t MARGIN = 3; + vstd::erase_if(tilesToMakePath, [&, h, w](const int3 & tile) + { + return tile.x < MARGIN || tile.x > (w - MARGIN) || + tile.y < MARGIN || tile.y > (h - MARGIN); + }); RandomGeneratorUtil::randomShuffle(tilesToMakePath, getRand()); int3 nodeFound(-1, -1, -1); @@ -248,7 +306,7 @@ void Zone::fractalize() { //find closest free tile int3 closestTile = clearedTiles.nearest(tileToMakePath); - if(closestTile.dist2dSQ(tileToMakePath) <= minDistance) + if(closestTile.dist2dSQ(tileToMakePath) <= freeDistance) tilesToIgnore.add(tileToMakePath); else { @@ -265,6 +323,16 @@ void Zone::fractalize() tilesToIgnore.clear(); } } + else + { + // Handle special case - place Monoliths at the edge of a zone + auto objectManager = getModificator(); + if (objectManager) + { + objectManager->createMonoliths(); + } + } + Lock lock(areaMutex); //cut straight paths towards the center. A* is too slow for that. auto areas = connectedAreas(clearedTiles, false); diff --git a/lib/rmg/Zone.h b/lib/rmg/Zone.h index 906605832..0d2376ad8 100644 --- a/lib/rmg/Zone.h +++ b/lib/rmg/Zone.h @@ -30,7 +30,7 @@ class CMapGenerator; class Modificator; class CRandomGenerator; -extern std::function AREA_NO_FILTER; +extern const std::function AREA_NO_FILTER; typedef std::list> TModificators; @@ -59,13 +59,14 @@ public: void fractalize(); FactionID getTownType() const; - void setTownType(si32 town); + void setTownType(FactionID town); TerrainId getTerrainType() const; void setTerrainType(TerrainId terrain); void connectPath(const rmg::Path & path); rmg::Path searchPath(const rmg::Area & src, bool onlyStraight, const std::function & areafilter = AREA_NO_FILTER) const; rmg::Path searchPath(const int3 & src, bool onlyStraight, const std::function & areafilter = AREA_NO_FILTER) const; + rmg::Path searchPath(const rmg::Area & src, bool onlyStraight, const rmg::Area & searchArea) const; TModificators getModificators(); @@ -108,7 +109,7 @@ protected: std::vector possibleQuestArtifactPos; //template info - si32 townType; + FactionID townType; TerrainId terrainType; }; diff --git a/lib/rmg/float3.h b/lib/rmg/float3.h index 538e6be77..ca8775c3a 100644 --- a/lib/rmg/float3.h +++ b/lib/rmg/float3.h @@ -16,7 +16,8 @@ VCMI_LIB_NAMESPACE_BEGIN class float3 { public: - float x, y; + float x; + float y; si32 z; float3() : x(0), y(0), z(0) {} @@ -112,27 +113,6 @@ public: return *this; } - bool operator==(const float3 & i) const { return (x == i.x) && (y == i.y) && (z == i.z); } - bool operator!=(const float3 & i) const { return (x != i.x) || (y != i.y) || (z != i.z); } - - bool operator<(const float3 & i) const - { - if (zi.z) - return false; - if (yi.y) - return false; - if (xi.x) - return false; - - return false; - } - std::string toString() const { return "(" + std::to_string(x) + diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index 44b1e11d2..89689c932 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -177,6 +177,21 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con int3 potentialPos = zone.areaPossible().nearest(borderPos); assert(borderPos != potentialPos); + //Check if guard pos doesn't touch any 3rd zone. This would create unwanted passage to 3rd zone + bool adjacentZone = false; + map.foreach_neighbour(potentialPos, [this, &adjacentZone, otherZoneId](int3 & pos) + { + auto zoneId = map.getZoneID(pos); + if (zoneId != zone.getId() && zoneId != otherZoneId) + { + adjacentZone = true; + } + }); + if (adjacentZone) + { + continue; + } + //Take into account distance to objects from both sides float dist = std::min(map.getTileInfo(potentialPos).getNearestObjectDistance(), map.getTileInfo(borderPos).getNearestObjectDistance()); @@ -302,6 +317,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c if(zone.isUnderground() != otherZone->isUnderground()) { int3 zShift(0, 0, zone.getPos().z - otherZone->getPos().z); + + std::scoped_lock doubleLock(zone.areaMutex, otherZone->areaMutex); auto commonArea = zone.areaPossible() * (otherZone->areaPossible() + zShift); if(!commonArea.empty()) { @@ -312,8 +329,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c auto & managerOther = *otherZone->getModificator(); auto factory = VLC->objtypeh->getHandlerFor(Obj::SUBTERRANEAN_GATE, 0); - auto * gate1 = factory->create(); - auto * gate2 = factory->create(); + auto * gate1 = factory->create(map.mapInstance->cb, nullptr); + auto * gate2 = factory->create(map.mapInstance->cb, nullptr); rmg::Object rmgGate1(*gate1); rmg::Object rmgGate2(*gate2); rmgGate1.setTemplate(zone.getTerrainType(), zone.getRand()); @@ -322,7 +339,6 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true); int minDist = 3; - std::scoped_lock doubleLock(zone.areaMutex, otherZone->areaMutex); rmg::Path path2(otherZone->area()); rmg::Path path1 = manager.placeAndConnectObject(commonArea, rmgGate1, [this, minDist, &path2, &rmgGate1, &zShift, guarded2, &managerOther, &rmgGate2 ](const int3 & tile) { @@ -334,16 +350,12 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c return -1.f; //This could fail is accessibleArea is below the map - rmg::Area toPlace(rmgGate1.getArea() + rmgGate1.getAccessibleArea()); - auto inTheMap = toPlace.getTilesVector(); - toPlace.clear(); - for (const int3& tile : inTheMap) + rmg::Area toPlace(rmgGate1.getArea()); + toPlace.unite(toPlace.getBorderOutside()); // Add a bit of extra space around + toPlace.erase_if([this](const int3 & tile) { - if (map.isOnMap(tile)) - { - toPlace.add(tile); - } - } + return !map.isOnMap(tile); + }); toPlace.translate(-zShift); path2 = managerOther.placeAndConnectObject(toPlace, rmgGate2, minDist, guarded2, true, ObjectManager::OptimizeType::NONE); @@ -371,8 +383,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c if(!success) { auto factory = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, generator.getNextMonlithIndex()); - auto * teleport1 = factory->create(); - auto * teleport2 = factory->create(); + auto * teleport1 = factory->create(map.mapInstance->cb, nullptr); + auto * teleport2 = factory->create(map.mapInstance->cb, nullptr); RequiredObjectInfo obj1(teleport1, connection.getGuardStrength(), allowRoad); RequiredObjectInfo obj2(teleport2, connection.getGuardStrength(), allowRoad); diff --git a/lib/rmg/modificators/ConnectionsPlacer.h b/lib/rmg/modificators/ConnectionsPlacer.h index 744a91b3c..62fec5225 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.h +++ b/lib/rmg/modificators/ConnectionsPlacer.h @@ -35,7 +35,8 @@ protected: void collectNeighbourZones(); protected: - std::vector dConnections, dCompleted; + std::vector dConnections; + std::vector dCompleted; std::map dNeighbourZones; }; diff --git a/lib/rmg/modificators/MinePlacer.cpp b/lib/rmg/modificators/MinePlacer.cpp index 880e0db9a..b0dc77a9e 100644 --- a/lib/rmg/modificators/MinePlacer.cpp +++ b/lib/rmg/modificators/MinePlacer.cpp @@ -12,6 +12,7 @@ #include "../RmgMap.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" #include "../../mapObjectConstructors/CObjectClassesHandler.h" +#include "../../mapObjects/MiscObjects.h" #include "../../mapping/CMapEditManager.h" #include "../RmgPath.h" #include "../RmgObject.h" @@ -56,7 +57,7 @@ bool MinePlacer::placeMines(ObjectManager & manager) { auto mineHandler = VLC->objtypeh->getHandlerFor(Obj::MINE, res); const auto & rmginfo = mineHandler->getRMGInfo(); - auto * mine = dynamic_cast(mineHandler->create()); + auto * mine = dynamic_cast(mineHandler->create(map.mapInstance->cb, nullptr)); mine->producedResource = res; mine->tempOwner = PlayerColor::NEUTRAL; mine->producedQuantity = mine->defaultResProduction(); @@ -87,7 +88,7 @@ bool MinePlacer::placeMines(ObjectManager & manager) { for(int rc = zone.getRand().nextInt(1, extraRes); rc > 0; --rc) { - auto * resource = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::RESOURCE, mine->producedResource)->create()); + auto * resource = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::RESOURCE, mine->producedResource)->create(map.mapInstance->cb, nullptr)); resource->amount = CGResource::RANDOM_AMOUNT; RequiredObjectInfo roi; diff --git a/lib/rmg/modificators/Modificator.h b/lib/rmg/modificators/Modificator.h index c4db98da8..ef5be49bc 100644 --- a/lib/rmg/modificators/Modificator.h +++ b/lib/rmg/modificators/Modificator.h @@ -21,8 +21,8 @@ class Zone; class MapProxy; #define MODIFICATOR(x) x(Zone & z, RmgMap & m, CMapGenerator & g): Modificator(z, m, g) {setName(#x);} -#define DEPENDENCY(x) dependency(zone.getModificator()); -#define POSTFUNCTION(x) postfunction(zone.getModificator()); +#define DEPENDENCY(x) dependency(zone.getModificator()) +#define POSTFUNCTION(x) postfunction(zone.getModificator()) #define DEPENDENCY_ALL(x) for(auto & z : map.getZones()) \ { \ dependency(z.second->getModificator()); \ diff --git a/lib/rmg/modificators/ObjectDistributor.cpp b/lib/rmg/modificators/ObjectDistributor.cpp index 7ed97708e..76a0ac0a6 100644 --- a/lib/rmg/modificators/ObjectDistributor.cpp +++ b/lib/rmg/modificators/ObjectDistributor.cpp @@ -15,6 +15,7 @@ #include "../RmgMap.h" #include "../CMapGenerator.h" #include "TreasurePlacer.h" +#include "PrisonHeroPlacer.h" #include "QuestArtifactPlacer.h" #include "TownPlacer.h" #include "TerrainPainter.h" @@ -42,8 +43,6 @@ void ObjectDistributor::init() void ObjectDistributor::distributeLimitedObjects() { - //FIXME: Must be called after TerrainPainter::process() - ObjectInfo oi; auto zones = map.getZones(); @@ -77,11 +76,12 @@ void ObjectDistributor::distributeLimitedObjects() auto rmgInfo = handler->getRMGInfo(); + RandomGeneratorUtil::randomShuffle(matchingZones, zone.getRand()); for (auto& zone : matchingZones) { - oi.generateObject = [primaryID, secondaryID]() -> CGObjectInstance * + oi.generateObject = [cb=map.mapInstance->cb, primaryID, secondaryID]() -> CGObjectInstance * { - return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(); + return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(cb, nullptr); }; oi.value = rmgInfo.value; @@ -146,7 +146,18 @@ void ObjectDistributor::distributePrisons() RandomGeneratorUtil::randomShuffle(zones, zone.getRand()); - size_t allowedPrisons = generator.getPrisonsRemaning(); + // TODO: Some shorthand for unique Modificator + PrisonHeroPlacer * prisonHeroPlacer = nullptr; + for(auto & z : map.getZones()) + { + prisonHeroPlacer = z.second->getModificator(); + if (prisonHeroPlacer) + { + break; + } + } + + size_t allowedPrisons = prisonHeroPlacer->getPrisonsRemaning(); for (int i = zones.size() - 1; i >= 0; i--) { auto zone = zones[i].second; diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 9d34b3833..8dcc24c25 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -95,7 +95,7 @@ void ObjectManager::updateDistances(std::function dista { RecursiveLock lock(externalAccessMutex); tilesByDistance.clear(); - for (auto tile : zone.areaPossible().getTiles()) //don't need to mark distance for not possible tiles + for (const auto & tile : zone.areaPossible().getTilesVector()) //don't need to mark distance for not possible tiles { ui32 d = distanceFunction(tile); map.setNearestObjectDistance(tile, std::min(static_cast(d), map.getNearestObjectDistance(tile))); @@ -178,7 +178,7 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object } else { - for(const auto & tile : searchArea.getTiles()) + for(const auto & tile : searchArea.getTilesVector()) { obj.setPosition(tile); @@ -238,15 +238,14 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg RecursiveLock lock(externalAccessMutex); return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile) { - auto ti = map.getTileInfo(tile); - float dist = ti.getNearestObjectDistance(); - if(dist < min_dist) - return -1.f; - + float bestDistance = 10e9; for(const auto & t : obj.getArea().getTilesVector()) { - if(map.getTileInfo(t).getNearestObjectDistance() < min_dist) + float distance = map.getTileInfo(t).getNearestObjectDistance(); + if(distance < min_dist) return -1.f; + else + vstd::amin(bestDistance, distance); } rmg::Area perimeter; @@ -298,7 +297,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg } } - return dist; + return bestDistance; }, isGuarded, onlyStraight, optimizer); } @@ -306,6 +305,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg { int3 pos; auto possibleArea = searchArea; + auto cachedArea = zone.areaPossible() + zone.freePaths(); while(true) { pos = findPlaceForObject(possibleArea, obj, weightFunction, optimizer); @@ -314,7 +314,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg return rmg::Path::invalid(); } possibleArea.erase(pos); //do not place again at this point - auto accessibleArea = obj.getAccessibleArea(isGuarded) * (zone.areaPossible() + zone.freePaths()); + auto accessibleArea = obj.getAccessibleArea(isGuarded) * cachedArea; //we should exclude tiles which will be covered if(isGuarded) { @@ -323,21 +323,31 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg accessibleArea.add(obj.instances().back()->getPosition(true)); } - auto path = zone.searchPath(accessibleArea, onlyStraight, [&obj, isGuarded](const int3 & t) + rmg::Area subArea; + if (isGuarded) { - if(isGuarded) + const auto & guardedArea = obj.instances().back()->getAccessibleArea(); + const auto & unguardedArea = obj.getAccessibleArea(isGuarded); + subArea = cachedArea.getSubarea([guardedArea, unguardedArea, obj](const int3 & t) { - const auto & guardedArea = obj.instances().back()->getAccessibleArea(); - const auto & unguardedArea = obj.getAccessibleArea(isGuarded); if(unguardedArea.contains(t) && !guardedArea.contains(t)) return false; //guard position is always target if(obj.instances().back()->getPosition(true) == t) return true; - } - return !obj.getArea().contains(t); - }); + + return !obj.getArea().contains(t); + }); + } + else + { + subArea = cachedArea.getSubarea([obj](const int3 & t) + { + return !obj.getArea().contains(t); + }); + } + auto path = zone.searchPath(accessibleArea, onlyStraight, subArea); if(path.valid()) { @@ -346,6 +356,41 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg } } +bool ObjectManager::createMonoliths() +{ + // Special case for Junction zone only + logGlobal->trace("Creating Monoliths"); + for(const auto & objInfo : requiredObjects) + { + if (objInfo.obj->ID != Obj::MONOLITH_TWO_WAY) + { + continue; + } + + rmg::Object rmgObject(*objInfo.obj); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); + bool guarded = addGuard(rmgObject, objInfo.guardStrength, true); + + Zone::Lock lock(zone.areaMutex); + auto path = placeAndConnectObject(zone.areaPossible(), rmgObject, 3, guarded, false, OptimizeType::DISTANCE); + + if(!path.valid()) + { + logGlobal->error("Failed to fill zone %d due to lack of space", zone.getId()); + return false; + } + + zone.connectPath(path); + placeObject(rmgObject, guarded, true, objInfo.createRoad); + } + + vstd::erase_if(requiredObjects, [](const auto & objInfo) + { + return objInfo.obj->ID == Obj::MONOLITH_TWO_WAY; + }); + return true; +} + bool ObjectManager::createRequiredObjects() { logGlobal->trace("Creating required objects"); @@ -424,7 +469,8 @@ bool ObjectManager::createRequiredObjects() } rmg::Object rmgNearObject(*nearby.obj); - rmg::Area possibleArea(rmg::Area(targetObject->getBlockedPos()).getBorderOutside()); + std::set blockedArea = targetObject->getBlockedPos(); + rmg::Area possibleArea(rmg::Area(rmg::Tileset(blockedArea.begin(), blockedArea.end())).getBorderOutside()); possibleArea.intersect(zone.areaPossible()); if(possibleArea.empty()) { @@ -447,7 +493,6 @@ bool ObjectManager::createRequiredObjects() instance->object().getObjectName(), instance->getPosition(true).toString()); mapProxy->removeObject(&instance->object()); } - rmgNearObject.clear(); } } @@ -514,6 +559,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD if(map.isOnMap(i) && map.isPossible(i)) map.setOccupied(i, ETileType::BLOCKED); } + lock.unlock(); if (updateDistance) { @@ -533,30 +579,53 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD for (auto id : adjacentZones) { - auto manager = map.getZones().at(id)->getModificator(); - if (manager) + auto otherZone = map.getZones().at(id); + if ((otherZone->getType() == ETemplateZoneType::WATER) == (zone.getType() == ETemplateZoneType::WATER)) { - manager->updateDistances(object); + // Do not update other zone if only one is water + auto manager = otherZone->getModificator(); + if (manager) + { + manager->updateDistances(object); + } } } } + // TODO: Add multiple tiles in one operation to avoid multiple invalidation for(auto * instance : object.instances()) { objectsVisitableArea.add(instance->getVisitablePosition()); objects.push_back(&instance->object()); if(auto * m = zone.getModificator()) { - //FIXME: Objects that can be removed, can be trespassed. Does not include Corpse - if(instance->object().appearance->isVisitableFromTop()) - m->areaForRoads().add(instance->getVisitablePosition()); - else + if (instance->object().blockVisit && !instance->object().removable) { - m->areaIsolated().add(instance->getVisitablePosition() + int3(0, -1, 0)); + //Cannot be trespassed (Corpse) + continue; + } + else if(instance->object().appearance->isVisitableFromTop()) + { + //Passable objects + m->areaForRoads().add(instance->getVisitablePosition()); + } + else if(!instance->object().appearance->isVisitableFromTop()) + { + // Do not route road behind visitable tile + int3 visitablePos = instance->getVisitablePosition(); + auto areaVisitable = rmg::Area({visitablePos}); + auto borderAbove = areaVisitable.getBorderOutside(); + vstd::erase_if(borderAbove, [&](const int3 & tile) + { + return tile.y >= visitablePos.y || + (!instance->object().blockingAt(tile + int3(0, 1, 0)) && + instance->object().blockingAt(tile)); + }); + m->areaIsolated().unite(borderAbove); } } - switch (instance->object().ID) + switch (instance->object().ID.toEnum()) { case Obj::RANDOM_TREASURE_ART: case Obj::RANDOM_MINOR_ART: //In OH3 quest artifacts have higher value than normal arts @@ -586,7 +655,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD case Obj::MONOLITH_ONE_WAY_EXIT: */ - switch (object.instances().front()->object().ID) + switch(object.instances().front()->object().ID.toEnum()) { case Obj::WATER_WHEEL: if (auto* m = zone.getModificator()) @@ -639,19 +708,19 @@ CGCreature * ObjectManager::chooseGuard(si32 strength, bool zoneGuard) if(!possibleCreatures.empty()) { creId = *RandomGeneratorUtil::nextItem(possibleCreatures, zone.getRand()); - amount = strength / VLC->creh->objects[creId]->getAIValue(); + amount = strength / creId.toEntity(VLC)->getAIValue(); if (amount >= 4) amount = static_cast(amount * zone.getRand().nextDouble(0.75, 1.25)); } else //just pick any available creature { - creId = CreatureID(132); //Azure Dragon - amount = strength / VLC->creh->objects[creId]->getAIValue(); + creId = CreatureID::AZURE_DRAGON; //Azure Dragon + amount = strength / creId.toEntity(VLC)->getAIValue(); } auto guardFactory = VLC->objtypeh->getHandlerFor(Obj::MONSTER, creId); - auto * guard = dynamic_cast(guardFactory->create()); + auto * guard = dynamic_cast(guardFactory->create(map.mapInstance->cb, nullptr)); guard->character = CGCreature::HOSTILE; auto * hlp = new CStackInstance(creId, amount); //will be set during initialization @@ -665,11 +734,21 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard if(!guard) return false; - rmg::Area visitablePos({object.getVisitablePosition()}); - visitablePos.unite(visitablePos.getBorderOutside()); + // Prefer non-blocking tiles, if any + auto entrableArea = object.getEntrableArea(); + if (entrableArea.empty()) + { + entrableArea.add(object.getVisitablePosition()); + } + + rmg::Area entrableBorder = entrableArea.getBorderOutside(); auto accessibleArea = object.getAccessibleArea(); - accessibleArea.intersect(visitablePos); + accessibleArea.erase_if([&](const int3 & tile) + { + return !entrableBorder.contains(tile); + }); + if(accessibleArea.empty()) { delete guard; diff --git a/lib/rmg/modificators/ObjectManager.h b/lib/rmg/modificators/ObjectManager.h index f346a921a..f6c7c4eff 100644 --- a/lib/rmg/modificators/ObjectManager.h +++ b/lib/rmg/modificators/ObjectManager.h @@ -48,7 +48,8 @@ public: { NONE = 0x00000000, WEIGHT = 0x00000001, - DISTANCE = 0x00000010 + DISTANCE = 0x00000010, + BOTH = 0x00000011 }; public: @@ -61,6 +62,7 @@ public: void addCloseObject(const RequiredObjectInfo & info); void addNearbyObject(const RequiredObjectInfo & info); + bool createMonoliths(); bool createRequiredObjects(); int3 findPlaceForObject(const rmg::Area & searchArea, rmg::Object & obj, si32 min_dist, OptimizeType optimizer) const; diff --git a/lib/rmg/modificators/ObstaclePlacer.cpp b/lib/rmg/modificators/ObstaclePlacer.cpp index 26244f8da..ecab26e77 100644 --- a/lib/rmg/modificators/ObstaclePlacer.cpp +++ b/lib/rmg/modificators/ObstaclePlacer.cpp @@ -24,6 +24,7 @@ #include "../../mapping/CMapEditManager.h" #include "../../mapping/CMap.h" #include "../../mapping/ObstacleProxy.h" +#include "../../mapObjects/CGObjectInstance.h" VCMI_LIB_NAMESPACE_BEGIN @@ -51,7 +52,7 @@ void ObstaclePlacer::process() do { toBlock.clear(); - for (const auto& tile : zone.areaPossible().getTiles()) + for (const auto& tile : zone.areaPossible().getTilesVector()) { rmg::Area neighbors; rmg::Area t; @@ -76,7 +77,7 @@ void ObstaclePlacer::process() } } zone.areaPossible().subtract(toBlock); - for (const auto& tile : toBlock.getTiles()) + for (const auto& tile : toBlock.getTilesVector()) { map.setOccupied(tile, ETileType::BLOCKED); } @@ -86,7 +87,7 @@ void ObstaclePlacer::process() prohibitedArea.unite(zone.areaPossible()); } - auto objs = createObstacles(zone.getRand()); + auto objs = createObstacles(zone.getRand(), map.mapInstance->cb); mapProxy->insertObjects(objs); } diff --git a/lib/rmg/modificators/PrisonHeroPlacer.cpp b/lib/rmg/modificators/PrisonHeroPlacer.cpp new file mode 100644 index 000000000..d4787784d --- /dev/null +++ b/lib/rmg/modificators/PrisonHeroPlacer.cpp @@ -0,0 +1,73 @@ +/* +* PrisonHeroPlacer.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#include "StdInc.h" +#include "PrisonHeroPlacer.h" +#include "../CMapGenerator.h" +#include "../RmgMap.h" +#include "TreasurePlacer.h" +#include "../CZonePlacer.h" +#include "../../VCMI_Lib.h" +#include "../../mapObjectConstructors/AObjectTypeHandler.h" +#include "../../mapObjectConstructors/CObjectClassesHandler.h" +#include "../../mapObjects/MapObjects.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void PrisonHeroPlacer::process() +{ + getAllowedHeroes(); +} + +void PrisonHeroPlacer::init() +{ + // Reserve at least 16 heroes for each player + reservedHeroes = 16 * generator.getMapGenOptions().getHumanOrCpuPlayerCount(); +} + +void PrisonHeroPlacer::getAllowedHeroes() +{ + // TODO: Give each zone unique HeroPlacer with private hero list? + + // Call that only once + if (allowedHeroes.empty()) + { + allowedHeroes = generator.getAllPossibleHeroes(); + } +} + +int PrisonHeroPlacer::getPrisonsRemaning() const +{ + return std::max(allowedHeroes.size() - reservedHeroes, 0); +} + +HeroTypeID PrisonHeroPlacer::drawRandomHero() +{ + RecursiveLock lock(externalAccessMutex); + if (getPrisonsRemaning() > 0) + { + RandomGeneratorUtil::randomShuffle(allowedHeroes, zone.getRand()); + HeroTypeID ret = allowedHeroes.back(); + allowedHeroes.pop_back(); + return ret; + } + else + { + throw rmgException("No unused heroes left for prisons!"); + } +} + +void PrisonHeroPlacer::restoreDrawnHero(const HeroTypeID & hid) +{ + RecursiveLock lock(externalAccessMutex); + allowedHeroes.push_back(hid); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/PrisonHeroPlacer.h b/lib/rmg/modificators/PrisonHeroPlacer.h new file mode 100644 index 000000000..62c32d381 --- /dev/null +++ b/lib/rmg/modificators/PrisonHeroPlacer.h @@ -0,0 +1,41 @@ +/* +* PrisonHeroPlacer, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#pragma once +#include "../Zone.h" +#include "../Functions.h" +#include "../../mapObjects/ObjectTemplate.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CRandomGenerator; + +class PrisonHeroPlacer : public Modificator +{ +public: + MODIFICATOR(PrisonHeroPlacer); + + void process() override; + void init() override; + + int getPrisonsRemaning() const; + [[nodiscard]] HeroTypeID drawRandomHero(); + void restoreDrawnHero(const HeroTypeID & hid); + +private: + void getAllowedHeroes(); + size_t reservedHeroes; + +protected: + + std::vector allowedHeroes; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/QuestArtifactPlacer.cpp b/lib/rmg/modificators/QuestArtifactPlacer.cpp index fd9fe44d8..812aa1df0 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.cpp +++ b/lib/rmg/modificators/QuestArtifactPlacer.cpp @@ -40,11 +40,18 @@ void QuestArtifactPlacer::addQuestArtZone(std::shared_ptr otherZone) void QuestArtifactPlacer::addQuestArtifact(const ArtifactID& id) { + logGlobal->info("Need to place quest artifact %s", VLC->artifacts()->getById(id)->getNameTranslated()); RecursiveLock lock(externalAccessMutex); - logGlobal->info("Need to place quest artifact artifact %s", VLC->artifacts()->getById(id)->getNameTranslated()); questArtifactsToPlace.emplace_back(id); } +void QuestArtifactPlacer::removeQuestArtifact(const ArtifactID& id) +{ + logGlobal->info("Will not try to place quest artifact %s", VLC->artifacts()->getById(id)->getNameTranslated()); + RecursiveLock lock(externalAccessMutex); + vstd::erase_if_present(questArtifactsToPlace, id); +} + void QuestArtifactPlacer::rememberPotentialArtifactToReplace(CGObjectInstance* obj) { RecursiveLock lock(externalAccessMutex); @@ -92,7 +99,7 @@ void QuestArtifactPlacer::placeQuestArtifacts(CRandomGenerator & rand) //Update appearance. Terrain is irrelevant. auto handler = VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, artifactToPlace); - auto newObj = handler->create(); + auto newObj = handler->create(map.mapInstance->cb, nullptr); auto templates = handler->getTemplates(); //artifactToReplace->appearance = templates.front(); newObj->appearance = templates.front(); @@ -131,9 +138,10 @@ ArtifactID QuestArtifactPlacer::drawRandomArtifact() RecursiveLock lock(externalAccessMutex); if (!questArtifacts.empty()) { + RandomGeneratorUtil::randomShuffle(questArtifacts, zone.getRand()); ArtifactID ret = questArtifacts.back(); questArtifacts.pop_back(); - RandomGeneratorUtil::randomShuffle(questArtifacts, zone.getRand()); + generator.banQuestArt(ret); return ret; } else @@ -142,10 +150,11 @@ ArtifactID QuestArtifactPlacer::drawRandomArtifact() } } -void QuestArtifactPlacer::addRandomArtifact(ArtifactID artid) +void QuestArtifactPlacer::addRandomArtifact(const ArtifactID & artid) { RecursiveLock lock(externalAccessMutex); questArtifacts.push_back(artid); + generator.unbanQuestArt(artid); } VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/QuestArtifactPlacer.h b/lib/rmg/modificators/QuestArtifactPlacer.h index fb46c8de6..b5b9f5987 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.h +++ b/lib/rmg/modificators/QuestArtifactPlacer.h @@ -29,14 +29,15 @@ public: void findZonesForQuestArts(); void addQuestArtifact(const ArtifactID& id); + void removeQuestArtifact(const ArtifactID& id); void rememberPotentialArtifactToReplace(CGObjectInstance* obj); std::vector getPossibleArtifactsToReplace() const; void placeQuestArtifacts(CRandomGenerator & rand); void dropReplacedArtifact(CGObjectInstance* obj); size_t getMaxQuestArtifactCount() const; - ArtifactID drawRandomArtifact(); - void addRandomArtifact(ArtifactID artid); + [[nodiscard]] ArtifactID drawRandomArtifact(); + void addRandomArtifact(const ArtifactID & artid); protected: diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index d9c7af3e1..c42f24513 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -395,7 +395,7 @@ void RiverPlacer::connectRiver(const int3 & tile) { if(templ->animationFile == targetTemplateName) { - auto * obj = handler->create(templ); + auto * obj = handler->create(map.mapInstance->cb, templ); rmg::Object deltaObj(*obj, deltaPositions[pos]); deltaObj.finalize(map, zone.getRand()); } diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index aca1acfd7..4c2b2fd41 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -17,6 +17,7 @@ #include "../CMapGenerator.h" #include "../threadpool/MapProxy.h" #include "../../mapping/CMapEditManager.h" +#include "../../mapObjects/CGObjectInstance.h" #include "../../modding/IdentifierStorage.h" #include "../../modding/ModScope.h" #include "../../TerrainHandler.h" @@ -85,7 +86,7 @@ bool RoadPlacer::createRoad(const int3 & dst) { if(areaIsolated().contains(dst) || areaIsolated().contains(src)) { - return 1e30; + return 1e12; } } else @@ -121,7 +122,7 @@ void RoadPlacer::drawRoads(bool secondary) //Do not draw roads on underground rock or water roads.erase_if([this](const int3& pos) -> bool { - const auto* terrain = map.getTile(pos).terType;; + const auto* terrain = map.getTile(pos).terType; return !terrain->isPassable() || !terrain->isLand(); }); diff --git a/lib/rmg/modificators/RoadPlacer.h b/lib/rmg/modificators/RoadPlacer.h index 531b0407d..252973ff8 100644 --- a/lib/rmg/modificators/RoadPlacer.h +++ b/lib/rmg/modificators/RoadPlacer.h @@ -36,7 +36,8 @@ protected: protected: rmg::Tileset roadNodes; //tiles to be connected with roads rmg::Area roads; //all tiles with roads - rmg::Area areaRoads, isolated; + rmg::Area areaRoads; + rmg::Area isolated; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/RockPlacer.h b/lib/rmg/modificators/RockPlacer.h index e3541c2ce..8dd015d84 100644 --- a/lib/rmg/modificators/RockPlacer.h +++ b/lib/rmg/modificators/RockPlacer.h @@ -28,7 +28,8 @@ public: protected: - rmg::Area rockArea, accessibleArea; + rmg::Area rockArea; + rmg::Area accessibleArea; TerrainId rockTerrain; }; diff --git a/lib/rmg/modificators/TerrainPainter.cpp b/lib/rmg/modificators/TerrainPainter.cpp index 2a795244c..5651ff8c0 100644 --- a/lib/rmg/modificators/TerrainPainter.cpp +++ b/lib/rmg/modificators/TerrainPainter.cpp @@ -20,6 +20,7 @@ #include "../RmgMap.h" #include "../../VCMI_Lib.h" #include "../../TerrainHandler.h" +#include "../../CTownHandler.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index 76bf3bbec..743ddfdf8 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -14,6 +14,7 @@ #include "../RmgMap.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" #include "../../mapObjectConstructors/CObjectClassesHandler.h" +#include "../../mapObjects/CGTownInstance.h" #include "../../mapping/CMap.h" #include "../../mapping/CMapEditManager.h" #include "../../spells/CSpellHandler.h" //for choosing random spells @@ -71,7 +72,7 @@ void TownPlacer::placeTowns(ObjectManager & manager) auto townFactory = VLC->objtypeh->getHandlerFor(Obj::TOWN, zone.getTownType()); - CGTownInstance * town = dynamic_cast(townFactory->create()); + CGTownInstance * town = dynamic_cast(townFactory->create(map.mapInstance->cb, nullptr)); town->tempOwner = player; town->builtBuildings.insert(BuildingID::FORT); town->builtBuildings.insert(BuildingID::DEFAULT); @@ -179,7 +180,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player { for(int i = 0; i < count; i++) { - si32 subType = zone.getTownType(); + FactionID subType = zone.getTownType(); if(totalTowns>0) { @@ -193,7 +194,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player } auto townFactory = VLC->objtypeh->getHandlerFor(Obj::TOWN, subType); - auto * town = dynamic_cast(townFactory->create()); + auto * town = dynamic_cast(townFactory->create(map.mapInstance->cb, nullptr)); town->ID = Obj::TOWN; town->tempOwner = player; @@ -223,7 +224,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player } } -si32 TownPlacer::getRandomTownType(bool matchUndergroundType) +FactionID TownPlacer::getRandomTownType(bool matchUndergroundType) { auto townTypesAllowed = (!zone.getTownTypes().empty() ? zone.getTownTypes() : zone.getDefaultTownTypes()); if(matchUndergroundType) diff --git a/lib/rmg/modificators/TownPlacer.h b/lib/rmg/modificators/TownPlacer.h index a144da17d..75028093b 100644 --- a/lib/rmg/modificators/TownPlacer.h +++ b/lib/rmg/modificators/TownPlacer.h @@ -28,7 +28,7 @@ public: protected: void cleanupBoundaries(const rmg::Object & rmgObject); void addNewTowns(int count, bool hasFort, const PlayerColor & player, ObjectManager & manager); - si32 getRandomTownType(bool matchUndergroundType = false); + FactionID getRandomTownType(bool matchUndergroundType = false); void placeTowns(ObjectManager & manager); bool placeMines(ObjectManager & manager); int3 placeMainTown(ObjectManager & manager, CGTownInstance & town); diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 20dc751b2..404a1ffd8 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -18,6 +18,7 @@ #include "../RmgMap.h" #include "../TileInfo.h" #include "../CZonePlacer.h" +#include "PrisonHeroPlacer.h" #include "QuestArtifactPlacer.h" #include "../../ArtifactUtils.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" @@ -25,6 +26,8 @@ #include "../../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../mapObjects/CGPandoraBox.h" +#include "../../mapObjects/CQuest.h" +#include "../../mapObjects/MiscObjects.h" #include "../../CCreatureHandler.h" #include "../../spells/CSpellHandler.h" //for choosing random spells #include "../../mapping/CMap.h" @@ -32,6 +35,12 @@ VCMI_LIB_NAMESPACE_BEGIN +ObjectInfo::ObjectInfo(): + destroyObject([](CGObjectInstance * obj){}) +{ + +} + void TreasurePlacer::process() { addAllPossibleObjects(); @@ -45,6 +54,7 @@ void TreasurePlacer::init() maxPrisons = 0; //Should be in the constructor, but we use macro for that DEPENDENCY(ObjectManager); DEPENDENCY(ConnectionsPlacer); + DEPENDENCY_ALL(PrisonHeroPlacer); POSTFUNCTION(RoadPlacer); } @@ -72,9 +82,9 @@ void TreasurePlacer::addAllPossibleObjects() continue; } - oi.generateObject = [primaryID, secondaryID]() -> CGObjectInstance * + oi.generateObject = [this, primaryID, secondaryID]() -> CGObjectInstance * { - return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(); + return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(map.mapInstance->cb, nullptr); }; oi.value = rmgInfo.value; oi.probability = rmgInfo.rarity; @@ -90,34 +100,50 @@ void TreasurePlacer::addAllPossibleObjects() auto prisonTemplates = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0)->getTemplates(zone.getTerrainType()); if (!prisonTemplates.empty()) { + PrisonHeroPlacer * prisonHeroPlacer = nullptr; + for(auto & z : map.getZones()) + { + prisonHeroPlacer = z.second->getModificator(); + if (prisonHeroPlacer) + { + break; + } + } + //prisons //levels 1, 5, 10, 20, 30 - static int prisonsLevels = std::min(generator.getConfig().prisonExperience.size(), generator.getConfig().prisonValues.size()); + static const int prisonsLevels = std::min(generator.getConfig().prisonExperience.size(), generator.getConfig().prisonValues.size()); size_t prisonsLeft = getMaxPrisons(); for (int i = prisonsLevels - 1; i >= 0; i--) { + ObjectInfo oi; // Create new instance which will hold destructor operation + oi.value = generator.getConfig().prisonValues[i]; if (oi.value > zone.getMaxTreasureValue()) { continue; } - oi.generateObject = [i, this]() -> CGObjectInstance* + oi.generateObject = [i, this, prisonHeroPlacer]() -> CGObjectInstance* { - auto possibleHeroes = generator.getAllPossibleHeroes(); - HeroTypeID hid = *RandomGeneratorUtil::nextItem(possibleHeroes, zone.getRand()); - + HeroTypeID hid = prisonHeroPlacer->drawRandomHero(); auto factory = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0); - auto* obj = dynamic_cast(factory->create()); + auto* obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); - obj->subID = hid; //will be initialized later + obj->setHeroType(hid); //will be initialized later obj->exp = generator.getConfig().prisonExperience[i]; obj->setOwner(PlayerColor::NEUTRAL); - generator.banHero(hid); return obj; }; + oi.destroyObject = [prisonHeroPlacer](CGObjectInstance* obj) + { + // Hero can be used again + auto* hero = dynamic_cast(obj); + prisonHeroPlacer->restoreDrawnHero(hero->getHeroType()); + }; + oi.setTemplates(Obj::PRISON, 0, zone.getTerrainType()); oi.value = generator.getConfig().prisonValues[i]; oi.probability = 30; @@ -136,12 +162,12 @@ void TreasurePlacer::addAllPossibleObjects() //all following objects are unlimited oi.maxPerZone = std::numeric_limits::max(); - std::vector creatures; //native creatures for this zone + std::vector creatures; //native creatures for this zone for(auto cre : VLC->creh->objects) { if(!cre->special && cre->getFaction() == zone.getTownType()) { - creatures.push_back(cre); + creatures.push_back(cre.get()); } } @@ -155,8 +181,8 @@ void TreasurePlacer::addAllPossibleObjects() if(dwellingType == Obj::CREATURE_GENERATOR1) { //don't spawn original "neutral" dwellings that got replaced by Conflux dwellings in AB - static int elementalConfluxROE[] = {7, 13, 16, 47}; - for(int & i : elementalConfluxROE) + static const MapObjectSubID elementalConfluxROE[] = {7, 13, 16, 47}; + for(auto const & i : elementalConfluxROE) vstd::erase_if_present(subObjects, i); } @@ -174,9 +200,9 @@ void TreasurePlacer::addAllPossibleObjects() oi.value = static_cast(cre->getAIValue() * cre->getGrowth() * (1 + (nativeZonesCount / map.getTotalZoneCount()) + (nativeZonesCount / 2))); oi.probability = 40; - oi.generateObject = [secondaryID, dwellingType]() -> CGObjectInstance * + oi.generateObject = [this, secondaryID, dwellingType]() -> CGObjectInstance * { - auto * obj = VLC->objtypeh->getHandlerFor(dwellingType, secondaryID)->create(); + auto * obj = VLC->objtypeh->getHandlerFor(dwellingType, secondaryID)->create(map.mapInstance->cb, nullptr); obj->tempOwner = PlayerColor::NEUTRAL; return obj; }; @@ -192,7 +218,7 @@ void TreasurePlacer::addAllPossibleObjects() oi.generateObject = [i, this]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SPELL_SCROLL, 0); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); std::vector out; for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) @@ -216,10 +242,10 @@ void TreasurePlacer::addAllPossibleObjects() //pandora box with gold for(int i = 1; i < 5; i++) { - oi.generateObject = [i]() -> CGObjectInstance * + oi.generateObject = [this, i]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); Rewardable::VisitInfo reward; reward.reward.resources[EGameResID::GOLD] = i * 5000; @@ -238,10 +264,10 @@ void TreasurePlacer::addAllPossibleObjects() //pandora box with experience for(int i = 1; i < 5; i++) { - oi.generateObject = [i]() -> CGObjectInstance * + oi.generateObject = [this, i]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); Rewardable::VisitInfo reward; reward.reward.heroExperience = i * 5000; @@ -260,7 +286,7 @@ void TreasurePlacer::addAllPossibleObjects() //pandora box with creatures const std::vector & tierValues = generator.getConfig().pandoraCreatureValues; - auto creatureToCount = [tierValues](CCreature * creature) -> int + auto creatureToCount = [tierValues](const CCreature * creature) -> int { if(!creature->getAIValue() || tierValues.empty()) //bug #2681 return 0; //this box won't be generated @@ -300,10 +326,10 @@ void TreasurePlacer::addAllPossibleObjects() if(!creaturesAmount) continue; - oi.generateObject = [creature, creaturesAmount]() -> CGObjectInstance * + oi.generateObject = [this, creature, creaturesAmount]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); Rewardable::VisitInfo reward; reward.reward.creatures.emplace_back(creature, creaturesAmount); @@ -325,13 +351,13 @@ void TreasurePlacer::addAllPossibleObjects() oi.generateObject = [i, this]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); - std::vector spells; + std::vector spells; for(auto spell : VLC->spellh->objects) { if(map.isAllowedSpell(spell->id) && spell->getLevel() == i) - spells.push_back(spell); + spells.push_back(spell.get()); } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); @@ -358,13 +384,13 @@ void TreasurePlacer::addAllPossibleObjects() oi.generateObject = [i, this]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); - std::vector spells; + std::vector spells; for(auto spell : VLC->spellh->objects) { - if(map.isAllowedSpell(spell->id) && spell->school[SpellSchool(i)]) - spells.push_back(spell); + if(map.isAllowedSpell(spell->id) && spell->hasSchool(SpellSchool(i))) + spells.push_back(spell.get()); } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); @@ -390,13 +416,13 @@ void TreasurePlacer::addAllPossibleObjects() oi.generateObject = [this]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); - std::vector spells; + std::vector spells; for(auto spell : VLC->spellh->objects) { if(map.isAllowedSpell(spell->id)) - spells.push_back(spell); + spells.push_back(spell.get()); } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); @@ -441,6 +467,21 @@ void TreasurePlacer::addAllPossibleObjects() RandomGeneratorUtil::randomShuffle(creatures, zone.getRand()); + auto setRandomArtifact = [qap](CGSeerHut * obj) + { + ArtifactID artid = qap->drawRandomArtifact(); + obj->quest->mission.artifacts.push_back(artid); + qap->addQuestArtifact(artid); + }; + auto destroyObject = [qap](CGObjectInstance * obj) + { + auto * seer = dynamic_cast(obj); + // Artifact can be used again + ArtifactID artid = seer->quest->mission.artifacts.front(); + qap->addRandomArtifact(artid); + qap->removeQuestArtifact(artid); + }; + for(int i = 0; i < static_cast(creatures.size()); i++) { auto * creature = creatures[i]; @@ -451,24 +492,22 @@ void TreasurePlacer::addAllPossibleObjects() int randomAppearance = chooseRandomAppearance(zone.getRand(), Obj::SEER_HUT, zone.getTerrainType()); - oi.generateObject = [creature, creaturesAmount, randomAppearance, this, qap]() -> CGObjectInstance * + // FIXME: Remove duplicated code for gold, exp and creaure reward + oi.generateObject = [cb=map.mapInstance->cb, creature, creaturesAmount, randomAppearance, setRandomArtifact]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(cb, nullptr)); Rewardable::VisitInfo reward; reward.reward.creatures.emplace_back(creature->getId(), creaturesAmount); reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->mission.artifacts.push_back(artid); - - generator.banQuestArt(artid); - zone.getModificator()->addQuestArtifact(artid); + setRandomArtifact(obj); return obj; }; + oi.destroyObject = destroyObject; oi.probability = 3; oi.setTemplates(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); oi.value = static_cast(((2 * (creature->getAIValue()) * creaturesAmount * (1 + static_cast(map.getZoneCount(creature->getFaction())) / map.getTotalZoneCount())) - 4000) / 3); @@ -483,7 +522,7 @@ void TreasurePlacer::addAllPossibleObjects() } } - static int seerLevels = std::min(generator.getConfig().questValues.size(), generator.getConfig().questRewardValues.size()); + static const int seerLevels = std::min(generator.getConfig().questValues.size(), generator.getConfig().questRewardValues.size()); for(int i = 0; i < seerLevels; i++) //seems that code for exp and gold reward is similiar { int randomAppearance = chooseRandomAppearance(zone.getRand(), Obj::SEER_HUT, zone.getTerrainType()); @@ -499,46 +538,40 @@ void TreasurePlacer::addAllPossibleObjects() oi.probability = 10; oi.maxPerZone = 1; - oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance * + oi.generateObject = [i, randomAppearance, this, setRandomArtifact]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); Rewardable::VisitInfo reward; reward.reward.heroExperience = generator.getConfig().questRewardValues[i]; reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - - ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->mission.artifacts.push_back(artid); - - generator.banQuestArt(artid); - zone.getModificator()->addQuestArtifact(artid); - + + setRandomArtifact(obj); + return obj; }; + oi.destroyObject = destroyObject; if(!oi.templates.empty()) possibleSeerHuts.push_back(oi); - oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance * + oi.generateObject = [i, randomAppearance, this, setRandomArtifact]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); - auto * obj = dynamic_cast(factory->create()); + auto * obj = dynamic_cast(factory->create(map.mapInstance->cb, nullptr)); Rewardable::VisitInfo reward; reward.reward.resources[EGameResID::GOLD] = generator.getConfig().questRewardValues[i]; reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->mission.artifacts.push_back(artid); - - generator.banQuestArt(artid); - zone.getModificator()->addQuestArtifact(artid); + setRandomArtifact(obj); return obj; }; + oi.destroyObject = destroyObject; if(!oi.templates.empty()) possibleSeerHuts.push_back(oi); @@ -584,7 +617,7 @@ std::vector TreasurePlacer::prepareTreasurePile(const CTreasureInfo int maxValue = treasureInfo.max; int minValue = treasureInfo.min; - const ui32 desiredValue =zone.getRand().nextInt(minValue, maxValue); + const ui32 desiredValue = zone.getRand().nextInt(minValue, maxValue); int currentValue = 0; bool hasLargeObject = false; @@ -614,6 +647,13 @@ std::vector TreasurePlacer::prepareTreasurePile(const CTreasureInfo oi->maxPerZone--; currentValue += oi->value; + + if (currentValue >= minValue) + { + // 50% chance to end right here + if (zone.getRand().nextInt() & 1) + break; + } } return objectInfos; @@ -625,16 +665,44 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector for(const auto & oi : treasureInfos) { auto blockedArea = rmgObject.getArea(); + auto entrableArea = rmgObject.getEntrableArea(); auto accessibleArea = rmgObject.getAccessibleArea(); + if(rmgObject.instances().empty()) + { accessibleArea.add(int3()); + } auto * object = oi->generateObject(); if(oi->templates.empty()) + { + logGlobal->warn("Deleting randomized object with no templates: %s", object->getObjectName()); + oi->destroyObject(object); + delete object; continue; + } - object->appearance = *RandomGeneratorUtil::nextItem(oi->templates, zone.getRand()); + auto templates = object->getObjectHandler()->getMostSpecificTemplates(zone.getTerrainType()); + + if (templates.empty()) + { + throw rmgException(boost::str(boost::format("Did not find template for object (%d,%d) at %s") % object->ID % object->subID % zone.getTerrainType().encode(zone.getTerrainType()))); + } + + object->appearance = *RandomGeneratorUtil::nextItem(templates, zone.getRand()); + + //Put object in accessible area next to entrable area (excluding blockvis tiles) + if (!entrableArea.empty()) + { + auto entrableBorder = entrableArea.getBorderOutside(); + accessibleArea.erase_if([&](const int3 & tile) + { + return !entrableBorder.count(tile); + }); + } + auto & instance = rmgObject.addInstance(*object); + instance.onCleared = oi->destroyObject; do { @@ -646,15 +714,26 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector } std::vector bestPositions; - if(densePlacement) + if(densePlacement && !entrableArea.empty()) { + // Choose positon which has access to as many entrable tiles as possible int bestPositionsWeight = std::numeric_limits::max(); for(const auto & t : accessibleArea.getTilesVector()) { instance.setPosition(t); - int w = rmgObject.getAccessibleArea().getTilesVector().size(); - if(w < bestPositionsWeight) + + auto currentAccessibleArea = rmgObject.getAccessibleArea(); + auto currentEntrableBorder = rmgObject.getEntrableArea().getBorderOutside(); + currentAccessibleArea.erase_if([&](const int3 & tile) { + return !currentEntrableBorder.count(tile); + }); + + size_t w = currentAccessibleArea.getTilesVector().size(); + + if(w > bestPositionsWeight) + { + // Minimum 1 position must be entrable bestPositions.clear(); bestPositions.push_back(t); bestPositionsWeight = w; @@ -665,7 +744,8 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector } } } - else + + if (bestPositions.empty()) { bestPositions = accessibleArea.getTilesVector(); } @@ -676,18 +756,17 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector auto instanceAccessibleArea = instance.getAccessibleArea(); if(instance.getBlockedArea().getTilesVector().size() == 1) { - if(instance.object().appearance->isVisitableFromTop() && instance.object().ID != Obj::CORPSE) + if(instance.object().appearance->isVisitableFromTop() && !instance.object().isBlockedVisitable()) instanceAccessibleArea.add(instance.getVisitablePosition()); } - //first object is good + //Do not clean up after first object if(rmgObject.instances().size() == 1) break; - - //condition for good position + if(!blockedArea.overlap(instance.getBlockedArea()) && accessibleArea.overlap(instanceAccessibleArea)) break; - + //fail - new position accessibleArea.erase(nextPos); } while(true); @@ -748,7 +827,7 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) int mapMonsterStrength = map.getMapGenOptions().getMonsterStrength(); int monsterStrength = (zone.monsterStrength == EMonsterStrength::ZONE_NONE ? 0 : zone.monsterStrength + mapMonsterStrength - 1); //array index from 0 to 4; pick any correct value for ZONE_NONE, minGuardedValue won't be used in this case anyway - static int minGuardedValues[] = { 6500, 4167, 3000, 1833, 1333 }; + static const int minGuardedValues[] = { 6500, 4167, 3000, 1833, 1333 }; minGuardedValue = minGuardedValues[monsterStrength]; auto valueComparator = [](const CTreasureInfo& lhs, const CTreasureInfo& rhs) -> bool @@ -763,7 +842,6 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) oi->maxPerZone++; } }; - //place biggest treasures first at large distance, place smaller ones inbetween auto treasureInfo = zone.getTreasureInfo(); boost::sort(treasureInfo, valueComparator); @@ -777,7 +855,7 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) size_t size = 0; { Zone::Lock lock(zone.areaMutex); - size = zone.getArea().getTiles().size(); + size = zone.getArea().getTilesVector().size(); } int totalDensity = 0; @@ -794,16 +872,17 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) totalDensity += t->density; - size_t count = size * t->density / 500; + const int DENSITY_CONSTANT = 300; + size_t count = (size * t->density) / DENSITY_CONSTANT; //Assure space for lesser treasures, if there are any left + const int averageValue = (t->min + t->max) / 2; if (t != (treasureInfo.end() - 1)) { - const int averageValue = (t->min + t->max) / 2; if (averageValue > 10000) { //Will surely be guarded => larger piles => less space inbetween - vstd::amin(count, size * (10.f / 500) / (std::sqrt((float)averageValue / 10000))); + vstd::amin(count, size * (10.f / DENSITY_CONSTANT) / (std::sqrt((float)averageValue / 10000))); } } @@ -822,13 +901,18 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; }); - for (ui32 attempt = 0; attempt <= 2; attempt++) + const ui32 maxPileGenerationAttemps = 2; + for (ui32 attempt = 0; attempt < maxPileGenerationAttemps; attempt++) { auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts); - if (rmgObject.instances().empty()) //handle incorrect placement + if (rmgObject.instances().empty()) { - restoreZoneLimits(treasurePileInfos); + // Restore once if all attemps failed + if (attempt == (maxPileGenerationAttemps - 1)) + { + restoreZoneLimits(treasurePileInfos); + } continue; } @@ -846,61 +930,58 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) { const bool guarded = rmgObject.isGuarded(); - for (int attempt = 0; attempt <= maxAttempts;) + auto path = rmg::Path::invalid(); + + Zone::Lock lock(zone.areaMutex); //We are going to subtract this area + auto possibleArea = zone.areaPossible(); + possibleArea.erase_if([this, &minDistance](const int3& tile) -> bool { - auto path = rmg::Path::invalid(); + auto ti = map.getTileInfo(tile); + return (ti.getNearestObjectDistance() < minDistance); + }); - Zone::Lock lock(zone.areaMutex); //We are going to subtract this area - auto possibleArea = zone.areaPossible(); + if (guarded) + { + path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile) + { + float bestDistance = 10e9; + for (const auto& t : rmgObject.getArea().getTilesVector()) + { + float distance = map.getTileInfo(t).getNearestObjectDistance(); + if (distance < minDistance) + return -1.f; + else + vstd::amin(bestDistance, distance); + } + const auto & guardedArea = rmgObject.instances().back()->getAccessibleArea(); + const auto areaToBlock = rmgObject.getAccessibleArea(true) - guardedArea; + + if (zone.freePaths().overlap(areaToBlock) || manager.getVisitableArea().overlap(areaToBlock)) + return -1.f; + + return bestDistance; + }, guarded, false, ObjectManager::OptimizeType::BOTH); + } + else + { + path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE); + } + lock.unlock(); + + if (path.valid()) + { + //debug purposes + treasureArea.unite(rmgObject.getArea()); if (guarded) { - path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile) - { - auto ti = map.getTileInfo(tile); - if (ti.getNearestObjectDistance() < minDistance) - return -1.f; - - for (const auto& t : rmgObject.getArea().getTilesVector()) - { - if (map.getTileInfo(t).getNearestObjectDistance() < minDistance) - return -1.f; - } - - auto guardedArea = rmgObject.instances().back()->getAccessibleArea(); - auto areaToBlock = rmgObject.getAccessibleArea(true); - areaToBlock.subtract(guardedArea); - if (areaToBlock.overlap(zone.freePaths()) || areaToBlock.overlap(manager.getVisitableArea())) - return -1.f; - - return ti.getNearestObjectDistance(); - }, guarded, false, ObjectManager::OptimizeType::DISTANCE); - } - else - { - path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE); - } - - if (path.valid()) - { - //debug purposes - treasureArea.unite(rmgObject.getArea()); - if (guarded) - { - guards.unite(rmgObject.instances().back()->getBlockedArea()); - auto guardedArea = rmgObject.instances().back()->getAccessibleArea(); - auto areaToBlock = rmgObject.getAccessibleArea(true); - areaToBlock.subtract(guardedArea); - treasureBlockArea.unite(areaToBlock); - } - zone.connectPath(path); - manager.placeObject(rmgObject, guarded, true); - break; - } - else - { - ++attempt; + guards.unite(rmgObject.instances().back()->getBlockedArea()); + auto guardedArea = rmgObject.instances().back()->getAccessibleArea(); + auto areaToBlock = rmgObject.getAccessibleArea(true) - guardedArea; + treasureBlockArea.unite(areaToBlock); } + zone.connectPath(path); + manager.placeObject(rmgObject, guarded, true); } } } @@ -918,7 +999,7 @@ char TreasurePlacer::dump(const int3 & t) return Modificator::dump(t); } -void ObjectInfo::setTemplates(si32 type, si32 subtype, TerrainId terrainType) +void ObjectInfo::setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrainType) { auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype); if(!templHandler) diff --git a/lib/rmg/modificators/TreasurePlacer.h b/lib/rmg/modificators/TreasurePlacer.h index ef4881e25..8c6a6c316 100644 --- a/lib/rmg/modificators/TreasurePlacer.h +++ b/lib/rmg/modificators/TreasurePlacer.h @@ -22,14 +22,17 @@ class CRandomGenerator; struct ObjectInfo { + ObjectInfo(); + std::vector> templates; ui32 value = 0; ui16 probability = 0; ui32 maxPerZone = 1; //ui32 maxPerMap; //unused std::function generateObject; + std::function destroyObject; - void setTemplates(si32 type, si32 subtype, TerrainId terrain); + void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain); }; class TreasurePlacer: public Modificator diff --git a/lib/rmg/modificators/WaterAdopter.h b/lib/rmg/modificators/WaterAdopter.h index 833d427c9..b066ea3df 100644 --- a/lib/rmg/modificators/WaterAdopter.h +++ b/lib/rmg/modificators/WaterAdopter.h @@ -31,7 +31,8 @@ protected: void createWater(EWaterContent::EWaterContent waterContent); protected: - rmg::Area noWaterArea, waterArea; + rmg::Area noWaterArea; + rmg::Area waterArea; TRmgTemplateZoneId waterZoneId; std::map distanceMap; std::map reverseDistanceMap; diff --git a/lib/rmg/modificators/WaterProxy.cpp b/lib/rmg/modificators/WaterProxy.cpp index 839511d7c..551b00860 100644 --- a/lib/rmg/modificators/WaterProxy.cpp +++ b/lib/rmg/modificators/WaterProxy.cpp @@ -15,6 +15,7 @@ #include "../../TerrainHandler.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" #include "../../mapObjectConstructors/CObjectClassesHandler.h" +#include "../../mapObjects/MiscObjects.h" #include "../../mapping/CMap.h" #include "../../mapping/CMapEditManager.h" #include "../RmgPath.h" @@ -112,7 +113,7 @@ void WaterProxy::collectLakes() for(const auto & t : lake.getBorderOutside()) if(map.isOnMap(t)) lakes.back().neighbourZones[map.getZoneID(t)].add(t); - for(const auto & t : lake.getTiles()) + for(const auto & t : lake.getTilesVector()) lakeMap[t] = lakeId; //each lake must have at least one free tile @@ -143,7 +144,7 @@ RouteInfo WaterProxy::waterRoute(Zone & dst) { if(!lake.keepConnections.count(dst.getId())) { - for(const auto & ct : lake.neighbourZones[dst.getId()].getTiles()) + for(const auto & ct : lake.neighbourZones[dst.getId()].getTilesVector()) { if(map.isPossible(ct)) map.setOccupied(ct, ETileType::BLOCKED); @@ -155,7 +156,7 @@ RouteInfo WaterProxy::waterRoute(Zone & dst) } //Don't place shipyard or boats on the very small lake - if (lake.area.getTiles().size() < 25) + if (lake.area.getTilesVector().size() < 25) { logGlobal->info("Skipping very small lake at zone %d", dst.getId()); continue; @@ -240,7 +241,7 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout for(auto subObj : subObjects) { //making a temporary object - std::unique_ptr obj(VLC->objtypeh->getHandlerFor(Obj::BOAT, subObj)->create()); + std::unique_ptr obj(VLC->objtypeh->getHandlerFor(Obj::BOAT, subObj)->create(map.mapInstance->cb, nullptr)); if(auto * testBoat = dynamic_cast(obj.get())) { if(testBoat->layer == EPathfindingLayer::SAIL) @@ -251,7 +252,7 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout if(sailingBoatTypes.empty()) return false; - auto * boat = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::BOAT, *RandomGeneratorUtil::nextItem(sailingBoatTypes, zone.getRand()))->create()); + auto * boat = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::BOAT, *RandomGeneratorUtil::nextItem(sailingBoatTypes, zone.getRand()))->create(map.mapInstance->cb, nullptr)); rmg::Object rmgObject(*boat); rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); @@ -273,7 +274,7 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout while(!boardingPositions.empty()) { - auto boardingPosition = *boardingPositions.getTiles().begin(); + auto boardingPosition = *boardingPositions.getTilesVector().begin(); rmg::Area shipPositions({boardingPosition}); auto boutside = shipPositions.getBorderOutside(); shipPositions.assign(boutside); @@ -315,7 +316,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool return false; int subtype = chooseRandomAppearance(zone.getRand(), Obj::SHIPYARD, land.getTerrainType()); - auto * shipyard = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::SHIPYARD, subtype)->create()); + auto * shipyard = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::SHIPYARD, subtype)->create(map.mapInstance->cb, nullptr)); shipyard->tempOwner = PlayerColor::NEUTRAL; rmg::Object rmgObject(*shipyard); @@ -336,7 +337,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool while(!boardingPositions.empty()) { - auto boardingPosition = *boardingPositions.getTiles().begin(); + auto boardingPosition = *boardingPositions.getTilesVector().begin(); rmg::Area shipPositions({boardingPosition}); auto boutside = shipPositions.getBorderOutside(); shipPositions.assign(boutside); diff --git a/lib/rmg/threadpool/MapProxy.cpp b/lib/rmg/threadpool/MapProxy.cpp index d1bf2f8ee..af872e2c5 100644 --- a/lib/rmg/threadpool/MapProxy.cpp +++ b/lib/rmg/threadpool/MapProxy.cpp @@ -14,47 +14,47 @@ VCMI_LIB_NAMESPACE_BEGIN MapProxy::MapProxy(RmgMap & map): - map(map) + map(map) { } void MapProxy::insertObject(CGObjectInstance * obj) { - Lock lock(mx); - map.getEditManager()->insertObject(obj); + Lock lock(mx); + map.getEditManager()->insertObject(obj); } void MapProxy::insertObjects(std::set& objects) { - Lock lock(mx); - map.getEditManager()->insertObjects(objects); + Lock lock(mx); + map.getEditManager()->insertObjects(objects); } void MapProxy::removeObject(CGObjectInstance * obj) { - Lock lock(mx); - map.getEditManager()->removeObject(obj); + Lock lock(mx); + map.getEditManager()->removeObject(obj); } void MapProxy::drawTerrain(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain) { - Lock lock(mx); + Lock lock(mx); map.getEditManager()->getTerrainSelection().setSelection(tiles); - map.getEditManager()->drawTerrain(terrain, &generator); + map.getEditManager()->drawTerrain(terrain, map.getDecorationsPercentage(), &generator); } void MapProxy::drawRivers(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain) { - Lock lock(mx); + Lock lock(mx); map.getEditManager()->getTerrainSelection().setSelection(tiles); map.getEditManager()->drawRiver(VLC->terrainTypeHandler->getById(terrain)->river, &generator); } void MapProxy::drawRoads(CRandomGenerator & generator, std::vector & tiles, RoadId roadType) { - Lock lock(mx); - map.getEditManager()->getTerrainSelection().setSelection(tiles); + Lock lock(mx); + map.getEditManager()->getTerrainSelection().setSelection(tiles); map.getEditManager()->drawRoad(roadType, &generator); } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/threadpool/MapProxy.h b/lib/rmg/threadpool/MapProxy.h index a85d74fbf..9cb265f8f 100644 --- a/lib/rmg/threadpool/MapProxy.h +++ b/lib/rmg/threadpool/MapProxy.h @@ -22,21 +22,21 @@ class RmgMap; class MapProxy { public: - MapProxy(RmgMap & map); + MapProxy(RmgMap & map); - void insertObject(CGObjectInstance * obj); - void insertObjects(std::set& objects); - void removeObject(CGObjectInstance* obj); + void insertObject(CGObjectInstance * obj); + void insertObjects(std::set& objects); + void removeObject(CGObjectInstance* obj); - void drawTerrain(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); - void drawRivers(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); - void drawRoads(CRandomGenerator & generator, std::vector & tiles, RoadId roadType); + void drawTerrain(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); + void drawRivers(CRandomGenerator & generator, std::vector & tiles, TerrainId terrain); + void drawRoads(CRandomGenerator & generator, std::vector & tiles, RoadId roadType); private: - mutable boost::shared_mutex mx; - using Lock = boost::unique_lock; + mutable boost::shared_mutex mx; + using Lock = boost::unique_lock; - RmgMap & map; + RmgMap & map; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/threadpool/ThreadPool.h b/lib/rmg/threadpool/ThreadPool.h index 3fbb81612..dcb69374e 100644 --- a/lib/rmg/threadpool/ThreadPool.h +++ b/lib/rmg/threadpool/ThreadPool.h @@ -179,7 +179,7 @@ auto ThreadPool::async(std::function&& f) const -> boost::future } } - std::shared_ptr task = std::make_shared(f); + auto task = std::make_shared(f); boost::future fut = task->get_future(); tasks.emplace([task]() -> void { diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index 95400caf9..5a82c58fe 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -9,97 +9,18 @@ */ #include "StdInc.h" #include "BinaryDeserializer.h" - #include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(BinaryDeserializer & s); - -CLoadFile::CLoadFile(const boost::filesystem::path & fname, int minimalVersion) - : serializer(this) +BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) { - registerTypes(serializer); - openNextFile(fname, minimalVersion); -} + saving = false; + version = Version::NONE; + smartPointerSerialization = true; + reverseEndianess = false; -//must be instantiated in .cpp file for access to complete types of all member fields -CLoadFile::~CLoadFile() = default; - -int CLoadFile::read(void * data, unsigned size) -{ - sfile->read(reinterpret_cast(data), size); - return size; -} - -void CLoadFile::openNextFile(const boost::filesystem::path & fname, int minimalVersion) -{ - assert(!serializer.reverseEndianess); - assert(minimalVersion <= SERIALIZATION_VERSION); - - try - { - fName = fname.string(); - sfile = std::make_unique(fname.c_str(), std::ios::in | std::ios::binary); - sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway - - if(!(*sfile)) - THROW_FORMAT("Error: cannot open to read %s!", fName); - - //we can read - char buffer[4]; - sfile->read(buffer, 4); - if(std::memcmp(buffer, "VCMI", 4) != 0) - THROW_FORMAT("Error: not a VCMI file(%s)!", fName); - - serializer & serializer.fileVersion; - if(serializer.fileVersion < minimalVersion) - THROW_FORMAT("Error: too old file format (%s)!", fName); - - if(serializer.fileVersion > SERIALIZATION_VERSION) - { - logGlobal->warn("Warning format version mismatch: found %d when current is %d! (file %s)\n", serializer.fileVersion, SERIALIZATION_VERSION , fName); - - auto * versionptr = reinterpret_cast(&serializer.fileVersion); - std::reverse(versionptr, versionptr + 4); - logGlobal->warn("Version number reversed is %x, checking...", serializer.fileVersion); - - if(serializer.fileVersion == SERIALIZATION_VERSION) - { - logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string()); - serializer.reverseEndianess = true; - } - else - THROW_FORMAT("Error: too new file format (%s)!", fName); - } - } - catch(...) - { - clear(); //if anything went wrong, we delete file and rethrow - throw; - } -} - -void CLoadFile::reportState(vstd::CLoggerBase * out) -{ - out->debug("CLoadFile"); - if(!!sfile && *sfile) - out->debug("\tOpened %s Position: %d", fName, sfile->tellg()); -} - -void CLoadFile::clear() -{ - sfile = nullptr; - fName.clear(); - serializer.fileVersion = 0; -} - -void CLoadFile::checkMagicBytes(const std::string &text) -{ - std::string loaded = text; - read((void *)loaded.data(), static_cast(text.length())); - if(loaded != text) - throw std::runtime_error("Magic bytes doesn't match!"); + registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 2ed7d553d..5f702e190 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -9,17 +9,13 @@ */ #pragma once -#include -#include - +#include "CSerializer.h" #include "CTypeList.h" +#include "ESerializationVersion.h" #include "../mapObjects/CGHeroInstance.h" -#include "../../Global.h" VCMI_LIB_NAMESPACE_BEGIN -class CStackInstance; - class DLL_LINKAGE CLoaderBase { protected: @@ -27,9 +23,13 @@ protected: public: CLoaderBase(IBinaryReader * r): reader(r){}; - inline int read(void * data, unsigned size) + inline void read(void * data, unsigned size, bool reverseEndianess) { - return reader->read(data, size); + auto bytePtr = reinterpret_cast(data); + + reader->read(bytePtr, size); + if(reverseEndianess) + std::reverse(bytePtr, bytePtr + size); }; }; @@ -37,41 +37,6 @@ public: /// Effectively revesed version of BinarySerializer class DLL_LINKAGE BinaryDeserializer : public CLoaderBase { - template - struct VariantLoaderHelper - { - Source & source; - std::vector> funcs; - - template - struct mpl_types_impl; - - template - struct mpl_types_impl> { - using type = boost::mpl::vector; - }; - - template - using mpl_types = typename mpl_types_impl::type; - - VariantLoaderHelper(Source & source): - source(source) - { - boost::mpl::for_each>(std::ref(*this)); - } - - template - void operator()(Type) - { - funcs.push_back([&]() -> Variant - { - Type obj; - source.load(obj); - return Variant(obj); - }); - } - }; - template struct LoadIfStackInstance { @@ -109,22 +74,33 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase template struct ClassObjectCreator { - static T *invoke() + static T *invoke(IGameCallback *cb) { - static_assert(!std::is_abstract::value, "Cannot call new upon abstract classes!"); + static_assert(!std::is_base_of_v, "Cannot call new upon map objects!"); + static_assert(!std::is_abstract_v, "Cannot call new upon abstract classes!"); return new T(); } }; template - struct ClassObjectCreator::value>::type> + struct ClassObjectCreator>> { - static T *invoke() + static T *invoke(IGameCallback *cb) { throw std::runtime_error("Something went really wrong during deserialization. Attempted creating an object of an abstract class " + std::string(typeid(T).name())); } }; + template + struct ClassObjectCreator && !std::is_abstract_v>> + { + static T *invoke(IGameCallback *cb) + { + static_assert(!std::is_abstract_v, "Cannot call new upon abstract classes!"); + return new T(cb); + } + }; + STRONG_INLINE ui32 readAndCheckLength() { ui32 length; @@ -138,60 +114,55 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase return length; } - template class CPointerLoader; + template class CPointerLoader; - class CBasicPointerLoader + class IPointerLoader { public: - virtual const std::type_info * loadPtr(CLoaderBase &ar, void *data, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER - virtual ~CBasicPointerLoader(){} + virtual void * loadPtr(CLoaderBase &ar, IGameCallback * cb, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER + virtual ~IPointerLoader() = default; - template static CBasicPointerLoader *getApplier(const T * t=nullptr) + template static IPointerLoader *getApplier(const Type * t = nullptr) { - return new CPointerLoader(); + return new CPointerLoader(); } }; - template class CPointerLoader : public CBasicPointerLoader + template + class CPointerLoader : public IPointerLoader { public: - const std::type_info * loadPtr(CLoaderBase &ar, void *data, ui32 pid) const override //data is pointer to the ACTUAL POINTER + void * loadPtr(CLoaderBase &ar, IGameCallback * cb, ui32 pid) const override //data is pointer to the ACTUAL POINTER { auto & s = static_cast(ar); - T *&ptr = *static_cast(data); //create new object under pointer - typedef typename std::remove_pointer::type npT; - ptr = ClassObjectCreator::invoke(); //does new npT or throws for abstract classes + Type * ptr = ClassObjectCreator::invoke(cb); //does new npT or throws for abstract classes s.ptrAllocated(ptr, pid); - //T is most derived known type, it's time to call actual serialize - assert(s.fileVersion != 0); - ptr->serialize(s,s.fileVersion); - return &typeid(T); + + ptr->serialize(s); + + return static_cast(ptr); } }; - CApplier applier; + CApplier applier; int write(const void * data, unsigned size); public: + using Version = ESerializationVersion; + bool reverseEndianess; //if source has different endianness than us, we reverse bytes - si32 fileVersion; + Version version; std::map loadedPointers; - std::map loadedPointersTypes; - std::map loadedSharedPointers; + std::map> loadedSharedPointers; + IGameCallback * cb = nullptr; bool smartPointerSerialization; bool saving; - BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) - { - saving = false; - fileVersion = 0; - smartPointerSerialization = true; - reverseEndianess = false; - } + BinaryDeserializer(IBinaryReader * r); template BinaryDeserializer & operator&(T & t) @@ -200,26 +171,21 @@ public: return * this; } - template < class T, typename std::enable_if < std::is_fundamental::value && !std::is_same::value, int >::type = 0 > + template < class T, typename std::enable_if_t < std::is_fundamental_v && !std::is_same_v, int > = 0 > void load(T &data) { - unsigned length = sizeof(data); - char * dataPtr = reinterpret_cast(&data); - this->read(dataPtr,length); - if(reverseEndianess) - std::reverse(dataPtr, dataPtr + length); + this->read(static_cast(&data), sizeof(data), reverseEndianess); } - template < typename T, typename std::enable_if < is_serializeable::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < is_serializeable::value, int > = 0 > void load(T &data) { - assert( fileVersion != 0 ); ////that const cast is evil because it allows to implicitly overwrite const objects when deserializing - typedef typename std::remove_const::type nonConstT; + typedef typename std::remove_const_t nonConstT; auto & hlp = const_cast(data); - hlp.serialize(*this,fileVersion); + hlp.serialize(*this); } - template < typename T, typename std::enable_if < std::is_array::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_array_v, int > = 0 > void load(T &data) { ui32 size = std::size(data); @@ -227,7 +193,7 @@ public: load(data[i]); } - template < typename T, typename std::enable_if < std::is_enum::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_enum_v, int > = 0 > void load(T &data) { si32 read; @@ -235,7 +201,7 @@ public: data = static_cast(read); } - template < typename T, typename std::enable_if < std::is_same::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_same_v, int > = 0 > void load(T &data) { ui8 read; @@ -243,16 +209,7 @@ public: data = static_cast(read); } - template < typename T, typename std::enable_if < std::is_same >::value, int >::type = 0 > - void load(T & data) - { - std::vector convData; - load(convData); - convData.resize(data.size()); - range::copy(convData, data.begin()); - } - - template ::value, int >::type = 0> + template , int > = 0> void load(std::vector &data) { ui32 length = readAndCheckLength(); @@ -261,20 +218,39 @@ public: load( data[i]); } - template < typename T, typename std::enable_if < std::is_pointer::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_pointer_v, int > = 0 > void load(T &data) { - ui8 hlp; - load( hlp ); - if(!hlp) + bool isNull; + load( isNull ); + if(isNull) { data = nullptr; return; } + loadPointerImpl(data); + } + + template < typename T, typename std::enable_if_t < std::is_base_of_v>, int > = 0 > + void loadPointerImpl(T &data) + { + using DataType = std::remove_pointer_t; + + typename DataType::IdentifierType index; + load(index); + + auto * constEntity = index.toEntity(VLC); + auto * constData = dynamic_cast(constEntity); + data = const_cast(constData); + } + + template < typename T, typename std::enable_if_t < !std::is_base_of_v>, int > = 0 > + void loadPointerImpl(T &data) + { if(reader->smartVectorMembersSerialization) { - typedef typename std::remove_const::type>::type TObjectType; //eg: const CGHeroInstance * => CGHeroInstance + typedef typename std::remove_const_t> TObjectType; //eg: const CGHeroInstance * => CGHeroInstance typedef typename VectorizedTypeFor::type VType; //eg: CGHeroInstance -> CGobjectInstance typedef typename VectorizedIDType::type IDType; if(const auto *info = reader->getVectorizedTypeInfo()) @@ -306,21 +282,19 @@ public: { // We already got this pointer // Cast it in case we are loading it to a non-first base pointer - assert(loadedPointersTypes.count(pid)); - data = reinterpret_cast(typeList.castRaw(i->second, loadedPointersTypes.at(pid), &typeid(typename std::remove_const::type>::type))); + data = static_cast(i->second); return; } } - //get type id ui16 tid; load( tid ); if(!tid) { - typedef typename std::remove_pointer::type npT; - typedef typename std::remove_const::type ncpT; - data = ClassObjectCreator::invoke(); + typedef typename std::remove_pointer_t npT; + typedef typename std::remove_const_t ncpT; + data = ClassObjectCreator::invoke(cb); ptrAllocated(data, pid); load(*data); } @@ -333,8 +307,7 @@ public: data = nullptr; return; } - auto typeInfo = app->loadPtr(*this,&data, pid); - data = reinterpret_cast(typeList.castRaw((void*)data, typeInfo, &typeid(typename std::remove_const::type>::type))); + data = static_cast(app->loadPtr(*this, cb, pid)); } } @@ -342,10 +315,7 @@ public: void ptrAllocated(const T *ptr, ui32 pid) { if(smartPointerSerialization && pid != 0xffffffff) - { - loadedPointersTypes[pid] = &typeid(T); loadedPointers[pid] = (void*)ptr; //add loaded pointer to our lookup map; cast is to avoid errors with const T* pt - } } template void registerType(const Base * b = nullptr, const Derived * d = nullptr) @@ -356,11 +326,11 @@ public: template void load(std::shared_ptr &data) { - typedef typename std::remove_const::type NonConstT; + typedef typename std::remove_const_t NonConstT; NonConstT *internalPtr; load(internalPtr); - void *internalPtrDerived = typeList.castToMostDerived(internalPtr); + void * internalPtrDerived = static_cast(internalPtr); if(internalPtr) { @@ -369,35 +339,13 @@ public: { // This pointers is already loaded. The "data" needs to be pointed to it, // so their shared state is actually shared. - try - { - auto actualType = typeList.getTypeInfo(internalPtr); - auto typeWeNeedToReturn = typeList.getTypeInfo(); - if(*actualType == *typeWeNeedToReturn) - { - // No casting needed, just unpack already stored shared_ptr and return it - data = std::any_cast>(itr->second); - } - else - { - // We need to perform series of casts - auto ret = typeList.castShared(itr->second, actualType, typeWeNeedToReturn); - data = std::any_cast>(ret); - } - } - catch(std::exception &e) - { - logGlobal->error(e.what()); - logGlobal->error("Failed to cast stored shared ptr. Real type: %s. Needed type %s. FIXME FIXME FIXME", itr->second.type().name(), typeid(std::shared_ptr).name()); - //TODO scenario with inheritance -> we can have stored ptr to base and load ptr to derived (or vice versa) - throw; - } + data = std::static_pointer_cast(itr->second); } else { auto hlp = std::shared_ptr(internalPtr); data = hlp; - loadedSharedPointers[internalPtrDerived] = typeList.castSharedToMostDerived(hlp); + loadedSharedPointers[internalPtrDerived] = std::static_pointer_cast(hlp); } } else @@ -481,46 +429,32 @@ public: ui32 length = readAndCheckLength(); data.clear(); T1 key; - T2 value; for(ui32 i=0;i(std::move(key), std::move(value))); - } - } - template - void load(std::multimap &data) - { - ui32 length = readAndCheckLength(); - data.clear(); - T1 key; - T2 value; - for(ui32 i = 0; i < length; i++) - { - load(key); - load(value); - data.insert(std::pair(std::move(key), std::move(value))); + load(data[key]); } } void load(std::string &data) { ui32 length = readAndCheckLength(); data.resize(length); - this->read((void*)data.c_str(),length); + this->read(static_cast(data.data()), length, false); } - template - void load(std::variant & data) + template + void load(std::variant & data) { - using TVariant = std::variant; - - VariantLoaderHelper loader(*this); - si32 which; load( which ); - assert(which < loader.funcs.size()); - data = loader.funcs.at(which)(); + assert(which < sizeof...(TN)); + + // Create array of variants that contains all default-constructed alternatives + const std::variant table[] = { TN{ }... }; + // use appropriate alternative for result + data = table[which]; + // perform actual load via std::visit dispatch + std::visit([&](auto& o) { load(o); }, data); } template @@ -545,7 +479,9 @@ public: void load(boost::multi_array & data) { ui32 length = readAndCheckLength(); - ui32 x, y, z; + ui32 x; + ui32 y; + ui32 z; load(x); load(y); load(z); @@ -579,30 +515,4 @@ public: } }; -class DLL_LINKAGE CLoadFile : public IBinaryReader -{ -public: - BinaryDeserializer serializer; - - std::string fName; - std::unique_ptr sfile; - - CLoadFile(const boost::filesystem::path & fname, int minimalVersion = SERIALIZATION_VERSION); //throws! - virtual ~CLoadFile(); - int read(void * data, unsigned size) override; //throws! - - void openNextFile(const boost::filesystem::path & fname, int minimalVersion); //throws! - void clear(); - void reportState(vstd::CLoggerBase * out) override; - - void checkMagicBytes(const std::string & text); - - template - CLoadFile & operator>>(T &t) - { - serializer & t; - return * this; - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinarySerializer.cpp b/lib/serializer/BinarySerializer.cpp index 46c99f6de..8ba41babd 100644 --- a/lib/serializer/BinarySerializer.cpp +++ b/lib/serializer/BinarySerializer.cpp @@ -9,69 +9,15 @@ */ #include "StdInc.h" #include "BinarySerializer.h" - #include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(BinarySerializer & s); - -CSaveFile::CSaveFile(const boost::filesystem::path &fname) - : serializer(this) +BinarySerializer::BinarySerializer(IBinaryWriter * w): CSaverBase(w) { - registerTypes(serializer); - openNextFile(fname); -} - -//must be instantiated in .cpp file for access to complete types of all member fields -CSaveFile::~CSaveFile() = default; - -int CSaveFile::write(const void * data, unsigned size) -{ - sfile->write((char *)data,size); - return size; -} - -void CSaveFile::openNextFile(const boost::filesystem::path &fname) -{ - fName = fname; - try - { - sfile = std::make_unique(fname.c_str(), std::ios::out | std::ios::binary); - sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway - - if(!(*sfile)) - THROW_FORMAT("Error: cannot open to write %s!", fname); - - sfile->write("VCMI",4); //write magic identifier - serializer & SERIALIZATION_VERSION; //write format version - } - catch(...) - { - logGlobal->error("Failed to save to %s", fname.string()); - clear(); - throw; - } -} - -void CSaveFile::reportState(vstd::CLoggerBase * out) -{ - out->debug("CSaveFile"); - if(sfile.get() && *sfile) - { - out->debug("\tOpened %s \tPosition: %d", fName, sfile->tellp()); - } -} - -void CSaveFile::clear() -{ - fName.clear(); - sfile = nullptr; -} - -void CSaveFile::putMagicBytes(const std::string &text) -{ - write(text.c_str(), static_cast(text.length())); + saving=true; + smartPointerSerialization = true; + registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 0afcab95e..28da150eb 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -9,7 +9,9 @@ */ #pragma once +#include "CSerializer.h" #include "CTypeList.h" +#include "ESerializationVersion.h" #include "../mapObjects/CArmedInstance.h" VCMI_LIB_NAMESPACE_BEGIN @@ -21,9 +23,9 @@ protected: public: CSaverBase(IBinaryWriter * w): writer(w){}; - inline int write(const void * data, unsigned size) + inline void write(const void * data, unsigned size) { - return writer->write(data, size); + writer->write(reinterpret_cast(data), size); }; }; @@ -102,23 +104,22 @@ class DLL_LINKAGE BinarySerializer : public CSaverBase const T *ptr = static_cast(data); //T is most derived known type, it's time to call actual serialize - const_cast(ptr)->serialize(s, SERIALIZATION_VERSION); + const_cast(ptr)->serialize(s); } }; CApplier applier; public: + using Version = ESerializationVersion; + std::map savedPointers; + const Version version = Version::CURRENT; bool smartPointerSerialization; bool saving; - BinarySerializer(IBinaryWriter * w): CSaverBase(w) - { - saving=true; - smartPointerSerialization = true; - } + BinarySerializer(IBinaryWriter * w); template void registerType(const Base * b = nullptr, const Derived * d = nullptr) @@ -133,36 +134,28 @@ public: return * this; } - template < typename T, typename std::enable_if < std::is_same::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_same_v, int > = 0 > void save(const T &data) { ui8 writ = static_cast(data); save(writ); } - template < typename T, typename std::enable_if < std::is_same >::value, int >::type = 0 > - void save(const T &data) - { - std::vector convData; - std::copy(data.begin(), data.end(), std::back_inserter(convData)); - save(convData); - } - - template < class T, typename std::enable_if < std::is_fundamental::value && !std::is_same::value, int >::type = 0 > + template < class T, typename std::enable_if_t < std::is_fundamental_v && !std::is_same_v, int > = 0 > void save(const T &data) { // save primitive - simply dump binary data to output - this->write(&data,sizeof(data)); + this->write(static_cast(&data), sizeof(data)); } - template < typename T, typename std::enable_if < std::is_enum::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_enum_v, int > = 0 > void save(const T &data) { si32 writ = static_cast(data); *this & writ; } - template < typename T, typename std::enable_if < std::is_array::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_array_v, int > = 0 > void save(const T &data) { ui32 size = std::size(data); @@ -170,20 +163,34 @@ public: *this & data[i]; } - template < typename T, typename std::enable_if < std::is_pointer::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < std::is_pointer_v, int > = 0 > void save(const T &data) { //write if pointer is not nullptr - ui8 hlp = (data!=nullptr); - save(hlp); + bool isNull = (data == nullptr); + save(isNull); //if pointer is nullptr then we don't need anything more... - if(!hlp) + if(data == nullptr) return; + savePointerImpl(data); + } + + template < typename T, typename std::enable_if_t < std::is_base_of_v>, int > = 0 > + void savePointerImpl(const T &data) + { + auto index = data->getId(); + save(index); + } + + template < typename T, typename std::enable_if_t < !std::is_base_of_v>, int > = 0 > + void savePointerImpl(const T &data) + { + typedef typename std::remove_const_t> TObjectType; + if(writer->smartVectorMembersSerialization) { - typedef typename std::remove_const::type>::type TObjectType; typedef typename VectorizedTypeFor::type VType; typedef typename VectorizedIDType::type IDType; @@ -207,7 +214,7 @@ public: { // We might have an object that has multiple inheritance and store it via the non-first base pointer. // Therefore, all pointers need to be normalized to the actual object address. - auto actualPointer = typeList.castToMostDerived(data); + const void * actualPointer = static_cast(data); auto i = savedPointers.find(actualPointer); if(i != savedPointers.end()) { @@ -223,19 +230,19 @@ public: } //write type identifier - ui16 tid = typeList.getTypeID(data); + uint16_t tid = CTypeList::getInstance().getTypeID(data); save(tid); if(!tid) save(*data); //if type is unregistered simply write all data in a standard way else - applier.getApplier(tid)->savePtr(*this, typeList.castToMostDerived(data)); //call serializer specific for our real type + applier.getApplier(tid)->savePtr(*this, static_cast(data)); //call serializer specific for our real type } - template < typename T, typename std::enable_if < is_serializeable::value, int >::type = 0 > + template < typename T, typename std::enable_if_t < is_serializeable::value, int > = 0 > void save(const T &data) { - const_cast(data).serialize(*this, SERIALIZATION_VERSION); + const_cast(data).serialize(*this); } void save(const std::monostate & data) @@ -261,7 +268,7 @@ public: T *internalPtr = data.get(); save(internalPtr); } - template ::value, int >::type = 0> + template , int > = 0> void save(const std::vector &data) { ui32 length = (ui32)data.size(); @@ -305,7 +312,7 @@ public: void save(const std::string &data) { save(ui32(data.length())); - this->write(data.c_str(),(unsigned int)data.size()); + this->write(static_cast(data.data()), data.size()); } template void save(const std::pair &data) @@ -362,7 +369,9 @@ public: ui32 length = data.num_elements(); *this & length; auto shape = data.shape(); - ui32 x = shape[0], y = shape[1], z = shape[2]; + ui32 x = shape[0]; + ui32 y = shape[1]; + ui32 z = shape[2]; *this & x & y & z; for(ui32 i = 0; i < length; i++) save(data.data()[i]); @@ -389,30 +398,4 @@ public: } }; -class DLL_LINKAGE CSaveFile : public IBinaryWriter -{ -public: - BinarySerializer serializer; - - boost::filesystem::path fName; - std::unique_ptr sfile; - - CSaveFile(const boost::filesystem::path &fname); //throws! - ~CSaveFile(); - int write(const void * data, unsigned size) override; - - void openNextFile(const boost::filesystem::path &fname); //throws! - void clear(); - void reportState(vstd::CLoggerBase * out) override; - - void putMagicBytes(const std::string &text); - - template - CSaveFile & operator<<(const T &t) - { - serializer & t; - return * this; - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadFile.cpp b/lib/serializer/CLoadFile.cpp new file mode 100644 index 000000000..ef43b8e27 --- /dev/null +++ b/lib/serializer/CLoadFile.cpp @@ -0,0 +1,100 @@ +/* + * CLoadFile.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CLoadFile.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CLoadFile::CLoadFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion) + : serializer(this) +{ + openNextFile(fname, minimalVersion); +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CLoadFile::~CLoadFile() = default; + +int CLoadFile::read(std::byte * data, unsigned size) +{ + sfile->read(reinterpret_cast(data), size); + return size; +} + +void CLoadFile::openNextFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion) +{ + assert(!serializer.reverseEndianess); + assert(minimalVersion <= ESerializationVersion::CURRENT); + + try + { + fName = fname.string(); + sfile = std::make_unique(fname.c_str(), std::ios::in | std::ios::binary); + sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway + + if(!(*sfile)) + THROW_FORMAT("Error: cannot open to read %s!", fName); + + //we can read + char buffer[4]; + sfile->read(buffer, 4); + if(std::memcmp(buffer, "VCMI", 4) != 0) + THROW_FORMAT("Error: not a VCMI file(%s)!", fName); + + serializer & serializer.version; + if(serializer.version < minimalVersion) + THROW_FORMAT("Error: too old file format (%s)!", fName); + + if(serializer.version > ESerializationVersion::CURRENT) + { + logGlobal->warn("Warning format version mismatch: found %d when current is %d! (file %s)\n", vstd::to_underlying(serializer.version), vstd::to_underlying(ESerializationVersion::CURRENT), fName); + + auto * versionptr = reinterpret_cast(&serializer.version); + std::reverse(versionptr, versionptr + 4); + logGlobal->warn("Version number reversed is %x, checking...", vstd::to_underlying(serializer.version)); + + if(serializer.version == ESerializationVersion::CURRENT) + { + logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string()); + serializer.reverseEndianess = true; + } + else + THROW_FORMAT("Error: too new file format (%s)!", fName); + } + } + catch(...) + { + clear(); //if anything went wrong, we delete file and rethrow + throw; + } +} + +void CLoadFile::reportState(vstd::CLoggerBase * out) +{ + out->debug("CLoadFile"); + if(!!sfile && *sfile) + out->debug("\tOpened %s Position: %d", fName, sfile->tellg()); +} + +void CLoadFile::clear() +{ + sfile = nullptr; + fName.clear(); + serializer.version = ESerializationVersion::NONE; +} + +void CLoadFile::checkMagicBytes(const std::string &text) +{ + std::string loaded = text; + read(reinterpret_cast(loaded.data()), text.length()); + if(loaded != text) + throw std::runtime_error("Magic bytes doesn't match!"); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadFile.h b/lib/serializer/CLoadFile.h new file mode 100644 index 000000000..800872038 --- /dev/null +++ b/lib/serializer/CLoadFile.h @@ -0,0 +1,42 @@ +/* + * CLoadFile.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "BinaryDeserializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CLoadFile : public IBinaryReader +{ +public: + BinaryDeserializer serializer; + + std::string fName; + std::unique_ptr sfile; + + CLoadFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion = ESerializationVersion::CURRENT); //throws! + virtual ~CLoadFile(); + int read(std::byte * data, unsigned size) override; //throws! + + void openNextFile(const boost::filesystem::path & fname, ESerializationVersion minimalVersion); //throws! + void clear(); + void reportState(vstd::CLoggerBase * out) override; + + void checkMagicBytes(const std::string & text); + + template + CLoadFile & operator>>(T &t) + { + serializer & t; + return * this; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadIntegrityValidator.cpp b/lib/serializer/CLoadIntegrityValidator.cpp deleted file mode 100644 index 81b3be830..000000000 --- a/lib/serializer/CLoadIntegrityValidator.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * CLoadIntegrityValidator.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "CLoadIntegrityValidator.h" - -#include "../registerTypes/RegisterTypes.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CLoadIntegrityValidator::CLoadIntegrityValidator(const boost::filesystem::path &primaryFileName, const boost::filesystem::path &controlFileName, int minimalVersion) - : serializer(this), foundDesync(false) -{ - registerTypes(serializer); - primaryFile = std::make_unique(primaryFileName, minimalVersion); - controlFile = std::make_unique(controlFileName, minimalVersion); - - assert(primaryFile->serializer.fileVersion == controlFile->serializer.fileVersion); - serializer.fileVersion = primaryFile->serializer.fileVersion; -} - -int CLoadIntegrityValidator::read( void * data, unsigned size ) -{ - assert(primaryFile); - assert(controlFile); - - if(!size) - return size; - - std::vector controlData(size); - auto ret = primaryFile->read(data, size); - - if(!foundDesync) - { - controlFile->read(controlData.data(), size); - if(std::memcmp(data, controlData.data(), size) != 0) - { - logGlobal->error("Desync found! Position: %d", primaryFile->sfile->tellg()); - foundDesync = true; - //throw std::runtime_error("Savegame dsynchronized!"); - } - } - return ret; -} - -std::unique_ptr CLoadIntegrityValidator::decay() -{ - primaryFile->serializer.loadedPointers = this->serializer.loadedPointers; - primaryFile->serializer.loadedPointersTypes = this->serializer.loadedPointersTypes; - return std::move(primaryFile); -} - -void CLoadIntegrityValidator::checkMagicBytes(const std::string & text) const -{ - assert(primaryFile); - assert(controlFile); - - primaryFile->checkMagicBytes(text); - controlFile->checkMagicBytes(text); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadIntegrityValidator.h b/lib/serializer/CLoadIntegrityValidator.h deleted file mode 100644 index 5adb4f3d3..000000000 --- a/lib/serializer/CLoadIntegrityValidator.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * CLoadIntegrityValidator.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "BinaryDeserializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// Simple byte-to-byte saves comparator -class DLL_LINKAGE CLoadIntegrityValidator - : public IBinaryReader -{ -public: - BinaryDeserializer serializer; - std::unique_ptr primaryFile, controlFile; - bool foundDesync; - - CLoadIntegrityValidator(const boost::filesystem::path &primaryFileName, const boost::filesystem::path &controlFileName, int minimalVersion = SERIALIZATION_VERSION); //throws! - - int read( void * data, unsigned size) override; //throws! - void checkMagicBytes(const std::string & text) const; - - std::unique_ptr decay(); //returns primary file. CLoadIntegrityValidator stops being usable anymore -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CMemorySerializer.cpp b/lib/serializer/CMemorySerializer.cpp index f9d62c619..13f3a59cf 100644 --- a/lib/serializer/CMemorySerializer.cpp +++ b/lib/serializer/CMemorySerializer.cpp @@ -10,33 +10,29 @@ #include "StdInc.h" #include "CMemorySerializer.h" -#include "../registerTypes/RegisterTypes.h" - VCMI_LIB_NAMESPACE_BEGIN -int CMemorySerializer::read(void * data, unsigned size) +int CMemorySerializer::read(std::byte * data, unsigned size) { if(buffer.size() < readPos + size) throw std::runtime_error(boost::str(boost::format("Cannot read past the buffer (accessing index %d, while size is %d)!") % (readPos + size - 1) % buffer.size())); - std::memcpy(data, buffer.data() + readPos, size); + std::copy_n(buffer.data() + readPos, size, data); readPos += size; return size; } -int CMemorySerializer::write(const void * data, unsigned size) +int CMemorySerializer::write(const std::byte * data, unsigned size) { auto oldSize = buffer.size(); //and the pos to write from buffer.resize(oldSize + size); - std::memcpy(buffer.data() + oldSize, data, size); + std::copy_n(data, size, buffer.data() + oldSize); return size; } CMemorySerializer::CMemorySerializer(): iser(this), oser(this), readPos(0) { - registerTypes(iser); - registerTypes(oser); - iser.fileVersion = SERIALIZATION_VERSION; + iser.version = ESerializationVersion::CURRENT; } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CMemorySerializer.h b/lib/serializer/CMemorySerializer.h index 586220724..caaa4cc24 100644 --- a/lib/serializer/CMemorySerializer.h +++ b/lib/serializer/CMemorySerializer.h @@ -18,15 +18,15 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CMemorySerializer : public IBinaryReader, public IBinaryWriter { - std::vector buffer; + std::vector buffer; size_t readPos; //index of the next byte to be read public: BinaryDeserializer iser; BinarySerializer oser; - int read(void * data, unsigned size) override; //throws! - int write(const void * data, unsigned size) override; + int read(std::byte * data, unsigned size) override; //throws! + int write(const std::byte * data, unsigned size) override; CMemorySerializer(); diff --git a/lib/serializer/CSaveFile.cpp b/lib/serializer/CSaveFile.cpp new file mode 100644 index 000000000..d406342b4 --- /dev/null +++ b/lib/serializer/CSaveFile.cpp @@ -0,0 +1,72 @@ +/* + * CSaveFile.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "CSaveFile.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CSaveFile::CSaveFile(const boost::filesystem::path &fname) + : serializer(this) +{ + openNextFile(fname); +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CSaveFile::~CSaveFile() = default; + +int CSaveFile::write(const std::byte * data, unsigned size) +{ + sfile->write(reinterpret_cast(data), size); + return size; +} + +void CSaveFile::openNextFile(const boost::filesystem::path &fname) +{ + fName = fname; + try + { + sfile = std::make_unique(fname.c_str(), std::ios::out | std::ios::binary); + sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway + + if(!(*sfile)) + THROW_FORMAT("Error: cannot open to write %s!", fname); + + sfile->write("VCMI",4); //write magic identifier + serializer & ESerializationVersion::CURRENT; //write format version + } + catch(...) + { + logGlobal->error("Failed to save to %s", fname.string()); + clear(); + throw; + } +} + +void CSaveFile::reportState(vstd::CLoggerBase * out) +{ + out->debug("CSaveFile"); + if(sfile.get() && *sfile) + { + out->debug("\tOpened %s \tPosition: %d", fName, sfile->tellp()); + } +} + +void CSaveFile::clear() +{ + fName.clear(); + sfile = nullptr; +} + +void CSaveFile::putMagicBytes(const std::string &text) +{ + write(reinterpret_cast(text.c_str()), text.length()); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CSaveFile.h b/lib/serializer/CSaveFile.h new file mode 100644 index 000000000..857b7f159 --- /dev/null +++ b/lib/serializer/CSaveFile.h @@ -0,0 +1,42 @@ +/* + * CSaveFile.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "BinarySerializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CSaveFile : public IBinaryWriter +{ +public: + BinarySerializer serializer; + + boost::filesystem::path fName; + std::unique_ptr sfile; + + CSaveFile(const boost::filesystem::path &fname); //throws! + ~CSaveFile(); + int write(const std::byte * data, unsigned size) override; + + void openNextFile(const boost::filesystem::path &fname); //throws! + void clear(); + void reportState(vstd::CLoggerBase * out) override; + + void putMagicBytes(const std::string &text); + + template + CSaveFile & operator<<(const T &t) + { + serializer & t; + return * this; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CSerializer.cpp b/lib/serializer/CSerializer.cpp index 1ce13bfe3..72d54ecd3 100644 --- a/lib/serializer/CSerializer.cpp +++ b/lib/serializer/CSerializer.cpp @@ -14,6 +14,7 @@ #include "../mapping/CMap.h" #include "../CHeroHandler.h" #include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CQuest.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index df7278f00..cc9392031 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,6 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 828; -const ui32 MINIMAL_SERIALIZATION_VERSION = 828; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; @@ -51,16 +49,16 @@ struct VectorizedObjectInfo }; /// Base class for serializers capable of reading or writing data -class DLL_LINKAGE CSerializer +class DLL_LINKAGE CSerializer : boost::noncopyable { - template - static si32 idToNumber(const T &t, typename std::enable_if::value>::type * dummy = nullptr) + template, bool> = true> + static int32_t idToNumber(const Numeric &t) { return t; } - template - static NT idToNumber(const IdentifierBase &t) + template, bool> = true> + static int32_t idToNumber(const IdentifierType &t) { return t.getNum(); } @@ -138,67 +136,53 @@ struct is_serializeable using No = char (&)[2]; template - static Yes test(U * data, S* arg1 = 0, - typename std::enable_ifserialize(*arg1, int(0))) - >::value>::type * = 0); + static Yes test(U * data, S* arg1 = nullptr, typename std::enable_if_tserialize(*arg1))>> * = nullptr); static No test(...); - static const bool value = sizeof(Yes) == sizeof(is_serializeable::test((typename std::remove_reference::type>::type*)0)); + static const bool value = sizeof(Yes) == sizeof(is_serializeable::test((typename std::remove_reference_t>*)nullptr)); }; template //metafunction returning CGObjectInstance if T is its derivate or T elsewise struct VectorizedTypeFor { - using type = typename - //if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else - boost::mpl::identity - >>::type; + using type = std::conditional_t, CGObjectInstance, T>; }; -template + +template <> +struct VectorizedTypeFor +{ + using type = CGHeroInstance; +}; + +template struct VectorizedIDType { - using type = typename - //if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else - boost::mpl::identity - >>>>>>::type; + using type = std::conditional_t, ObjectInstanceID, int32_t>; +}; + +template <> +struct VectorizedIDType +{ + using type = ArtifactInstanceID; +}; + +template <> +struct VectorizedIDType +{ + using type = HeroTypeID; }; /// Base class for deserializers class DLL_LINKAGE IBinaryReader : public virtual CSerializer { public: - virtual int read(void * data, unsigned size) = 0; + virtual int read(std::byte * data, unsigned size) = 0; }; /// Base class for serializers class DLL_LINKAGE IBinaryWriter : public virtual CSerializer { public: - virtual int write(const void * data, unsigned size) = 0; + virtual int write(const std::byte * data, unsigned size) = 0; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CTypeList.cpp b/lib/serializer/CTypeList.cpp index 1c94dcf43..8c75517ca 100644 --- a/lib/serializer/CTypeList.cpp +++ b/lib/serializer/CTypeList.cpp @@ -14,128 +14,9 @@ VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(CTypeList & s); - -CTypeList typeList; - CTypeList::CTypeList() { registerTypes(*this); } -CTypeList::TypeInfoPtr CTypeList::registerType(const std::type_info *type) -{ - if(auto typeDescr = getTypeDescriptor(type, false)) - return typeDescr; //type found, return ptr to structure - - //type not found - add it to the list and return given ID - auto newType = std::make_shared(); - newType->typeID = static_cast(typeInfos.size() + 1); - newType->name = type->name(); - typeInfos[type] = newType; - - return newType; -} - -ui16 CTypeList::getTypeID(const std::type_info *type, bool throws) const -{ - auto descriptor = getTypeDescriptor(type, throws); - if (descriptor == nullptr) - { - return 0; - } - return descriptor->typeID; -} - -CTypeList::TypeInfoPtr CTypeList::getTypeDescriptor(ui16 typeID) const -{ - auto found = std::find_if(typeInfos.begin(), typeInfos.end(), [typeID](const std::pair & p) -> bool - { - return p.second->typeID == typeID; - }); - - if(found != typeInfos.end()) - { - return found->second; - } - - return TypeInfoPtr(); -} - -std::vector CTypeList::castSequence(TypeInfoPtr from, TypeInfoPtr to) const -{ - if(!strcmp(from->name, to->name)) - return std::vector(); - - // Perform a simple BFS in the class hierarchy. - - auto BFS = [&](bool upcast) - { - std::map previous; - std::queue q; - q.push(to); - while(!q.empty()) - { - auto typeNode = q.front(); - q.pop(); - for(auto & weakNode : (upcast ? typeNode->parents : typeNode->children) ) - { - auto nodeBase = weakNode.lock(); - if(!previous.count(nodeBase)) - { - previous[nodeBase] = typeNode; - q.push(nodeBase); - } - } - } - - std::vector ret; - - if(!previous.count(from)) - return ret; - - ret.push_back(from); - TypeInfoPtr ptr = from; - do - { - ptr = previous.at(ptr); - ret.push_back(ptr); - } while(ptr != to); - - return ret; - }; - - // Try looking both up and down. - auto ret = BFS(true); - if(ret.empty()) - ret = BFS(false); - - if(ret.empty()) - THROW_FORMAT("Cannot find relation between types %s and %s. Were they (and all classes between them) properly registered?", from->name % to->name); - - return ret; -} - -std::vector CTypeList::castSequence(const std::type_info *from, const std::type_info *to) const -{ - //This additional if is needed because getTypeDescriptor might fail if type is not registered - // (and if casting is not needed, then registereing should no be required) - if(!strcmp(from->name(), to->name())) - return std::vector(); - - return castSequence(getTypeDescriptor(from), getTypeDescriptor(to)); -} - -CTypeList::TypeInfoPtr CTypeList::getTypeDescriptor(const std::type_info *type, bool throws) const -{ - auto i = typeInfos.find(type); - if(i != typeInfos.end()) - return i->second; //type found, return ptr to structure - - if(!throws) - return nullptr; - - THROW_FORMAT("Cannot find type descriptor for type %s. Was it registered?", type->name()); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CTypeList.h b/lib/serializer/CTypeList.h index fc5b7c0ac..df0eec7bc 100644 --- a/lib/serializer/CTypeList.h +++ b/lib/serializer/CTypeList.h @@ -9,199 +9,72 @@ */ #pragma once -#include "CSerializer.h" - VCMI_LIB_NAMESPACE_BEGIN -struct IPointerCaster -{ - virtual std::any castRawPtr(const std::any &ptr) const = 0; // takes From*, returns To* - virtual std::any castSharedPtr(const std::any &ptr) const = 0; // takes std::shared_ptr, performs dynamic cast, returns std::shared_ptr - virtual std::any castWeakPtr(const std::any &ptr) const = 0; // takes std::weak_ptr, performs dynamic cast, returns std::weak_ptr. The object under poitner must live. - //virtual std::any castUniquePtr(const std::any &ptr) const = 0; // takes std::unique_ptr, performs dynamic cast, returns std::unique_ptr - virtual ~IPointerCaster() = default; -}; - -template -struct PointerCaster : IPointerCaster -{ - virtual std::any castRawPtr(const std::any &ptr) const override // takes void* pointing to From object, performs dynamic cast, returns void* pointing to To object - { - From * from = (From*)std::any_cast(ptr); - To * ret = static_cast(from); - return (void*)ret; - } - - // Helper function performing casts between smart pointers - template - std::any castSmartPtr(const std::any &ptr) const - { - try - { - auto from = std::any_cast(ptr); - auto ret = std::static_pointer_cast(from); - return ret; - } - catch(std::exception &e) - { - THROW_FORMAT("Failed cast %s -> %s. Given argument was %s. Error message: %s", typeid(From).name() % typeid(To).name() % ptr.type().name() % e.what()); - } - } - - virtual std::any castSharedPtr(const std::any &ptr) const override - { - return castSmartPtr>(ptr); - } - virtual std::any castWeakPtr(const std::any &ptr) const override - { - auto from = std::any_cast>(ptr); - return castSmartPtr>(from.lock()); - } -}; - /// Class that implements basic reflection-like mechanisms /// For every type registered via registerType() generates inheritance tree /// Rarely used directly - usually used as part of CApplier -class DLL_LINKAGE CTypeList: public boost::noncopyable +class CTypeList { -//public: - struct TypeDescriptor; - using TypeInfoPtr = std::shared_ptr; - using WeakTypeInfoPtr = std::weak_ptr; - struct TypeDescriptor - { - ui16 typeID; - const char *name; - std::vector children, parents; - }; - using TMutex = boost::shared_mutex; - using TUniqueLock = boost::unique_lock; - using TSharedLock = boost::shared_lock; + std::map typeInfos; -private: - mutable TMutex mx; - - std::map typeInfos; - std::map, std::unique_ptr> casters; //for each pair we provide a caster (each registered relations creates a single entry here) - - /// Returns sequence of types starting from "from" and ending on "to". Every next type is derived from the previous. - /// Throws if there is no link registered. - std::vector castSequence(TypeInfoPtr from, TypeInfoPtr to) const; - std::vector castSequence(const std::type_info *from, const std::type_info *to) const; - - template - std::any castHelper(std::any inputPtr, const std::type_info *fromArg, const std::type_info *toArg) const - { - TSharedLock lock(mx); - auto typesSequence = castSequence(fromArg, toArg); - - std::any ptr = inputPtr; - for(int i = 0; i < static_cast(typesSequence.size()) - 1; i++) - { - auto &from = typesSequence[i]; - auto &to = typesSequence[i + 1]; - auto castingPair = std::make_pair(from, to); - if(!casters.count(castingPair)) - THROW_FORMAT("Cannot find caster for conversion %s -> %s which is needed to cast %s -> %s", from->name % to->name % fromArg->name() % toArg->name()); - - const auto & caster = casters.at(castingPair); - ptr = (*caster.*CastingFunction)(ptr); //Why does unique_ptr not have operator->* ..? - } - - return ptr; - } - CTypeList & operator=(CTypeList &) = delete; - - TypeInfoPtr getTypeDescriptor(const std::type_info *type, bool throws = true) const; //if not throws, failure returns nullptr - TypeInfoPtr registerType(const std::type_info *type); - -public: - - CTypeList(); - - template - void registerType(const Base * b = nullptr, const Derived * d = nullptr) - { - TUniqueLock lock(mx); - static_assert(std::is_base_of::value, "First registerType template parameter needs to ba a base class of the second one."); - static_assert(std::has_virtual_destructor::value, "Base class needs to have a virtual destructor."); - static_assert(!std::is_same::value, "Parameters of registerTypes should be two different types."); - auto bt = getTypeInfo(b); - auto dt = getTypeInfo(d); //obtain std::type_info - auto bti = registerType(bt); - auto dti = registerType(dt); //obtain our TypeDescriptor - - // register the relation between classes - bti->children.push_back(dti); - dti->parents.push_back(bti); - casters[std::make_pair(bti, dti)] = std::make_unique>(); - casters[std::make_pair(dti, bti)] = std::make_unique>(); - } - - ui16 getTypeID(const std::type_info *type, bool throws = false) const; + DLL_LINKAGE CTypeList(); template - ui16 getTypeID(const T * t = nullptr, bool throws = false) const - { - return getTypeID(getTypeInfo(t), throws); - } - - TypeInfoPtr getTypeDescriptor(ui16 typeID) const; - - template - void * castToMostDerived(const TInput * inputPtr) const - { - const auto & baseType = typeid(typename std::remove_cv::type); - auto derivedType = getTypeInfo(inputPtr); - - if (strcmp(baseType.name(), derivedType->name()) == 0) - { - return const_cast(reinterpret_cast(inputPtr)); - } - - return std::any_cast(castHelper<&IPointerCaster::castRawPtr>( - const_cast(reinterpret_cast(inputPtr)), &baseType, - derivedType)); - } - - template - std::any castSharedToMostDerived(const std::shared_ptr inputPtr) const - { - const auto & baseType = typeid(typename std::remove_cv::type); - auto derivedType = getTypeInfo(inputPtr.get()); - - if (!strcmp(baseType.name(), derivedType->name())) - return inputPtr; - - return castHelper<&IPointerCaster::castSharedPtr>(inputPtr, &baseType, derivedType); - } - - void * castRaw(void *inputPtr, const std::type_info *from, const std::type_info *to) const - { - return std::any_cast(castHelper<&IPointerCaster::castRawPtr>(inputPtr, from, to)); - } - std::any castShared(std::any inputPtr, const std::type_info *from, const std::type_info *to) const - { - return castHelper<&IPointerCaster::castSharedPtr>(inputPtr, from, to); - } - - template const std::type_info * getTypeInfo(const T * t = nullptr) const + const std::type_info & getTypeInfo(const T * t = nullptr) const { if(t) - return &typeid(*t); + return typeid(*t); else - return &typeid(T); + return typeid(T); + } + +public: + static CTypeList & getInstance() + { + static CTypeList registry; + return registry; + } + + template + void registerType() + { + registerType(); + registerType(); + } + + template + void registerType() + { + const std::type_info & typeInfo = typeid(T); + + if (typeInfos.count(typeInfo.name()) != 0) + return; + + typeInfos[typeInfo.name()] = typeInfos.size() + 1; + } + + template + uint16_t getTypeID(T * typePtr) + { + static_assert(!std::is_pointer_v, "CTypeList does not supports pointers!"); + static_assert(!std::is_reference_v, "CTypeList does not supports references!"); + + const std::type_info & typeInfo = getTypeInfo(typePtr); + + if (typeInfos.count(typeInfo.name()) == 0) + return 0; + + return typeInfos.at(typeInfo.name()); } }; -extern DLL_LINKAGE CTypeList typeList; - /// Wrapper over CTypeList. Allows execution of templated class T for any type /// that was resgistered for this applier template class CApplier : boost::noncopyable { - std::map> apps; + std::map> apps; template void addApplier(ui16 ID) @@ -224,9 +97,8 @@ public: template void registerType(const Base * b = nullptr, const Derived * d = nullptr) { - typeList.registerType(b, d); - addApplier(typeList.getTypeID(b)); - addApplier(typeList.getTypeID(d)); + addApplier(CTypeList::getInstance().getTypeID(nullptr)); + addApplier(CTypeList::getInstance().getTypeID(nullptr)); } }; diff --git a/lib/serializer/Cast.h b/lib/serializer/Cast.h index ebc7957af..7525f6d85 100644 --- a/lib/serializer/Cast.h +++ b/lib/serializer/Cast.h @@ -9,58 +9,18 @@ */ #pragma once -#include -#include -#include "CTypeList.h" - VCMI_LIB_NAMESPACE_BEGIN template inline const T * dynamic_ptr_cast(const F * ptr) { -#ifndef VCMI_APPLE return dynamic_cast(ptr); -#else - if(!strcmp(typeid(*ptr).name(), typeid(T).name())) - { - return static_cast(ptr); - } - try - { - auto * sourceTypeInfo = typeList.getTypeInfo(ptr); - auto * targetTypeInfo = &typeid(typename std::remove_const::type>::type); - typeList.castRaw((void *)ptr, sourceTypeInfo, targetTypeInfo); - } - catch(...) - { - return nullptr; - } - return static_cast(ptr); -#endif } template inline T * dynamic_ptr_cast(F * ptr) { -#ifndef VCMI_APPLE return dynamic_cast(ptr); -#else - if(!strcmp(typeid(*ptr).name(), typeid(T).name())) - { - return static_cast(ptr); - } - try - { - auto * sourceTypeInfo = typeList.getTypeInfo(ptr); - auto * targetTypeInfo = &typeid(typename std::remove_const::type>::type); - typeList.castRaw((void *)ptr, sourceTypeInfo, targetTypeInfo); - } - catch(...) - { - return nullptr; - } - return static_cast(ptr); -#endif } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index 3029aaf91..f76b77262 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -10,343 +10,167 @@ #include "StdInc.h" #include "Connection.h" -#include "../registerTypes/RegisterTypes.h" -#include "../mapping/CMapHeader.h" +#include "BinaryDeserializer.h" +#include "BinarySerializer.h" -#include +#include "../gameState/CGameState.h" +#include "../networkPacks/NetPacksBase.h" +#include "../network/NetworkInterface.h" VCMI_LIB_NAMESPACE_BEGIN -using namespace boost; -using namespace boost::asio::ip; - -struct ConnectionBuffers +class DLL_LINKAGE ConnectionPackWriter final : public IBinaryWriter { - boost::asio::streambuf readBuffer; - boost::asio::streambuf writeBuffer; +public: + std::vector buffer; + + int write(const std::byte * data, unsigned size) final; }; -void CConnection::init() +class DLL_LINKAGE ConnectionPackReader final : public IBinaryReader { - enableBufferedWrite = false; - enableBufferedRead = false; - connectionBuffers = std::make_unique(); +public: + const std::vector * buffer; + size_t position; - socket->set_option(boost::asio::ip::tcp::no_delay(true)); - try - { - socket->set_option(boost::asio::socket_base::send_buffer_size(4194304)); - socket->set_option(boost::asio::socket_base::receive_buffer_size(4194304)); - } - catch (const boost::system::system_error & e) - { - logNetwork->error("error setting socket option: %s", e.what()); - } + int read(std::byte * data, unsigned size) final; +}; - enableSmartPointerSerialization(); - disableStackSendingByID(); - registerTypes(iser); - registerTypes(oser); -#ifndef VCMI_ENDIAN_BIG - myEndianess = true; -#else - myEndianess = false; -#endif - connected = true; - std::string pom; - //we got connection - oser & std::string("Aiya!\n") & name & uuid & myEndianess; //identify ourselves - iser & pom & pom & contactUuid & contactEndianess; - logNetwork->info("Established connection with %s. UUID: %s", pom, contactUuid); - mutexRead = std::make_shared(); - mutexWrite = std::make_shared(); - - iser.fileVersion = SERIALIZATION_VERSION; -} - -CConnection::CConnection(const std::string & host, ui16 port, std::string Name, std::string UUID): - io_service(std::make_shared()), - iser(this), - oser(this), - name(std::move(Name)), - uuid(std::move(UUID)) +int ConnectionPackWriter::write(const std::byte * data, unsigned size) { - int i = 0; - boost::system::error_code error = asio::error::host_not_found; - socket = std::make_shared(*io_service); - - tcp::resolver resolver(*io_service); - tcp::resolver::iterator end; - tcp::resolver::iterator pom; - tcp::resolver::iterator endpoint_iterator = resolver.resolve(tcp::resolver::query(host, std::to_string(port)), error); - if(error) - { - logNetwork->error("Problem with resolving: \n%s", error.message()); - throw std::runtime_error("Problem with resolving"); - } - pom = endpoint_iterator; - if(pom != end) - logNetwork->info("Found endpoints:"); - else - { - logNetwork->error("Critical problem: No endpoints found!"); - throw std::runtime_error("No endpoints found!"); - } - while(pom != end) - { - logNetwork->info("\t%d:%s", i, (boost::asio::ip::tcp::endpoint&)*pom); - pom++; - } - i=0; - while(endpoint_iterator != end) - { - logNetwork->info("Trying connection to %s(%d)", (boost::asio::ip::tcp::endpoint&)*endpoint_iterator, i++); - socket->connect(*endpoint_iterator, error); - if(!error) - { - init(); - return; - } - else - { - throw std::runtime_error("Failed to connect!"); - } - endpoint_iterator++; - } + buffer.insert(buffer.end(), data, data + size); + return size; } -CConnection::CConnection(std::shared_ptr Socket, std::string Name, std::string UUID): - iser(this), - oser(this), - socket(std::move(Socket)), - name(std::move(Name)), - uuid(std::move(UUID)) +int ConnectionPackReader::read(std::byte * data, unsigned size) { - init(); + if (position + size > buffer->size()) + throw std::runtime_error("End of file reached when reading received network pack!"); + + std::copy_n(buffer->begin() + position, size, data); + position += size; + return size; } -CConnection::CConnection(const std::shared_ptr & acceptor, - const std::shared_ptr & io_service, - std::string Name, - std::string UUID): - io_service(io_service), - iser(this), - oser(this), - name(std::move(Name)), - uuid(std::move(UUID)) + +CConnection::CConnection(std::weak_ptr networkConnection) + : networkConnection(networkConnection) + , packReader(std::make_unique()) + , packWriter(std::make_unique()) + , deserializer(std::make_unique(packReader.get())) + , serializer(std::make_unique(packWriter.get())) + , connectionID(-1) { - boost::system::error_code error = asio::error::host_not_found; - socket = std::make_shared(*io_service); - acceptor->accept(*socket,error); - if (error) - { - logNetwork->error("Error on accepting: %s", error.message()); - socket.reset(); - throw std::runtime_error("Can't establish connection :("); - } - init(); + assert(networkConnection.lock() != nullptr); + + enterLobbyConnectionMode(); + deserializer->version = ESerializationVersion::CURRENT; } -void CConnection::flushBuffers() -{ - if(!enableBufferedWrite) - return; - - try - { - asio::write(*socket, connectionBuffers->writeBuffer); - } - catch(...) - { - //connection has been lost - connected = false; - throw; - } - - enableBufferedWrite = false; -} - -int CConnection::write(const void * data, unsigned size) -{ - try - { - if(enableBufferedWrite) - { - std::ostream ostream(&connectionBuffers->writeBuffer); - - ostream.write(static_cast(data), size); - - return size; - } - - int ret = static_cast(asio::write(*socket, asio::const_buffers_1(asio::const_buffer(data, size)))); - return ret; - } - catch(...) - { - //connection has been lost - connected = false; - throw; - } -} - -int CConnection::read(void * data, unsigned size) -{ - try - { - if(enableBufferedRead) - { - auto available = connectionBuffers->readBuffer.size(); - - while(available < size) - { - auto bytesRead = socket->read_some(connectionBuffers->readBuffer.prepare(1024)); - connectionBuffers->readBuffer.commit(bytesRead); - available = connectionBuffers->readBuffer.size(); - } - - std::istream istream(&connectionBuffers->readBuffer); - - istream.read(static_cast(data), size); - - return size; - } - - int ret = static_cast(asio::read(*socket,asio::mutable_buffers_1(asio::mutable_buffer(data,size)))); - return ret; - } - catch(...) - { - //connection has been lost - connected = false; - throw; - } -} - -CConnection::~CConnection() -{ - if(handler) - handler->join(); - - close(); -} - -template -CConnection & CConnection::operator&(const T &t) { -// throw std::exception(); -//XXX this is temporaly ? solution to fix gcc (4.3.3, other?) compilation -// problem for more details contact t0@czlug.icis.pcz.pl or impono@gmail.com -// do not remove this exception it shoudnt be called - return *this; -} - -void CConnection::close() -{ - if(socket) - { - socket->close(); - socket.reset(); - } -} - -bool CConnection::isOpen() const -{ - return socket && connected; -} - -void CConnection::reportState(vstd::CLoggerBase * out) -{ - out->debug("CConnection"); - if(socket && socket->is_open()) - { - out->debug("\tWe have an open and valid socket"); - out->debug("\t %d bytes awaiting", socket->available()); - } -} - -CPack * CConnection::retrievePack() -{ - enableBufferedRead = true; - - CPack * pack = nullptr; - boost::unique_lock lock(*mutexRead); - iser & pack; - logNetwork->trace("Received CPack of type %s", (pack ? typeid(*pack).name() : "nullptr")); - if(pack == nullptr) - { - logNetwork->error("Received a nullptr CPack! You should check whether client and server ABI matches."); - } - else - { - pack->c = this->shared_from_this(); - } - - enableBufferedRead = false; - - return pack; -} +CConnection::~CConnection() = default; void CConnection::sendPack(const CPack * pack) { - boost::unique_lock lock(*mutexWrite); + boost::mutex::scoped_lock lock(writeMutex); + + auto connectionPtr = networkConnection.lock(); + + if (!connectionPtr) + throw std::runtime_error("Attempt to send packet on a closed connection!"); + + *serializer & pack; + logNetwork->trace("Sending a pack of type %s", typeid(*pack).name()); - enableBufferedWrite = true; + connectionPtr->sendPacket(packWriter->buffer); + packWriter->buffer.clear(); +} - oser & pack; +CPack * CConnection::retrievePack(const std::vector & data) +{ + CPack * result; - flushBuffers(); + packReader->buffer = &data; + packReader->position = 0; + + *deserializer & result; + + if (result == nullptr) + throw std::runtime_error("Failed to retrieve pack!"); + + if (packReader->position != data.size()) + throw std::runtime_error("Failed to retrieve pack! Not all data has been read!"); + + logNetwork->trace("Received CPack of type %s", typeid(*result).name()); + return result; +} + +bool CConnection::isMyConnection(const std::shared_ptr & otherConnection) const +{ + return otherConnection != nullptr && networkConnection.lock() == otherConnection; +} + +std::shared_ptr CConnection::getConnection() +{ + return networkConnection.lock(); } void CConnection::disableStackSendingByID() { - CSerializer::sendStackInstanceByIds = false; + packReader->sendStackInstanceByIds = false; + packWriter->sendStackInstanceByIds = false; } void CConnection::enableStackSendingByID() { - CSerializer::sendStackInstanceByIds = true; -} - -void CConnection::disableSmartPointerSerialization() -{ - iser.smartPointerSerialization = oser.smartPointerSerialization = false; -} - -void CConnection::enableSmartPointerSerialization() -{ - iser.smartPointerSerialization = oser.smartPointerSerialization = true; + packReader->sendStackInstanceByIds = true; + packWriter->sendStackInstanceByIds = true; } void CConnection::enterLobbyConnectionMode() { - iser.loadedPointers.clear(); - oser.savedPointers.clear(); + deserializer->loadedPointers.clear(); + serializer->savedPointers.clear(); disableSmartVectorMemberSerialization(); disableSmartPointerSerialization(); + disableStackSendingByID(); +} + +void CConnection::setCallback(IGameCallback * cb) +{ + deserializer->cb = cb; } void CConnection::enterGameplayConnectionMode(CGameState * gs) { enableStackSendingByID(); disableSmartPointerSerialization(); - addStdVecItems(gs); + + setCallback(gs->callback); + enableSmartVectorMemberSerializatoin(gs); +} + +void CConnection::disableSmartPointerSerialization() +{ + deserializer->smartPointerSerialization = false; + serializer->smartPointerSerialization = false; +} + +void CConnection::enableSmartPointerSerialization() +{ + deserializer->smartPointerSerialization = true; + serializer->smartPointerSerialization = true; } void CConnection::disableSmartVectorMemberSerialization() { - CSerializer::smartVectorMembersSerialization = false; + packReader->smartVectorMembersSerialization = false; + packWriter->smartVectorMembersSerialization = false; } -void CConnection::enableSmartVectorMemberSerializatoin() +void CConnection::enableSmartVectorMemberSerializatoin(CGameState * gs) { - CSerializer::smartVectorMembersSerialization = true; -} - -std::string CConnection::toString() const -{ - boost::format fmt("Connection with %s (ID: %d UUID: %s)"); - fmt % name % connectionID % uuid; - return fmt.str(); + packWriter->addStdVecItems(gs); + packReader->addStdVecItems(gs); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/Connection.h b/lib/serializer/Connection.h index 518223246..68d23c34e 100644 --- a/lib/serializer/Connection.h +++ b/lib/serializer/Connection.h @@ -9,123 +9,54 @@ */ #pragma once -#include "BinaryDeserializer.h" -#include "BinarySerializer.h" - -#if BOOST_VERSION >= 107000 // Boost version >= 1.70 -#include -using TSocket = boost::asio::basic_stream_socket; -using TAcceptor = boost::asio::basic_socket_acceptor; -#else -namespace boost -{ - namespace asio - { - namespace ip - { - class tcp; - } - -#if BOOST_VERSION >= 106600 // Boost version >= 1.66 - class io_context; - typedef io_context io_service; -#else - class io_service; -#endif - - template class stream_socket_service; - template - class basic_stream_socket; - - template class socket_acceptor_service; - template - class basic_socket_acceptor; - } - class mutex; -} - -typedef boost::asio::basic_stream_socket < boost::asio::ip::tcp , boost::asio::stream_socket_service > TSocket; -typedef boost::asio::basic_socket_acceptor > TAcceptor; -#endif - - VCMI_LIB_NAMESPACE_BEGIN +class BinaryDeserializer; +class BinarySerializer; struct CPack; -struct ConnectionBuffers; +class INetworkConnection; +class ConnectionPackReader; +class ConnectionPackWriter; +class CGameState; +class IGameCallback; -/// Main class for network communication -/// Allows establishing connection and bidirectional read-write -class DLL_LINKAGE CConnection - : public IBinaryReader, public IBinaryWriter, public std::enable_shared_from_this +/// Wrapper class for game connection +/// Handles serialization and deserialization of data received from network +class DLL_LINKAGE CConnection : boost::noncopyable { - void init(); - void reportState(vstd::CLoggerBase * out) override; + /// Non-owning pointer to underlying connection + std::weak_ptr networkConnection; - int write(const void * data, unsigned size) override; - int read(void * data, unsigned size) override; - void flushBuffers(); + std::unique_ptr packReader; + std::unique_ptr packWriter; + std::unique_ptr deserializer; + std::unique_ptr serializer; - std::shared_ptr io_service; //can be empty if connection made from socket - - bool enableBufferedWrite; - bool enableBufferedRead; - std::unique_ptr connectionBuffers; - -public: - BinaryDeserializer iser; - BinarySerializer oser; - - std::shared_ptr mutexRead; - std::shared_ptr mutexWrite; - std::shared_ptr socket; - bool connected; - bool myEndianess, contactEndianess; //true if little endian, if endianness is different we'll have to revert received multi-byte vars - std::string contactUuid; - std::string name; //who uses this connection - std::string uuid; - - int connectionID; - std::shared_ptr handler; - - CConnection(const std::string & host, ui16 port, std::string Name, std::string UUID); - CConnection(const std::shared_ptr & acceptor, const std::shared_ptr & Io_service, std::string Name, std::string UUID); - CConnection(std::shared_ptr Socket, std::string Name, std::string UUID); //use immediately after accepting connection into socket - - void close(); - bool isOpen() const; - template - CConnection &operator&(const T&); - virtual ~CConnection(); - - CPack * retrievePack(); - void sendPack(const CPack * pack); + boost::mutex writeMutex; void disableStackSendingByID(); void enableStackSendingByID(); void disableSmartPointerSerialization(); void enableSmartPointerSerialization(); void disableSmartVectorMemberSerialization(); - void enableSmartVectorMemberSerializatoin(); + void enableSmartVectorMemberSerializatoin(CGameState * gs); + +public: + bool isMyConnection(const std::shared_ptr & otherConnection) const; + std::shared_ptr getConnection(); + + std::string uuid; + int connectionID; + + explicit CConnection(std::weak_ptr networkConnection); + ~CConnection(); + + void sendPack(const CPack * pack); + CPack * retrievePack(const std::vector & data); void enterLobbyConnectionMode(); + void setCallback(IGameCallback * cb); void enterGameplayConnectionMode(CGameState * gs); - - std::string toString() const; - - template - CConnection & operator>>(T &t) - { - iser & t; - return * this; - } - - template - CConnection & operator<<(const T &t) - { - oser & t; - return * this; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/ESerializationVersion.h b/lib/serializer/ESerializationVersion.h new file mode 100644 index 000000000..12436b1c3 --- /dev/null +++ b/lib/serializer/ESerializationVersion.h @@ -0,0 +1,42 @@ +/* + * ESerializationVersion.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +/// This enumeration controls save compatibility support. +/// - 'MINIMAL' represents the oldest supported version counter. A saved game can be loaded if its version is at least 'MINIMAL'. +/// - 'CURRENT' represents the current save version. Saved games are created using the 'CURRENT' version. +/// +/// To make a save-breaking change: +/// - change 'MINIMAL' to a value higher than 'CURRENT' +/// - remove all keys in enumeration between 'MINIMAL' and 'CURRENT' as well as all their usage (will be detected by compiler) +/// - change 'CURRENT' to 'CURRENT = MINIMAL' +/// +/// To make a non-breaking change: +/// - add new enumeration value before 'CURRENT' +/// - change 'CURRENT' to 'CURRENT = NEW_TEST_KEY'. +/// +/// To check for version in serialize() call use form +/// if (h.version >= Handler::Version::NEW_TEST_KEY) +/// h & newKey; // loading/saving save of a new version +/// else +/// newKey = saneDefaultValue; // loading of old save +enum class ESerializationVersion : int32_t +{ + NONE = 0, + + MINIMAL = 831, + RELEASE_143, // 832 +text container in campaigns, +starting hero in RMG options + HAS_EXTRA_OPTIONS, // 833 +extra options struct as part of startinfo + DESTROYED_OBJECTS, // 834 +list of objects destroyed by player + CAMPAIGN_MAP_TRANSLATIONS, // 835 +campaigns include translations for its maps + JSON_FLAGS, // 836 json uses new format for flags + + CURRENT = JSON_FLAGS +}; diff --git a/lib/serializer/ILICReader.cpp b/lib/serializer/ILICReader.cpp deleted file mode 100644 index a8bcc3793..000000000 --- a/lib/serializer/ILICReader.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * JsonTreeSerializer.cpp, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#include "StdInc.h" -#include "ILICReader.h" - -#include "../JsonNode.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -void ILICReader::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, bool val, std::vector & value) const -{ - for(const auto & index : part.Vector()) - { - const std::string & identifier = index.String(); - const std::string type = typeid(decltype(this)).name(); - - const si32 rawId = decoder(identifier); - if(rawId >= 0) - { - if(rawId < value.size()) - value[rawId] = val; - else - logGlobal->error("%s::serializeLIC: id out of bounds %d", type, rawId); - } - } -} - -void ILICReader::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const -{ - for(const auto & index : part.Vector()) - { - const std::string & identifier = index.String(); - - const si32 rawId = decoder(identifier); - if(rawId != -1) - value.insert(rawId); - } -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/ILICReader.h b/lib/serializer/ILICReader.h deleted file mode 100644 index 83d16b10f..000000000 --- a/lib/serializer/ILICReader.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * ILICReader.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -#include "JsonTreeSerializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE ILICReader -{ -protected: - void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, bool val, std::vector & value) const; - void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const; -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonDeserializer.cpp b/lib/serializer/JsonDeserializer.cpp index 0c933aee2..87cda93d6 100644 --- a/lib/serializer/JsonDeserializer.cpp +++ b/lib/serializer/JsonDeserializer.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" #include "JsonDeserializer.h" -#include "../JsonNode.h" - #include VCMI_LIB_NAMESPACE_BEGIN @@ -45,7 +43,7 @@ void JsonDeserializer::serializeInternal(const std::string & fieldName, si32 & v if(rawId < 0) //may be, user has installed the mod into another directory... { auto internalId = vstd::splitStringToPair(identifier, ':').second; - auto currentScope = getCurrent().meta; + auto currentScope = getCurrent().getModScope(); auto actualId = currentScope.length() > 0 ? currentScope + ":" + internalId : internalId; rawId = decoder(actualId); @@ -131,75 +129,11 @@ void JsonDeserializer::serializeInternal(int64_t & value) value = currentObject->Integer(); } -void JsonDeserializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonDeserializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - const JsonNode & field = currentObject->operator[](fieldName); - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty() && allOf.Vector().empty()) - { - //permissive mode - value = standard; - } - else - { - //restrictive mode - value.clear(); - value.resize(standard.size(), false); - - readLICPart(anyOf, decoder, true, value); - readLICPart(allOf, decoder, true, value); - } - - readLICPart(noneOf, decoder, false, value); -} - -void JsonDeserializer::serializeLIC(const std::string & fieldName, LIC & value) -{ - const JsonNode & field = currentObject->operator[](fieldName); - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty()) - { - //permissive mode - value.any = value.standard; - } - else - { - //restrictive mode - value.any.clear(); - value.any.resize(value.standard.size(), false); - - readLICPart(anyOf, value.decoder, true, value.any); - } - - readLICPart(allOf, value.decoder, true, value.all); - readLICPart(noneOf, value.decoder, true, value.none); - - //remove any banned from allowed and required - for(si32 idx = 0; idx < value.none.size(); idx++) - { - if(value.none[idx]) - { - value.all[idx] = false; - value.any[idx] = false; - } - } - - //add all required to allowed - for(si32 idx = 0; idx < value.all.size(); idx++) - { - if(value.all[idx]) - { - value.any[idx] = true; - } - } + LICSet lic(standard, decoder, encoder); + serializeLIC(fieldName, lic); + value = lic.any; } void JsonDeserializer::serializeLIC(const std::string & fieldName, LICSet & value) diff --git a/lib/serializer/JsonDeserializer.h b/lib/serializer/JsonDeserializer.h index 9a4f52c7f..9a3aab5ee 100644 --- a/lib/serializer/JsonDeserializer.h +++ b/lib/serializer/JsonDeserializer.h @@ -9,18 +9,16 @@ */ #pragma once -#include "ILICReader.h" #include "JsonTreeSerializer.h" VCMI_LIB_NAMESPACE_BEGIN -class DLL_LINKAGE JsonDeserializer: public JsonTreeSerializer, public ILICReader +class DLL_LINKAGE JsonDeserializer: public JsonTreeSerializer { public: JsonDeserializer(const IInstanceResolver * instanceResolver_, const JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; diff --git a/lib/serializer/JsonSerializeFormat.cpp b/lib/serializer/JsonSerializeFormat.cpp index aff5c8eb9..e258d5b83 100644 --- a/lib/serializer/JsonSerializeFormat.cpp +++ b/lib/serializer/JsonSerializeFormat.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" #include "JsonSerializeFormat.h" -#include "../JsonNode.h" - VCMI_LIB_NAMESPACE_BEGIN //JsonSerializeHelper @@ -91,17 +89,6 @@ size_t JsonArraySerializer::size() const return thisNode->Vector().size(); } -//JsonSerializeFormat::LIC -JsonSerializeFormat::LIC::LIC(const std::vector & Standard, TDecoder Decoder, TEncoder Encoder): - standard(Standard), - decoder(std::move(Decoder)), - encoder(std::move(Encoder)) -{ - any.resize(standard.size(), false); - all.resize(standard.size(), false); - none.resize(standard.size(), false); -} - JsonSerializeFormat::LICSet::LICSet(const std::set & Standard, TDecoder Decoder, TEncoder Encoder): standard(Standard), decoder(std::move(Decoder)), @@ -140,4 +127,23 @@ void JsonSerializeFormat::serializeBool(const std::string & fieldName, bool & va serializeBool(fieldName, value, true, false, defaultValue); } +void JsonSerializeFormat::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const +{ + for(const auto & index : part.Vector()) + { + const std::string & identifier = index.String(); + + try + { + const si32 rawId = decoder(identifier); + value.insert(rawId); + } + catch (const IdentifierResolutionException & e) + { + // downgrade exception to warning (printed as part of exception generation + // this is usually caused by loading allowed heroes / artifacts list from vmap's + } + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index c5b274005..e5d16021d 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -9,7 +9,8 @@ */ #pragma once -#include "../JsonNode.h" +#include "../constants/IdentifierBase.h" +#include "../json/JsonNode.h" #include "../modding/IdentifierStorage.h" #include "../modding/ModScope.h" #include "../VCMI_Lib.h" @@ -137,18 +138,6 @@ public: ///may assume that object index is valid using TEncoder = std::function; - using TSerialize = std::function; - - struct LIC - { - LIC(const std::vector & Standard, TDecoder Decoder, TEncoder Encoder); - - const std::vector & standard; - const TDecoder decoder; - const TEncoder encoder; - std::vector all, any, none; - }; - struct LICSet { LICSet(const std::set & Standard, TDecoder Decoder, TEncoder Encoder); @@ -156,7 +145,9 @@ public: const std::set & standard; const TDecoder decoder; const TEncoder encoder; - std::set all, any, none; + std::set all; + std::set any; + std::set none; }; const bool saving; @@ -211,13 +202,28 @@ public: * @param value target value, must be resized properly * */ - virtual void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) = 0; + virtual void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) = 0; - /** @brief Complete serialization of Logical identifier condition - */ - virtual void serializeLIC(const std::string & fieldName, LIC & value) = 0; + template + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) + { + std::set standardInt; + std::set valueInt; - /** @brief Complete serialization of Logical identifier condition. (Special version) + for (auto entry : standard) + standardInt.insert(entry.getNum()); + + for (auto entry : value) + valueInt.insert(entry.getNum()); + + serializeLIC(fieldName, decoder, encoder, standardInt, valueInt); + + value.clear(); + for (auto entry : valueInt) + value.insert(T(entry)); + } + + /** @brief Complete serialization of Logical identifier condition. * Assumes that all values are allowed by default, and standard contains them */ virtual void serializeLIC(const std::string & fieldName, LICSet & value) = 0; @@ -295,14 +301,16 @@ public: } ///si32-convertible identifier <-> Json string - template - void serializeId(const std::string & fieldName, T & value, const U & defaultValue) + template + void serializeId(const std::string & fieldName, IdentifierType & value, const IdentifierTypeBase & defaultValue = IdentifierType::NONE) { + static_assert(std::is_base_of_v, "This method can only serialize Identifier classes!"); + if (saving) { if (value != defaultValue) { - std::string fieldValue = E::encode(value); + std::string fieldValue = IdentifierType::encode(value.getNum()); serializeString(fieldName, fieldValue); } } @@ -313,13 +321,13 @@ public: if (!fieldValue.empty()) { - VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), E::entityType(), fieldValue, [&value](int32_t index){ - value = T(index); + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), IdentifierType::entityType(), fieldValue, [&value](int32_t index){ + value = IdentifierType(index); }); } else { - value = T(defaultValue); + value = IdentifierType(defaultValue); } } } @@ -333,7 +341,7 @@ public: std::vector fieldValue; for(const T & vitem : value) - fieldValue.push_back(E::encode(vitem)); + fieldValue.push_back(E::encode(vitem.getNum())); serializeInternal(fieldName, fieldValue); } @@ -362,7 +370,7 @@ public: std::vector fieldValue; for(const T & vitem : value) - fieldValue.push_back(U::encode(vitem)); + fieldValue.push_back(U::encode(vitem.getNum())); serializeInternal(fieldName, fieldValue); } @@ -387,7 +395,24 @@ public: const TDecoder decoder = std::bind(&IInstanceResolver::decode, instanceResolver, _1); const TEncoder encoder = std::bind(&IInstanceResolver::encode, instanceResolver, _1); - serializeId(fieldName, value, defaultValue, decoder, encoder); + if (saving) + { + if (value != defaultValue) + { + std::string fieldValue = encoder(value.getNum()); + serializeString(fieldName, fieldValue); + } + } + else + { + std::string fieldValue; + serializeString(fieldName, fieldValue); + + if (!fieldValue.empty()) + value = T(decoder(fieldValue)); + else + value = T(defaultValue); + } } ///any serializable object <-> Json struct @@ -435,6 +460,8 @@ protected: virtual void serializeInternal(std::string & value) = 0; virtual void serializeInternal(int64_t & value) = 0; + void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const; + private: const IInstanceResolver * instanceResolver; diff --git a/lib/serializer/JsonSerializer.cpp b/lib/serializer/JsonSerializer.cpp index 15a050d0e..b699c2ee9 100644 --- a/lib/serializer/JsonSerializer.cpp +++ b/lib/serializer/JsonSerializer.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" #include "JsonSerializer.h" -#include "../JsonNode.h" - VCMI_LIB_NAMESPACE_BEGIN JsonSerializer::JsonSerializer(const IInstanceResolver * instanceResolver_, JsonNode & root_): @@ -37,7 +35,7 @@ void JsonSerializer::serializeInternal(const std::string & fieldName, si32 & val void JsonSerializer::serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) { - if(!defaultValue || defaultValue.value() != value) + if(!defaultValue || !vstd::isAlmostEqual(defaultValue.value(), value)) currentObject->operator[](fieldName).Float() = value; } @@ -62,11 +60,7 @@ void JsonSerializer::serializeInternal(const std::string & fieldName, std::vecto data.reserve(value.size()); for(const si32 rawId : value) - { - JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); - jsonElement.String() = encoder(rawId); - data.push_back(std::move(jsonElement)); - } + data.emplace_back(rawId); } void JsonSerializer::serializeInternal(const std::string & fieldName, std::vector & value) @@ -78,11 +72,7 @@ void JsonSerializer::serializeInternal(const std::string & fieldName, std::vecto data.reserve(value.size()); for(const auto & rawId : value) - { - JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); - jsonElement.String() = rawId; - data.push_back(std::move(jsonElement)); - } + data.emplace_back(rawId); } void JsonSerializer::serializeInternal(std::string & value) @@ -95,24 +85,14 @@ void JsonSerializer::serializeInternal(int64_t & value) currentObject->Integer() = value; } -void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - assert(standard.size() == value.size()); if(standard == value) return; writeLICPart(fieldName, "anyOf", encoder, value); } -void JsonSerializer::serializeLIC(const std::string & fieldName, LIC & value) -{ - if(value.any != value.standard) - writeLICPart(fieldName, "anyOf", value.encoder, value.any); - - writeLICPart(fieldName, "allOf", value.encoder, value.all); - writeLICPart(fieldName, "noneOf", value.encoder, value.none); -} - void JsonSerializer::serializeLIC(const std::string & fieldName, LICSet & value) { if(value.any != value.standard) @@ -195,11 +175,7 @@ void JsonSerializer::writeLICPartBuffer(const std::string & fieldName, const std auto & target = currentObject->operator[](fieldName)[partName].Vector(); for(auto & s : buffer) - { - JsonNode val(JsonNode::JsonType::DATA_STRING); - std::swap(val.String(), s); - target.push_back(std::move(val)); - } + target.emplace_back(s); } } diff --git a/lib/serializer/JsonSerializer.h b/lib/serializer/JsonSerializer.h index e4fe9d87e..89bbbf26d 100644 --- a/lib/serializer/JsonSerializer.h +++ b/lib/serializer/JsonSerializer.h @@ -18,8 +18,7 @@ class DLL_LINKAGE JsonSerializer : public JsonTreeSerializer public: JsonSerializer(const IInstanceResolver * instanceResolver_, JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; diff --git a/lib/serializer/JsonUpdater.cpp b/lib/serializer/JsonUpdater.cpp index f80581f19..92b4bfc45 100644 --- a/lib/serializer/JsonUpdater.cpp +++ b/lib/serializer/JsonUpdater.cpp @@ -10,10 +10,9 @@ #include "StdInc.h" #include "JsonUpdater.h" -#include "../JsonNode.h" - #include "../bonuses/CBonusSystemNode.h" #include "../bonuses/Bonus.h" +#include "../json/JsonBonus.h" VCMI_LIB_NAMESPACE_BEGIN @@ -105,81 +104,11 @@ void JsonUpdater::serializeInternal(int64_t & value) value = currentObject->Integer(); } -void JsonUpdater::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonUpdater::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - const JsonNode & field = currentObject->operator[](fieldName); - - if(field.isNull()) - return; - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty() && allOf.Vector().empty()) - { - //permissive mode - value = standard; - } - else - { - //restrictive mode - value.clear(); - value.resize(standard.size(), false); - - readLICPart(anyOf, decoder, true, value); - readLICPart(allOf, decoder, true, value); - } - - readLICPart(noneOf, decoder, false, value); -} - -void JsonUpdater::serializeLIC(const std::string & fieldName, LIC & value) -{ - const JsonNode & field = currentObject->operator[](fieldName); - - if(field.isNull()) - return; - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty()) - { - //permissive mode - value.any = value.standard; - } - else - { - //restrictive mode - value.any.clear(); - value.any.resize(value.standard.size(), false); - - readLICPart(anyOf, value.decoder, true, value.any); - } - - readLICPart(allOf, value.decoder, true, value.all); - readLICPart(noneOf, value.decoder, true, value.none); - - //remove any banned from allowed and required - for(si32 idx = 0; idx < value.none.size(); idx++) - { - if(value.none[idx]) - { - value.all[idx] = false; - value.any[idx] = false; - } - } - - //add all required to allowed - for(si32 idx = 0; idx < value.all.size(); idx++) - { - if(value.all[idx]) - { - value.any[idx] = true; - } - } + LICSet lic(standard, decoder, encoder); + serializeLIC(fieldName, lic); + value = lic.any; } void JsonUpdater::serializeLIC(const std::string & fieldName, LICSet & value) diff --git a/lib/serializer/JsonUpdater.h b/lib/serializer/JsonUpdater.h index b09fd6044..e0cd5f508 100644 --- a/lib/serializer/JsonUpdater.h +++ b/lib/serializer/JsonUpdater.h @@ -9,20 +9,18 @@ */ #pragma once -#include "ILICReader.h" #include "JsonTreeSerializer.h" VCMI_LIB_NAMESPACE_BEGIN class CBonusSystemNode; -class DLL_LINKAGE JsonUpdater: public JsonTreeSerializer, public ILICReader +class DLL_LINKAGE JsonUpdater: public JsonTreeSerializer { public: JsonUpdater(const IInstanceResolver * instanceResolver_, const JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; diff --git a/lib/spells/AbilityCaster.cpp b/lib/spells/AbilityCaster.cpp index 01b1c105a..f6365684d 100644 --- a/lib/spells/AbilityCaster.cpp +++ b/lib/spells/AbilityCaster.cpp @@ -28,7 +28,7 @@ AbilityCaster::AbilityCaster(const battle::Unit * actualCaster_, int32_t baseSpe AbilityCaster::~AbilityCaster() = default; -int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { auto skill = baseSpellLevel; const auto * unit = dynamic_cast(actualCaster); diff --git a/lib/spells/AbilityCaster.h b/lib/spells/AbilityCaster.h index bbfd723d7..35685c409 100644 --- a/lib/spells/AbilityCaster.h +++ b/lib/spells/AbilityCaster.h @@ -23,7 +23,7 @@ public: AbilityCaster(const battle::Unit * actualCaster_, int32_t baseSpellLevel_); virtual ~AbilityCaster(); - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; void getCastDescription(const Spell * spell, const std::vector & attacked, MetaString & text) const override; void spendMana(ServerCallback * server, const int32_t spellCost) const override; diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 7e1e3c19e..5689e290a 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -18,6 +18,8 @@ #include "../CPlayerState.h" #include "../CRandomGenerator.h" #include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../mapObjects/MiscObjects.h" #include "../mapping/CMap.h" #include "../networkPacks/PacksForClient.h" @@ -83,7 +85,7 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnviron for(const Bonus & b : bonuses) { GiveBonus gb; - gb.id = parameters.caster->getCasterUnitId(); + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); gb.bonus = b; env->apply(&gb); } @@ -323,7 +325,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm } GiveBonus gb; - gb.id = parameters.caster->getCasterUnitId(); + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id)); env->apply(&gb); @@ -528,8 +530,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons request.player = parameters.caster->getCasterOwner(); request.title.appendLocalString(EMetaText::JK_TXT, 40); request.description.appendLocalString(EMetaText::JK_TXT, 41); - request.icon.id = Component::EComponentType::SPELL; - request.icon.subtype = owner->id.toEnum(); + request.icon = Component(ComponentType::SPELL, owner->id); env->genericQuery(&request, request.player, queryCallback); @@ -601,7 +602,7 @@ ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner); - const auto fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; + const auto & fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; for(const CGObjectInstance * obj : env->getMap()->objects) { diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index fc333fc93..8ad548bfa 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -19,6 +19,7 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/SetStackEffect.h" #include "../CStack.h" +#include "../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -213,7 +214,24 @@ bool BattleSpellMechanics::canBeCastAt(const Target & target, Problem & problem) Target spellTarget = transformSpellTarget(target); - return effects->applicable(problem, this, target, spellTarget); + const battle::Unit * mainTarget = nullptr; + + if (!getSpell()->canCastOnSelf()) + { + if(spellTarget.front().unitValue) + { + mainTarget = target.front().unitValue; + } + else if(spellTarget.front().hexValue.isValid()) + { + mainTarget = battle()->battleGetUnitByPos(target.front().hexValue, true); + } + + if (mainTarget && mainTarget == caster) + return false; // can't cast on self + } + + return effects->applicable(problem, this, target, spellTarget); } std::vector BattleSpellMechanics::getAffectedStacks(const Target & target) const diff --git a/lib/spells/BonusCaster.cpp b/lib/spells/BonusCaster.cpp index 19ad943af..5c635c97a 100644 --- a/lib/spells/BonusCaster.cpp +++ b/lib/spells/BonusCaster.cpp @@ -45,7 +45,7 @@ void BonusCaster::getCastDescription(const Spell * spell, const std::vectorgetIndex()); + text.replaceName(spell->getId()); if(singleTarget) attacked.at(0)->addNameReplacement(text, true); } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index ff235b245..aaf101276 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -25,7 +25,8 @@ #include "../battle/BattleInfo.h" #include "../battle/CBattleInfoCallback.h" #include "../battle/Unit.h" - +#include "../json/JsonBonus.h" +#include "../json/JsonUtils.h" #include "../mapObjects/CGHeroInstance.h" //todo: remove #include "../serializer/CSerializer.h" #include "../modding/IdentifierStorage.h" @@ -76,6 +77,7 @@ CSpell::CSpell(): power(0), combat(false), creatureAbility(false), + castOnSelf(false), positiveness(ESpellPositiveness::NEUTRAL), defaultProbability(0), rising(false), @@ -107,8 +109,8 @@ const CSpell::LevelInfo & CSpell::getLevelInfo(const int32_t level) const { if(level < 0 || level >= GameConstants::SPELL_SCHOOL_LEVELS) { - logGlobal->error("CSpell::getLevelInfo: invalid school level %d", level); - return levels.at(0); + logGlobal->error("CSpell::getLevelInfo: invalid school mastery level %d", level); + return levels.at(MasteryLevel::EXPERT); } return levels.at(level); @@ -154,7 +156,7 @@ void CSpell::forEachSchool(const std::function CSpellHandler::loadLegacyData() { do { - JsonNode lineNode(JsonNode::JsonType::DATA_STRUCT); + JsonNode lineNode; const auto id = legacyData.size(); @@ -702,6 +709,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & spell->school[info.id] = schoolNames[info.jsonName].Bool(); } + spell->castOnSelf = json["canCastOnSelf"].Bool(); spell->level = static_cast(json["level"].Integer()); spell->power = static_cast(json["power"].Integer()); @@ -711,7 +719,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { const int chance = static_cast(node.second.Integer()); - VLC->identifiers()->requestIdentifier(node.second.meta, "faction", node.first, [=](si32 factionID) + VLC->identifiers()->requestIdentifier(node.second.getModScope(), "faction", node.first, [=](si32 factionID) { spell->probabilities[FactionID(factionID)] = chance; }); @@ -734,7 +742,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { if(counteredSpell.second.Bool()) { - VLC->identifiers()->requestIdentifier(counteredSpell.second.meta, "spell", counteredSpell.first, [=](si32 id) + VLC->identifiers()->requestIdentifier(counteredSpell.second.getModScope(), "spell", counteredSpell.first, [=](si32 id) { spell->counteredSpells.emplace_back(id); }); @@ -825,7 +833,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { logMod->warn("Spell %s has old target condition format. Expected configuration: ", spell->getNameTranslated()); spell->targetCondition = spell->convertTargetCondition(immunities, absoluteImmunities, limiters, absoluteLimiters); - logMod->warn("\n\"targetCondition\" : %s", spell->targetCondition.toJson()); + logMod->warn("\n\"targetCondition\" : %s", spell->targetCondition.toString()); } } else @@ -962,7 +970,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & void CSpellHandler::afterLoadFinalization() { - for(auto spell : objects) + for(auto & spell : objects) { spell->setupMechanics(); } @@ -986,15 +994,13 @@ void CSpellHandler::beforeValidate(JsonNode & object) inheritNode("expert"); } -std::vector CSpellHandler::getDefaultAllowed() const +std::set CSpellHandler::getDefaultAllowed() const { - std::vector allowedSpells; - allowedSpells.reserve(objects.size()); + std::set allowedSpells; - for(const CSpell * s : objects) - { - allowedSpells.push_back( !(s->isSpecial() || s->isCreatureAbility())); - } + for(auto const & s : objects) + if (!s->isSpecial() && !s->isCreatureAbility()) + allowedSpells.insert(s->getId()); return allowedSpells; } diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index ccf575518..b4f079c90 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -13,7 +13,6 @@ #include #include #include -#include "../JsonNode.h" #include "../IHandlerBase.h" #include "../ConstTransitivePtr.h" #include "../int3.h" @@ -21,6 +20,7 @@ #include "../battle/BattleHex.h" #include "../bonuses/Bonus.h" #include "../filesystem/ResourcePath.h" +#include "../json/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -68,7 +68,7 @@ public: ///resource name AnimationPath resourceName; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & minimumAngle; h & resourceName; @@ -84,11 +84,10 @@ public: AnimationItem(); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & resourceName; - if (version > 806) - h & effectName; + h & effectName; h & verticalPosition; h & pause; } @@ -112,7 +111,7 @@ public: ///use selectProjectile to access std::vector projectile; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & projectile; h & hit; @@ -141,7 +140,7 @@ public: JsonNode battleEffects; - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & cost; h & power; @@ -203,6 +202,7 @@ public: int64_t calculateDamage(const spells::Caster * caster) const override; bool hasSchool(SpellSchool school) const override; + bool canCastOnSelf() const override; /** * Calls cb for each school this spell belongs to @@ -267,39 +267,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler & h, const int version) - { - h & identifier; - if (version > 820) - h & modScope; - h & id; - h & level; - h & power; - h & probabilities; - h & attributes; - h & combat; - h & creatureAbility; - h & positiveness; - h & counteredSpells; - h & rising; - h & damage; - h & offensive; - h & targetType; - h & targetCondition; - h & iconImmune; - h & defaultProbability; - h & special; - h & castSound; - h & iconBook; - h & iconEffect; - h & iconScenarioBonus; - h & iconScroll; - h & levels; - h & school; - h & animationInfo; - h & nonMagical; - h & onlyOnWaterMap; - } friend class CSpellHandler; friend class Graphics; friend class test::CSpellTest; @@ -362,6 +329,7 @@ private: si32 power; //spell's power bool combat; //is this spell combat (true) or adventure (false) bool creatureAbility; //if true, only creatures can use this spell + bool castOnSelf; // if set, creature caster can cast this spell on itself si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative std::unique_ptr mechanics;//(!) do not serialize @@ -382,16 +350,7 @@ public: * Gets a list of default allowed spells. OH3 spells are all allowed by default. * */ - std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - if(!h.saving) - { - afterLoadFinalization(); - } - } + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; diff --git a/lib/spells/ExternalCaster.cpp b/lib/spells/ExternalCaster.cpp index a48735bb9..6c8fd8e29 100644 --- a/lib/spells/ExternalCaster.cpp +++ b/lib/spells/ExternalCaster.cpp @@ -42,7 +42,7 @@ void ExternalCaster::spendMana(ServerCallback * server, const int32_t spellCost) //do nothing } -int32_t ExternalCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t ExternalCaster::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { return schoolLevel; } diff --git a/lib/spells/ExternalCaster.h b/lib/spells/ExternalCaster.h index 22e91fcb1..4fcdf0dfa 100644 --- a/lib/spells/ExternalCaster.h +++ b/lib/spells/ExternalCaster.h @@ -14,6 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN +class SpellSchool; + namespace spells { @@ -27,7 +29,7 @@ public: void setActualCaster(const Caster * actualCaster); void setSpellSchoolLevel(int level); - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; void spendMana(ServerCallback * server, const int32_t spellCost) const override; }; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 0a2be59cc..6e260347f 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -48,7 +48,7 @@ namespace spells static std::shared_ptr makeCondition(const CSpell * s) { - std::shared_ptr res = std::make_shared(); + auto res = std::make_shared(); JsonDeserializer deser(nullptr, s->targetCondition); res->serializeJson(deser, TargetConditionItemFactory::getDefault()); @@ -139,7 +139,6 @@ public: BattleCast::BattleCast(const CBattleInfoCallback * cb_, const Caster * caster_, const Mode mode_, const CSpell * spell_): spell(spell_), cb(cb_), - gameCb(IObjectInterface::cb), //FIXME: pass player callback (problem is that BattleAI do not have one) caster(caster_), mode(mode_), smart(boost::logic::indeterminate), @@ -150,7 +149,6 @@ BattleCast::BattleCast(const CBattleInfoCallback * cb_, const Caster * caster_, BattleCast::BattleCast(const BattleCast & orig, const Caster * caster_) : spell(orig.spell), cb(orig.cb), - gameCb(orig.gameCb), caster(caster_), mode(Mode::MAGIC_MIRROR), magicSkillLevel(orig.magicSkillLevel), @@ -184,11 +182,6 @@ const CBattleInfoCallback * BattleCast::getBattle() const return cb; } -const IGameInfoCallback * BattleCast::getGame() const -{ - return gameCb; -} - BattleCast::OptionalValue BattleCast::getSpellLevel() const { return magicSkillLevel; @@ -417,8 +410,7 @@ BaseMechanics::BaseMechanics(const IBattleCast * event): mode(event->getMode()), smart(event->isSmart()), massive(event->isMassive()), - cb(event->getBattle()), - gameCb(event->getGame()) + cb(event->getBattle()) { caster = event->getCaster(); @@ -483,13 +475,13 @@ bool BaseMechanics::adaptProblem(ESpellCastProblem source, Problem & target) con return adaptGenericProblem(target); //Recanter's Cloak or similar effect. Try to retrieve bonus - const auto b = hero->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_MAGIC_ABOVE)); + const auto b = hero->getFirstBonus(Selector::type()(BonusType::BLOCK_MAGIC_ABOVE)); //TODO what about other values and non-artifact sources? if(b && b->val == 2 && b->source == BonusSource::ARTIFACT) { //The %s prevents %s from casting 3rd level or higher spells. text.appendLocalString(EMetaText::GENERAL_TXT, 536); - text.replaceLocalString(EMetaText::ART_NAMES, b->sid.as()); + text.replaceName(b->sid.as()); caster->getCasterName(text); target.add(std::move(text), spells::Problem::NORMAL); } @@ -696,11 +688,6 @@ const Service * BaseMechanics::spells() const return VLC->spells(); //todo: redirect } -const IGameInfoCallback * BaseMechanics::game() const -{ - return gameCb; -} - const CBattleInfoCallback * BaseMechanics::battle() const { return cb; @@ -717,7 +704,7 @@ IAdventureSpellMechanics::IAdventureSpellMechanics(const CSpell * s) std::unique_ptr IAdventureSpellMechanics::createMechanics(const CSpell * s) { - switch (s->id) + switch(s->id.toEnum()) { case SpellID::SUMMON_BOAT: return std::make_unique(s); diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index e3185acd9..982b1f744 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -23,15 +23,20 @@ VCMI_LIB_NAMESPACE_BEGIN struct Query; class IBattleState; class CRandomGenerator; +class CreatureService; class CMap; class CGameInfoCallback; class CBattleInfoCallback; -class IGameInfoCallback; class JsonNode; class CStack; class CGObjectInstance; class CGHeroInstance; +namespace spells +{ +class Service; +} + namespace vstd { class RNG; @@ -75,7 +80,6 @@ public: virtual Mode getMode() const = 0; virtual const Caster * getCaster() const = 0; virtual const CBattleInfoCallback * getBattle() const = 0; - virtual const IGameInfoCallback * getGame() const = 0; virtual OptionalValue getSpellLevel() const = 0; @@ -108,7 +112,6 @@ public: Mode getMode() const override; const Caster * getCaster() const override; const CBattleInfoCallback * getBattle() const override; - const IGameInfoCallback * getGame() const override; OptionalValue getSpellLevel() const override; @@ -156,7 +159,6 @@ private: Mode mode; const CSpell * spell; const CBattleInfoCallback * cb; - const IGameInfoCallback * gameCb; const Caster * caster; }; @@ -246,7 +248,6 @@ public: #endif virtual const Service * spells() const = 0; - virtual const IGameInfoCallback * game() const = 0; virtual const CBattleInfoCallback * battle() const = 0; const Caster * caster; @@ -305,7 +306,6 @@ public: #endif const Service * spells() const override; - const IGameInfoCallback * game() const override; const CBattleInfoCallback * battle() const override; protected: @@ -329,7 +329,6 @@ private: boost::logic::tribool smart; boost::logic::tribool massive; - const IGameInfoCallback * gameCb; const CBattleInfoCallback * cb; }; diff --git a/lib/spells/ObstacleCasterProxy.cpp b/lib/spells/ObstacleCasterProxy.cpp index 5b1d5ea78..8909fd17e 100644 --- a/lib/spells/ObstacleCasterProxy.cpp +++ b/lib/spells/ObstacleCasterProxy.cpp @@ -22,7 +22,7 @@ ObstacleCasterProxy::ObstacleCasterProxy(PlayerColor owner_, const Caster * hero { } -int32_t ObstacleCasterProxy::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t ObstacleCasterProxy::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { return obs.spellLevel; } @@ -71,7 +71,8 @@ SilentCaster::SilentCaster(PlayerColor owner_, const Caster * hero_): void SilentCaster::getCasterName(MetaString & text) const { - logGlobal->error("Unexpected call to SilentCaster::getCasterName"); + // NOTE: can be triggered (for example) if creature steps into Tower mines/moat while hero has Recanter's Cloak + logGlobal->debug("Unexpected call to SilentCaster::getCasterName"); } void SilentCaster::getCastDescription(const Spell * spell, const std::vector & attacked, MetaString & text) const diff --git a/lib/spells/ObstacleCasterProxy.h b/lib/spells/ObstacleCasterProxy.h index 075d31cf4..cd1c540d5 100644 --- a/lib/spells/ObstacleCasterProxy.h +++ b/lib/spells/ObstacleCasterProxy.h @@ -35,7 +35,7 @@ class DLL_LINKAGE ObstacleCasterProxy : public SilentCaster public: ObstacleCasterProxy(PlayerColor owner_, const Caster * hero_, const SpellCreatedObstacle & obs_); - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; int64_t getSpellBonus(const Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int32_t getEffectPower(const Spell * spell) const override; diff --git a/lib/spells/ProxyCaster.cpp b/lib/spells/ProxyCaster.cpp index 2ffaa49c7..7622392bb 100644 --- a/lib/spells/ProxyCaster.cpp +++ b/lib/spells/ProxyCaster.cpp @@ -36,7 +36,7 @@ int32_t ProxyCaster::getCasterUnitId() const return -1; } -int32_t ProxyCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t ProxyCaster::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { if(actualCaster) return actualCaster->getSpellSchoolLevel(spell, outSelectedSchool); diff --git a/lib/spells/ProxyCaster.h b/lib/spells/ProxyCaster.h index 85fcf240c..67557dc02 100644 --- a/lib/spells/ProxyCaster.h +++ b/lib/spells/ProxyCaster.h @@ -24,7 +24,7 @@ public: virtual ~ProxyCaster(); int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; int64_t getSpellBonus(const Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int64_t getSpecificSpellBonus(const Spell * spell, int64_t base) const override; diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index bda1f32df..c55e18719 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -17,7 +17,7 @@ #include "../battle/Unit.h" #include "../bonuses/BonusParams.h" #include "../bonuses/BonusList.h" - +#include "../json/JsonBonus.h" #include "../modding/IdentifierStorage.h" #include "../modding/ModUtility.h" #include "../serializer/JsonSerializeFormat.h" @@ -318,19 +318,19 @@ class DefaultTargetConditionItemFactory : public TargetConditionItemFactory public: Object createAbsoluteLevel() const override { - static std::shared_ptr antimagicCondition = std::make_shared(); + static auto antimagicCondition = std::make_shared(); return antimagicCondition; } Object createAbsoluteSpell() const override { - static std::shared_ptr alCondition = std::make_shared(); + static auto alCondition = std::make_shared(); return alCondition; } Object createElemental() const override { - static std::shared_ptr elementalCondition = std::make_shared(); + static auto elementalCondition = std::make_shared(); return elementalCondition; } @@ -342,13 +342,13 @@ public: Object createNormalLevel() const override { - static std::shared_ptr nlCondition = std::make_shared(); + static auto nlCondition = std::make_shared(); return nlCondition; } Object createNormalSpell() const override { - static std::shared_ptr nsCondition = std::make_shared(); + static auto nsCondition = std::make_shared(); return nsCondition; } @@ -424,13 +424,13 @@ public: Object createReceptiveFeature() const override { - static std::shared_ptr condition = std::make_shared(); + static auto condition = std::make_shared(); return condition; } Object createImmunityNegation() const override { - static std::shared_ptr condition = std::make_shared(); + static auto condition = std::make_shared(); return condition; } }; @@ -544,7 +544,7 @@ void TargetCondition::loadConditions(const JsonNode & source, bool exclusive, bo ModUtility::parseIdentifier(keyValue.first, scope, type, identifier); - item = itemFactory->createConfigurable(keyValue.second.meta, type, identifier); + item = itemFactory->createConfigurable(keyValue.second.getModScope(), type, identifier); } if(item) diff --git a/lib/spells/ViewSpellInt.cpp b/lib/spells/ViewSpellInt.cpp index 2b8e36ec7..1507d761a 100644 --- a/lib/spells/ViewSpellInt.cpp +++ b/lib/spells/ViewSpellInt.cpp @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN ObjectPosInfo::ObjectPosInfo(const CGObjectInstance * obj): - pos(obj->visitablePos()), id(obj->ID), subId(obj->subID), owner(obj->tempOwner) + pos(obj->visitablePos()), id(obj->ID), subId(obj->getObjTypeIndex()), owner(obj->tempOwner) { } diff --git a/lib/spells/ViewSpellInt.h b/lib/spells/ViewSpellInt.h index 18447d43a..a4c589daa 100644 --- a/lib/spells/ViewSpellInt.h +++ b/lib/spells/ViewSpellInt.h @@ -26,7 +26,7 @@ VCMI_LIB_NAMESPACE_BEGIN ObjectPosInfo() = default; ObjectPosInfo(const CGObjectInstance * obj); - template void serialize(Handler & h, const int version) + template void serialize(Handler & h) { h & pos; h & id; diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 74d0cb332..167aaabd6 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -20,6 +20,7 @@ #include "../../mapObjects/CGTownInstance.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" +#include "../../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -72,6 +73,7 @@ void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const return; CatapultAttack ca; + ca.battleID = m->battle()->getBattle()->getBattleID(); ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId(); for(int i = 0; i < targetsToAttack; i++) diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index bb788193b..425607008 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -20,10 +20,12 @@ #include "../../battle/CBattleInfoCallback.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../CGeneralTextHandler.h" +#include "../../Languages.h" #include "../../serializer/JsonSerializeFormat.h" #include + VCMI_LIB_NAMESPACE_BEGIN namespace spells @@ -152,6 +154,16 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, m->caster->getCasterName(line); log.push_back(line); } + else if(m->getSpell()->getJsonKey().find("accurateShot") != std::string::npos && !multiple) + { + MetaString line; + std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); + std::string textID = "vcmi.battleWindow.accurateShot.resultDescription"; + line.appendTextID(Languages::getPluralFormTextID( preferredLanguage, kills, textID)); + line.replaceNumber(kills); + firstTarget->addNameReplacement(line, kills != 1); + log.push_back(line); + } else if(m->getSpellIndex() == SpellID::THUNDERBOLT && !multiple) { { @@ -176,7 +188,7 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, { MetaString line; line.appendLocalString(EMetaText::GENERAL_TXT, 376); // Spell %s does %d damage - line.replaceLocalString(EMetaText::SPELL_NAME, m->getSpellIndex()); + line.replaceName(m->getSpellId()); line.replaceNumber(static_cast(damage)); log.push_back(line); diff --git a/lib/spells/effects/DemonSummon.cpp b/lib/spells/effects/DemonSummon.cpp index 09f8ec79e..8f5376702 100644 --- a/lib/spells/effects/DemonSummon.cpp +++ b/lib/spells/effects/DemonSummon.cpp @@ -49,7 +49,7 @@ void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const Effe break; } - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); int32_t deadCount = targetStack->unitBaseAmount(); int32_t deadTotalHealth = targetStack->getTotalHealth(); @@ -111,7 +111,7 @@ bool DemonSummon::isValidTarget(const Mechanics * m, const battle::Unit * unit) if (unit->isGhost()) return false; - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); if (unit->getTotalHealth() < creatureType->getMaxHealth()) return false; diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index 3fb418acd..8ad649f9a 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -93,7 +93,7 @@ std::shared_ptr Dispel::getBonuses(const Mechanics * m, const b { if(bonus->source == BonusSource::SPELL_EFFECT) { - const Spell * sourceSpell = bonus->sid.as().toSpell(m->spells()); + const Spell * sourceSpell = bonus->sid.as().toEntity(m->spells()); if(!sourceSpell) return false;//error diff --git a/lib/spells/effects/Heal.cpp b/lib/spells/effects/Heal.cpp index d3d84d8a9..815d6888c 100644 --- a/lib/spells/effects/Heal.cpp +++ b/lib/spells/effects/Heal.cpp @@ -132,7 +132,8 @@ void Heal::prepareHealEffect(int64_t value, BattleUnitsChanged & pack, BattleLog else if (unitHPgained > 0 && m->caster->getHeroCaster() == nullptr) //Show text about healed HP if healed by unit { MetaString healText; - auto casterUnit = dynamic_cast(m->caster); + auto casterUnitID = m->caster->getCasterUnitId(); + auto casterUnit = m->battle()->battleGetUnitByID(casterUnitID); healText.appendLocalString(EMetaText::GENERAL_TXT, 414); casterUnit->addNameReplacement(healText, false); state->addNameReplacement(healText, false); diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 8546f779b..45531d8b1 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -18,6 +18,7 @@ #include "../../bonuses/Limiters.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../json/JsonBonus.h" #include "../../serializer/JsonSerializeFormat.h" #include "../../networkPacks/PacksForClient.h" #include "../../networkPacks/PacksForClientBattle.h" @@ -117,7 +118,7 @@ void Moat::apply(ServerCallback * server, const Mechanics * m, const EffectTarge for(auto & b : converted) { GiveBonus gb(GiveBonus::ETarget::BATTLE); - gb.id = m->battle()->getBattle()->getBattleID().getNum(); + gb.id = m->battle()->getBattle()->getBattleID(); gb.bonus = b; server->apply(&gb); } diff --git a/lib/spells/effects/Moat.h b/lib/spells/effects/Moat.h index 536cf4bfd..c30130743 100644 --- a/lib/spells/effects/Moat.h +++ b/lib/spells/effects/Moat.h @@ -14,6 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN +struct Bonus; + namespace spells { namespace effects diff --git a/lib/spells/effects/Registry.cpp b/lib/spells/effects/Registry.cpp index ced61dc7f..ef51f05f4 100644 --- a/lib/spells/effects/Registry.cpp +++ b/lib/spells/effects/Registry.cpp @@ -80,7 +80,7 @@ private: Registry * GlobalRegistry::get() { - static std::unique_ptr Instance = std::make_unique(); + static auto Instance = std::make_unique(); return Instance.get(); } diff --git a/lib/spells/effects/Summon.cpp b/lib/spells/effects/Summon.cpp index 432f6ab7c..39b4dceca 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -42,6 +42,12 @@ void Summon::adjustTargetTypes(std::vector & types) const bool Summon::applicable(Problem & problem, const Mechanics * m) const { + if (creature == CreatureID::NONE) + { + logMod->error("Attempt to summon non-existing creature!"); + return m->adaptGenericProblem(problem); + } + if(exclusive) { //check if there are summoned creatures of other type @@ -66,7 +72,7 @@ bool Summon::applicable(Problem & problem, const Mechanics * m) const { text.replaceRawString(caster->getNameTranslated()); - text.replaceLocalString(EMetaText::CRE_PL_NAMES, elemental->creatureIndex()); + text.replaceNamePlural(elemental->creatureId()); if(caster->type->gender == EHeroGender::FEMALE) text.replaceLocalString(EMetaText::GENERAL_TXT, 540); @@ -107,7 +113,7 @@ void Summon::apply(ServerCallback * server, const Mechanics * m, const EffectTar if(summonByHealth) { - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); auto creatureMaxHealth = creatureType->getMaxHealth(); amount = static_cast(valueWithBonus / creatureMaxHealth); } diff --git a/lib/spells/effects/Teleport.cpp b/lib/spells/effects/Teleport.cpp index bffffa6fd..4d29e9483 100644 --- a/lib/spells/effects/Teleport.cpp +++ b/lib/spells/effects/Teleport.cpp @@ -12,6 +12,7 @@ #include "Teleport.h" #include "Registry.h" #include "../ISpellMechanics.h" +#include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../networkPacks/PacksForClientBattle.h" @@ -76,6 +77,7 @@ void Teleport::apply(ServerCallback * server, const Mechanics * m, const EffectT const auto destination = target[1].hexValue; BattleStackMoved pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); pack.distance = 0; pack.stack = targetUnit->unitId(); std::vector tiles; diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index e006d37a4..a90c903d7 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -17,6 +17,7 @@ #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../json/JsonBonus.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../networkPacks/SetStackEffect.h" @@ -112,9 +113,9 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg const auto *casterHero = dynamic_cast(m->caster); if(casterHero) { - peculiarBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, BonusSubtypeID(m->getSpellId()))); - addedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); - fixedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); + peculiarBonus = casterHero->getFirstBonus(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, BonusSubtypeID(m->getSpellId()))); + addedValueBonus = casterHero->getFirstBonus(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); + fixedValueBonus = casterHero->getFirstBonus(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); } //TODO: does hero specialty should affects his stack casting spells? @@ -169,7 +170,6 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg case 1: //Coronius style specialty bonus. //Please note that actual Coronius isnt here, because Slayer is a spell that doesnt affect monster stats and is used only in calculateDmgRange - power = std::max(5 - tier, 0); break; } if(m->isNegativeSpell()) diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index 02f0cd377..a9800ab3d 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -1,42 +1,22 @@ #include "StdInc.h" #include -#if defined(VCMI_ANDROID) -#include "../CAndroidVMHelper.h" -#endif - VCMI_LIB_NAMESPACE_BEGIN namespace vstd { - DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt) + DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt, std::string format) { -#if defined(VCMI_ANDROID) - CAndroidVMHelper vmHelper; - return vmHelper.callStaticStringMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "getFormattedDateTime"); -#endif - std::tm tm = *std::localtime(&dt); std::stringstream s; - try - { - s.imbue(std::locale("")); - } - catch(const std::runtime_error & e) - { - // locale not be available - keep default / global - } - s << std::put_time(&tm, "%x %X"); + s << std::put_time(&tm, format.c_str()); return s.str(); } DLL_LINKAGE std::string getDateTimeISO8601Basic(std::time_t dt) { - std::tm tm = *std::localtime(&dt); - std::stringstream s; - s << std::put_time(&tm, "%Y%m%dT%H%M%S"); - return s.str(); + return getFormattedDateTime(dt, "%Y%m%dT%H%M%S"); } } diff --git a/lib_server/CMakeLists.txt b/lib_server/CMakeLists.txt deleted file mode 100644 index 193e4ac5c..000000000 --- a/lib_server/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -add_main_lib(vcmi_lib_server STATIC) -target_compile_definitions(vcmi_lib_server PUBLIC VCMI_LIB_NAMESPACE=LIB_SERVER) -target_compile_definitions(vcmi_lib_server PUBLIC VCMI_DLL_STATIC=1) diff --git a/lobby/CMakeLists.txt b/lobby/CMakeLists.txt new file mode 100644 index 000000000..b4169ab09 --- /dev/null +++ b/lobby/CMakeLists.txt @@ -0,0 +1,45 @@ +set(lobby_SRCS + StdInc.cpp + + EntryPoint.cpp + LobbyDatabase.cpp + LobbyServer.cpp + SQLiteConnection.cpp +) + +set(lobby_HEADERS + StdInc.h + + LobbyDatabase.h + LobbyDefines.h + LobbyServer.h + SQLiteConnection.h +) + +assign_source_group(${lobby_SRCS} ${lobby_HEADERS}) + +add_executable(vcmilobby ${lobby_SRCS} ${lobby_HEADERS}) +set(lobby_LIBS vcmi) + +if(CMAKE_SYSTEM_NAME MATCHES FreeBSD OR HAIKU) + set(lobby_LIBS execinfo ${lobby_LIBS}) +endif() + +target_link_libraries(vcmilobby PRIVATE ${lobby_LIBS} ${SQLite3_LIBRARIES}) + +target_include_directories(vcmilobby PRIVATE ${SQLite3_INCLUDE_DIRS}) +target_include_directories(vcmilobby PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +if(WIN32) + set_target_properties(vcmilobby + PROPERTIES + OUTPUT_NAME "VCMI_lobby" + PROJECT_LABEL "VCMI_lobby" + ) +endif() + +vcmi_set_output_dir(vcmilobby "") +enable_pch(vcmilobby) + +install(TARGETS vcmilobby DESTINATION ${BIN_DIR}) + diff --git a/lobby/EntryPoint.cpp b/lobby/EntryPoint.cpp new file mode 100644 index 000000000..48a6cd783 --- /dev/null +++ b/lobby/EntryPoint.cpp @@ -0,0 +1,37 @@ +/* + * EntryPoint.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "LobbyServer.h" + +#include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/VCMIDirs.h" + +static const int LISTENING_PORT = 30303; + +int main(int argc, const char * argv[]) +{ +#ifndef VCMI_IOS + console = new CConsoleHandler(); +#endif + CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Lobby_log.txt", console); + logConfig.configureDefault(); + + auto databasePath = VCMIDirs::get().userDataPath() / "vcmiLobby.db"; + logGlobal->info("Opening database %s", databasePath.string()); + + LobbyServer server(databasePath); + logGlobal->info("Starting server on port %d", LISTENING_PORT); + + server.start(LISTENING_PORT); + server.run(); + + return 0; +} diff --git a/lobby/LobbyDatabase.cpp b/lobby/LobbyDatabase.cpp new file mode 100644 index 000000000..f4aa5d9b4 --- /dev/null +++ b/lobby/LobbyDatabase.cpp @@ -0,0 +1,540 @@ +/* + * LobbyServer.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "LobbyDatabase.h" + +#include "SQLiteConnection.h" + +void LobbyDatabase::createTables() +{ + static const std::string createChatMessages = R"( + CREATE TABLE IF NOT EXISTS chatMessages ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + senderName TEXT, + roomType TEXT, + messageText TEXT, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableGameRooms = R"( + CREATE TABLE IF NOT EXISTS gameRooms ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + roomID TEXT, + hostAccountID TEXT, + status INTEGER NOT NULL DEFAULT 0, + playerLimit INTEGER NOT NULL, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableGameRoomPlayers = R"( + CREATE TABLE IF NOT EXISTS gameRoomPlayers ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + roomID TEXT, + accountID TEXT + ); + )"; + + static const std::string createTableAccounts = R"( + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + accountID TEXT, + displayName TEXT, + online INTEGER NOT NULL, + lastLoginTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableAccountCookies = R"( + CREATE TABLE IF NOT EXISTS accountCookies ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + accountID TEXT, + cookieUUID TEXT, + creationTime TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + )"; + + static const std::string createTableGameRoomInvites = R"( + CREATE TABLE IF NOT EXISTS gameRoomInvites ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + roomID TEXT, + accountID TEXT + ); + )"; + + database->prepare(createChatMessages)->execute(); + database->prepare(createTableGameRoomPlayers)->execute(); + database->prepare(createTableGameRooms)->execute(); + database->prepare(createTableAccounts)->execute(); + database->prepare(createTableAccountCookies)->execute(); + database->prepare(createTableGameRoomInvites)->execute(); +} + +void LobbyDatabase::clearOldData() +{ + static const std::string removeActiveAccounts = R"( + UPDATE accounts + SET online = 0 + WHERE online <> 0 + )"; + + static const std::string removeActiveRooms = R"( + UPDATE gameRooms + SET status = 5 + WHERE status <> 5 + )"; + + database->prepare(removeActiveAccounts)->execute(); + database->prepare(removeActiveRooms)->execute(); +} + +void LobbyDatabase::prepareStatements() +{ + // INSERT INTO + + static const std::string insertChatMessageText = R"( + INSERT INTO chatMessages(senderName, messageText) VALUES( ?, ?); + )"; + + static const std::string insertAccountText = R"( + INSERT INTO accounts(accountID, displayName, online) VALUES(?,?,0); + )"; + + static const std::string insertAccessCookieText = R"( + INSERT INTO accountCookies(accountID, cookieUUID) VALUES(?,?); + )"; + + static const std::string insertGameRoomText = R"( + INSERT INTO gameRooms(roomID, hostAccountID, status, playerLimit) VALUES(?, ?, 0, 8); + )"; + + static const std::string insertGameRoomPlayersText = R"( + INSERT INTO gameRoomPlayers(roomID, accountID) VALUES(?,?); + )"; + + static const std::string insertGameRoomInvitesText = R"( + INSERT INTO gameRoomInvites(roomID, accountID) VALUES(?,?); + )"; + + // DELETE FROM + + static const std::string deleteGameRoomPlayersText = R"( + DELETE FROM gameRoomPlayers WHERE roomID = ? AND accountID = ? + )"; + + static const std::string deleteGameRoomInvitesText = R"( + DELETE FROM gameRoomInvites WHERE roomID = ? AND accountID = ? + )"; + + // UPDATE + + static const std::string setAccountOnlineText = R"( + UPDATE accounts + SET online = ? + WHERE accountID = ? + )"; + + static const std::string setGameRoomStatusText = R"( + UPDATE gameRooms + SET status = ? + WHERE roomID = ? + )"; + + static const std::string updateAccountLoginTimeText = R"( + UPDATE accounts + SET lastLoginTime = CURRENT_TIMESTAMP + WHERE accountID = ? + )"; + + // SELECT FROM + + static const std::string getRecentMessageHistoryText = R"( + SELECT senderName, displayName, messageText, strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',cm.creationTime) AS secondsElapsed + FROM chatMessages cm + LEFT JOIN accounts on accountID = senderName + WHERE secondsElapsed < 60*60*18 + ORDER BY cm.creationTime DESC + LIMIT 100 + )"; + + static const std::string getIdleGameRoomText = R"( + SELECT roomID + FROM gameRooms + WHERE hostAccountID = ? AND status = 0 + LIMIT 1 + )"; + + static const std::string getGameRoomStatusText = R"( + SELECT status + FROM gameRooms + WHERE roomID = ? + )"; + + static const std::string getAccountGameRoomText = R"( + SELECT grp.roomID + FROM gameRoomPlayers grp + LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID + WHERE accountID = ? AND status IN (1, 2) + LIMIT 1 + )"; + + static const std::string getActiveAccountsText = R"( + SELECT accountID, displayName + FROM accounts + WHERE online = 1 + )"; + + static const std::string getActiveGameRoomsText = R"( + SELECT roomID, hostAccountID, displayName, status, playerLimit + FROM gameRooms + LEFT JOIN accounts ON hostAccountID = accountID + WHERE status = 1 + )"; + + static const std::string countRoomUsedSlotsText = R"( + SELECT COUNT(accountID) + FROM gameRoomPlayers + WHERE roomID = ? + )"; + + static const std::string countRoomTotalSlotsText = R"( + SELECT playerLimit + FROM gameRooms + WHERE roomID = ? + )"; + + static const std::string getAccountDisplayNameText = R"( + SELECT displayName + FROM accounts + WHERE accountID = ? + )"; + + static const std::string isAccountCookieValidText = R"( + SELECT COUNT(accountID) + FROM accountCookies + WHERE accountID = ? AND cookieUUID = ? + )"; + + static const std::string isGameRoomCookieValidText = R"( + SELECT COUNT(roomID) + FROM gameRooms + LEFT JOIN accountCookies ON accountCookies.accountID = gameRooms.hostAccountID + WHERE roomID = ? AND cookieUUID = ? AND strftime('%s',CURRENT_TIMESTAMP)- strftime('%s',creationTime) < ? + )"; + + static const std::string isPlayerInGameRoomText = R"( + SELECT COUNT(accountID) + FROM gameRoomPlayers grp + LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID + WHERE accountID = ? AND grp.roomID = ? AND status IN (1, 2) + )"; + + static const std::string isPlayerInAnyGameRoomText = R"( + SELECT COUNT(accountID) + FROM gameRoomPlayers grp + LEFT JOIN gameRooms gr ON gr.roomID = grp.roomID + WHERE accountID = ? AND status IN (1, 2) + )"; + + static const std::string isAccountIDExistsText = R"( + SELECT COUNT(accountID) + FROM accounts + WHERE accountID = ? + )"; + + static const std::string isAccountNameExistsText = R"( + SELECT COUNT(displayName) + FROM accounts + WHERE displayName = ? + )"; + + insertChatMessageStatement = database->prepare(insertChatMessageText); + insertAccountStatement = database->prepare(insertAccountText); + insertAccessCookieStatement = database->prepare(insertAccessCookieText); + insertGameRoomStatement = database->prepare(insertGameRoomText); + insertGameRoomPlayersStatement = database->prepare(insertGameRoomPlayersText); + insertGameRoomInvitesStatement = database->prepare(insertGameRoomInvitesText); + + deleteGameRoomPlayersStatement = database->prepare(deleteGameRoomPlayersText); + deleteGameRoomInvitesStatement = database->prepare(deleteGameRoomInvitesText); + + setAccountOnlineStatement = database->prepare(setAccountOnlineText); + setGameRoomStatusStatement = database->prepare(setGameRoomStatusText); + updateAccountLoginTimeStatement = database->prepare(updateAccountLoginTimeText); + + getRecentMessageHistoryStatement = database->prepare(getRecentMessageHistoryText); + getIdleGameRoomStatement = database->prepare(getIdleGameRoomText); + getGameRoomStatusStatement = database->prepare(getGameRoomStatusText); + getAccountGameRoomStatement = database->prepare(getAccountGameRoomText); + getActiveAccountsStatement = database->prepare(getActiveAccountsText); + getActiveGameRoomsStatement = database->prepare(getActiveGameRoomsText); + getAccountDisplayNameStatement = database->prepare(getAccountDisplayNameText); + countRoomUsedSlotsStatement = database->prepare(countRoomUsedSlotsText); + countRoomTotalSlotsStatement = database->prepare(countRoomTotalSlotsText); + + isAccountCookieValidStatement = database->prepare(isAccountCookieValidText); + isPlayerInGameRoomStatement = database->prepare(isPlayerInGameRoomText); + isPlayerInAnyGameRoomStatement = database->prepare(isPlayerInAnyGameRoomText); + isAccountIDExistsStatement = database->prepare(isAccountIDExistsText); + isAccountNameExistsStatement = database->prepare(isAccountNameExistsText); +} + +LobbyDatabase::~LobbyDatabase() = default; + +LobbyDatabase::LobbyDatabase(const boost::filesystem::path & databasePath) +{ + database = SQLiteInstance::open(databasePath, true); + createTables(); + clearOldData(); + prepareStatements(); +} + +void LobbyDatabase::insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomName, const std::string & messageText) +{ + insertChatMessageStatement->executeOnce(sender, messageText); +} + +bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID) +{ + bool result = false; + + isPlayerInAnyGameRoomStatement->setBinds(accountID); + if(isPlayerInAnyGameRoomStatement->execute()) + isPlayerInAnyGameRoomStatement->getColumns(result); + isPlayerInAnyGameRoomStatement->reset(); + + return result; +} + +bool LobbyDatabase::isPlayerInGameRoom(const std::string & accountID, const std::string & roomID) +{ + bool result = false; + + isPlayerInGameRoomStatement->setBinds(accountID, roomID); + if(isPlayerInGameRoomStatement->execute()) + isPlayerInGameRoomStatement->getColumns(result); + isPlayerInGameRoomStatement->reset(); + + return result; +} + +std::vector LobbyDatabase::getRecentMessageHistory() +{ + std::vector result; + + while(getRecentMessageHistoryStatement->execute()) + { + LobbyChatMessage message; + getRecentMessageHistoryStatement->getColumns(message.accountID, message.displayName, message.messageText, message.age); + result.push_back(message); + } + getRecentMessageHistoryStatement->reset(); + + return result; +} + +void LobbyDatabase::setAccountOnline(const std::string & accountID, bool isOnline) +{ + setAccountOnlineStatement->executeOnce(isOnline ? 1 : 0, accountID); +} + +void LobbyDatabase::setGameRoomStatus(const std::string & roomID, LobbyRoomState roomStatus) +{ + setGameRoomStatusStatement->executeOnce(vstd::to_underlying(roomStatus), roomID); +} + +void LobbyDatabase::insertPlayerIntoGameRoom(const std::string & accountID, const std::string & roomID) +{ + insertGameRoomPlayersStatement->executeOnce(roomID, accountID); +} + +void LobbyDatabase::deletePlayerFromGameRoom(const std::string & accountID, const std::string & roomID) +{ + deleteGameRoomPlayersStatement->executeOnce(roomID, accountID); +} + +void LobbyDatabase::deleteGameRoomInvite(const std::string & targetAccountID, const std::string & roomID) +{ + deleteGameRoomInvitesStatement->executeOnce(roomID, targetAccountID); +} + +void LobbyDatabase::insertGameRoomInvite(const std::string & targetAccountID, const std::string & roomID) +{ + insertGameRoomInvitesStatement->executeOnce(roomID, targetAccountID); +} + +void LobbyDatabase::insertGameRoom(const std::string & roomID, const std::string & hostAccountID) +{ + insertGameRoomStatement->executeOnce(roomID, hostAccountID); +} + +void LobbyDatabase::insertAccount(const std::string & accountID, const std::string & displayName) +{ + insertAccountStatement->executeOnce(accountID, displayName); +} + +void LobbyDatabase::insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID) +{ + insertAccessCookieStatement->executeOnce(accountID, accessCookieUUID); +} + +void LobbyDatabase::updateAccountLoginTime(const std::string & accountID) +{ + updateAccountLoginTimeStatement->executeOnce(accountID); +} + +std::string LobbyDatabase::getAccountDisplayName(const std::string & accountID) +{ + std::string result; + + getAccountDisplayNameStatement->setBinds(accountID); + if(getAccountDisplayNameStatement->execute()) + getAccountDisplayNameStatement->getColumns(result); + getAccountDisplayNameStatement->reset(); + + return result; +} + +LobbyCookieStatus LobbyDatabase::getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID) +{ + bool result = false; + + isAccountCookieValidStatement->setBinds(accountID, accessCookieUUID); + if(isAccountCookieValidStatement->execute()) + isAccountCookieValidStatement->getColumns(result); + isAccountCookieValidStatement->reset(); + + return result ? LobbyCookieStatus::VALID : LobbyCookieStatus::INVALID; +} + +LobbyInviteStatus LobbyDatabase::getAccountInviteStatus(const std::string & accountID, const std::string & roomID) +{ + assert(0); + return {}; +} + +LobbyRoomState LobbyDatabase::getGameRoomStatus(const std::string & roomID) +{ + int result = -1; + + getGameRoomStatusStatement->setBinds(roomID); + if(getGameRoomStatusStatement->execute()) + getGameRoomStatusStatement->getColumns(result); + getGameRoomStatusStatement->reset(); + + if (result != -1) + return static_cast(result); + return LobbyRoomState::CLOSED; +} + +uint32_t LobbyDatabase::getGameRoomFreeSlots(const std::string & roomID) +{ + uint32_t usedSlots = 0; + uint32_t totalSlots = 0; + + countRoomUsedSlotsStatement->setBinds(roomID); + if(countRoomUsedSlotsStatement->execute()) + countRoomUsedSlotsStatement->getColumns(usedSlots); + countRoomUsedSlotsStatement->reset(); + + countRoomTotalSlotsStatement->setBinds(roomID); + if(countRoomTotalSlotsStatement->execute()) + countRoomTotalSlotsStatement->getColumns(totalSlots); + countRoomTotalSlotsStatement->reset(); + + + if (totalSlots > usedSlots) + return totalSlots - usedSlots; + return 0; +} + +bool LobbyDatabase::isAccountNameExists(const std::string & displayName) +{ + bool result = false; + + isAccountNameExistsStatement->setBinds(displayName); + if(isAccountNameExistsStatement->execute()) + isAccountNameExistsStatement->getColumns(result); + isAccountNameExistsStatement->reset(); + return result; +} + +bool LobbyDatabase::isAccountIDExists(const std::string & accountID) +{ + bool result = false; + + isAccountIDExistsStatement->setBinds(accountID); + if(isAccountIDExistsStatement->execute()) + isAccountIDExistsStatement->getColumns(result); + isAccountIDExistsStatement->reset(); + return result; +} + +std::vector LobbyDatabase::getActiveGameRooms() +{ + std::vector result; + + while(getActiveGameRoomsStatement->execute()) + { + LobbyGameRoom entry; + getActiveGameRoomsStatement->getColumns(entry.roomID, entry.hostAccountID, entry.hostAccountDisplayName, entry.roomStatus, entry.playersLimit); + result.push_back(entry); + } + getActiveGameRoomsStatement->reset(); + + for (auto & room : result) + { + countRoomUsedSlotsStatement->setBinds(room.roomID); + if(countRoomUsedSlotsStatement->execute()) + countRoomUsedSlotsStatement->getColumns(room.playersCount); + countRoomUsedSlotsStatement->reset(); + } + return result; +} + +std::vector LobbyDatabase::getActiveAccounts() +{ + std::vector result; + + while(getActiveAccountsStatement->execute()) + { + LobbyAccount entry; + getActiveAccountsStatement->getColumns(entry.accountID, entry.displayName); + result.push_back(entry); + } + getActiveAccountsStatement->reset(); + return result; +} + +std::string LobbyDatabase::getIdleGameRoom(const std::string & hostAccountID) +{ + std::string result; + + getIdleGameRoomStatement->setBinds(hostAccountID); + if(getIdleGameRoomStatement->execute()) + getIdleGameRoomStatement->getColumns(result); + + getIdleGameRoomStatement->reset(); + return result; +} + +std::string LobbyDatabase::getAccountGameRoom(const std::string & accountID) +{ + std::string result; + + getAccountGameRoomStatement->setBinds(accountID); + if(getAccountGameRoomStatement->execute()) + getAccountGameRoomStatement->getColumns(result); + + getAccountGameRoomStatement->reset(); + return result; +} diff --git a/lobby/LobbyDatabase.h b/lobby/LobbyDatabase.h new file mode 100644 index 000000000..d1fbd3c60 --- /dev/null +++ b/lobby/LobbyDatabase.h @@ -0,0 +1,96 @@ +/* + * LobbyDatabase.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "LobbyDefines.h" + +class SQLiteInstance; +class SQLiteStatement; + +using SQLiteInstancePtr = std::unique_ptr; +using SQLiteStatementPtr = std::unique_ptr; + +class LobbyDatabase +{ + SQLiteInstancePtr database; + + SQLiteStatementPtr insertChatMessageStatement; + SQLiteStatementPtr insertAccountStatement; + SQLiteStatementPtr insertAccessCookieStatement; + SQLiteStatementPtr insertGameRoomStatement; + SQLiteStatementPtr insertGameRoomPlayersStatement; + SQLiteStatementPtr insertGameRoomInvitesStatement; + + SQLiteStatementPtr deleteGameRoomPlayersStatement; + SQLiteStatementPtr deleteGameRoomInvitesStatement; + + SQLiteStatementPtr setAccountOnlineStatement; + SQLiteStatementPtr setGameRoomStatusStatement; + SQLiteStatementPtr updateAccountLoginTimeStatement; + + SQLiteStatementPtr getRecentMessageHistoryStatement; + SQLiteStatementPtr getIdleGameRoomStatement; + SQLiteStatementPtr getGameRoomStatusStatement; + SQLiteStatementPtr getActiveGameRoomsStatement; + SQLiteStatementPtr getActiveAccountsStatement; + SQLiteStatementPtr getAccountGameRoomStatement; + SQLiteStatementPtr getAccountDisplayNameStatement; + SQLiteStatementPtr countRoomUsedSlotsStatement; + SQLiteStatementPtr countRoomTotalSlotsStatement; + + SQLiteStatementPtr isAccountCookieValidStatement; + SQLiteStatementPtr isGameRoomCookieValidStatement; + SQLiteStatementPtr isPlayerInGameRoomStatement; + SQLiteStatementPtr isPlayerInAnyGameRoomStatement; + SQLiteStatementPtr isAccountIDExistsStatement; + SQLiteStatementPtr isAccountNameExistsStatement; + + void prepareStatements(); + void createTables(); + void clearOldData(); + +public: + explicit LobbyDatabase(const boost::filesystem::path & databasePath); + ~LobbyDatabase(); + + void setAccountOnline(const std::string & accountID, bool isOnline); + void setGameRoomStatus(const std::string & roomID, LobbyRoomState roomStatus); + + void insertPlayerIntoGameRoom(const std::string & accountID, const std::string & roomID); + void deletePlayerFromGameRoom(const std::string & accountID, const std::string & roomID); + + void deleteGameRoomInvite(const std::string & targetAccountID, const std::string & roomID); + void insertGameRoomInvite(const std::string & targetAccountID, const std::string & roomID); + + void insertGameRoom(const std::string & roomID, const std::string & hostAccountID); + void insertAccount(const std::string & accountID, const std::string & displayName); + void insertAccessCookie(const std::string & accountID, const std::string & accessCookieUUID); + void insertChatMessage(const std::string & sender, const std::string & roomType, const std::string & roomID, const std::string & messageText); + + void updateAccountLoginTime(const std::string & accountID); + + std::vector getActiveGameRooms(); + std::vector getActiveAccounts(); + std::vector getRecentMessageHistory(); + + std::string getIdleGameRoom(const std::string & hostAccountID); + std::string getAccountGameRoom(const std::string & accountID); + std::string getAccountDisplayName(const std::string & accountID); + + LobbyCookieStatus getAccountCookieStatus(const std::string & accountID, const std::string & accessCookieUUID); + LobbyInviteStatus getAccountInviteStatus(const std::string & accountID, const std::string & roomID); + LobbyRoomState getGameRoomStatus(const std::string & roomID); + uint32_t getGameRoomFreeSlots(const std::string & roomID); + + bool isPlayerInGameRoom(const std::string & accountID); + bool isPlayerInGameRoom(const std::string & accountID, const std::string & roomID); + bool isAccountNameExists(const std::string & displayName); + bool isAccountIDExists(const std::string & accountID); +}; diff --git a/lobby/LobbyDefines.h b/lobby/LobbyDefines.h new file mode 100644 index 000000000..b2b323f65 --- /dev/null +++ b/lobby/LobbyDefines.h @@ -0,0 +1,57 @@ +/* + * LobbyDefines.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +struct LobbyAccount +{ + std::string accountID; + std::string displayName; +}; + +struct LobbyGameRoom +{ + std::string roomID; + std::string hostAccountID; + std::string hostAccountDisplayName; + std::string roomStatus; + uint32_t playersCount; + uint32_t playersLimit; +}; + +struct LobbyChatMessage +{ + std::string accountID; + std::string displayName; + std::string messageText; + std::chrono::seconds age; +}; + +enum class LobbyCookieStatus : int32_t +{ + INVALID, + VALID +}; + +enum class LobbyInviteStatus : int32_t +{ + NOT_INVITED, + INVITED, + DECLINED +}; + +enum class LobbyRoomState : int32_t +{ + IDLE = 0, // server is ready but no players are in the room + PUBLIC = 1, // host has joined and allows anybody to join + PRIVATE = 2, // host has joined but only allows those he invited to join + //BUSY = 3, // match is ongoing + //CANCELLED = 4, // game room was cancelled without starting the game + CLOSED = 5, // game room was closed after playing for some time +}; diff --git a/lobby/LobbyServer.cpp b/lobby/LobbyServer.cpp new file mode 100644 index 000000000..45af54a1a --- /dev/null +++ b/lobby/LobbyServer.cpp @@ -0,0 +1,592 @@ +/* + * LobbyServer.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "LobbyServer.h" + +#include "LobbyDatabase.h" + +#include "../lib/json/JsonNode.h" + +#include +#include + +bool LobbyServer::isAccountNameValid(const std::string & accountName) const +{ + if(accountName.size() < 4) + return false; + + if(accountName.size() > 20) + return false; + + for(const auto & c : accountName) + if(!std::isalnum(c)) + return false; + + return true; +} + +std::string LobbyServer::sanitizeChatMessage(const std::string & inputString) const +{ + // TODO: sanitize message and remove any "weird" symbols from it + return inputString; +} + +NetworkConnectionPtr LobbyServer::findAccount(const std::string & accountID) const +{ + for(const auto & account : activeAccounts) + if(account.second == accountID) + return account.first; + + return nullptr; +} + +NetworkConnectionPtr LobbyServer::findGameRoom(const std::string & gameRoomID) const +{ + for(const auto & account : activeGameRooms) + if(account.second == gameRoomID) + return account.first; + + return nullptr; +} + +void LobbyServer::sendMessage(const NetworkConnectionPtr & target, const JsonNode & json) +{ + target->sendPacket(json.toBytes()); +} + +void LobbyServer::sendAccountCreated(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & accountCookie) +{ + JsonNode reply; + reply["type"].String() = "accountCreated"; + reply["accountID"].String() = accountID; + reply["accountCookie"].String() = accountCookie; + sendMessage(target, reply); +} + +void LobbyServer::sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID) +{ + JsonNode reply; + reply["type"].String() = "inviteReceived"; + reply["accountID"].String() = accountID; + reply["gameRoomID"].String() = gameRoomID; + sendMessage(target, reply); +} + +void LobbyServer::sendOperationFailed(const NetworkConnectionPtr & target, const std::string & reason) +{ + JsonNode reply; + reply["type"].String() = "operationFailed"; + reply["reason"].String() = reason; + sendMessage(target, reply); +} + +void LobbyServer::sendLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie, const std::string & displayName) +{ + JsonNode reply; + reply["type"].String() = "loginSuccess"; + reply["accountCookie"].String() = accountCookie; + if(!displayName.empty()) + reply["displayName"].String() = displayName; + sendMessage(target, reply); +} + +void LobbyServer::sendChatHistory(const NetworkConnectionPtr & target, const std::vector & history) +{ + JsonNode reply; + reply["type"].String() = "chatHistory"; + + for(const auto & message : boost::adaptors::reverse(history)) + { + JsonNode jsonEntry; + + jsonEntry["accountID"].String() = message.accountID; + jsonEntry["displayName"].String() = message.displayName; + jsonEntry["messageText"].String() = message.messageText; + jsonEntry["ageSeconds"].Integer() = message.age.count(); + + reply["messages"].Vector().push_back(jsonEntry); + } + + sendMessage(target, reply); +} + +void LobbyServer::broadcastActiveAccounts() +{ + auto activeAccountsStats = database->getActiveAccounts(); + + JsonNode reply; + reply["type"].String() = "activeAccounts"; + + for(const auto & account : activeAccountsStats) + { + JsonNode jsonEntry; + jsonEntry["accountID"].String() = account.accountID; + jsonEntry["displayName"].String() = account.displayName; + jsonEntry["status"].String() = "In Lobby"; // TODO: in room status, in match status, offline status(?) + reply["accounts"].Vector().push_back(jsonEntry); + } + + for(const auto & connection : activeAccounts) + sendMessage(connection.first, reply); +} + +JsonNode LobbyServer::prepareActiveGameRooms() +{ + auto activeGameRoomStats = database->getActiveGameRooms(); + JsonNode reply; + reply["type"].String() = "activeGameRooms"; + + for(const auto & gameRoom : activeGameRoomStats) + { + JsonNode jsonEntry; + jsonEntry["gameRoomID"].String() = gameRoom.roomID; + jsonEntry["hostAccountID"].String() = gameRoom.hostAccountID; + jsonEntry["hostAccountDisplayName"].String() = gameRoom.hostAccountDisplayName; + jsonEntry["description"].String() = "TODO: ROOM DESCRIPTION"; + jsonEntry["playersCount"].Integer() = gameRoom.playersCount; + jsonEntry["playersLimit"].Integer() = gameRoom.playersLimit; + reply["gameRooms"].Vector().push_back(jsonEntry); + } + + return reply; +} + +void LobbyServer::broadcastActiveGameRooms() +{ + auto reply = prepareActiveGameRooms(); + + for(const auto & connection : activeAccounts) + sendMessage(connection.first, reply); +} + +void LobbyServer::sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID) +{ + JsonNode reply; + reply["type"].String() = "accountJoinsRoom"; + reply["accountID"].String() = accountID; + sendMessage(target, reply); +} + +void LobbyServer::sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID, bool proxyMode) +{ + JsonNode reply; + reply["type"].String() = "joinRoomSuccess"; + reply["gameRoomID"].String() = gameRoomID; + reply["proxyMode"].Bool() = proxyMode; + sendMessage(target, reply); +} + +void LobbyServer::sendChatMessage(const NetworkConnectionPtr & target, const std::string & roomMode, const std::string & roomName, const std::string & accountID, const std::string & displayName, const std::string & messageText) +{ + JsonNode reply; + reply["type"].String() = "chatMessage"; + reply["messageText"].String() = messageText; + reply["accountID"].String() = accountID; + reply["displayName"].String() = displayName; + reply["roomMode"].String() = roomMode; + reply["roomName"].String() = roomName; + + sendMessage(target, reply); +} + +void LobbyServer::onNewConnection(const NetworkConnectionPtr & connection) +{ + // no-op - waiting for incoming data +} + +void LobbyServer::onDisconnected(const NetworkConnectionPtr & connection, const std::string & errorMessage) +{ + if(activeAccounts.count(connection)) + { + database->setAccountOnline(activeAccounts.at(connection), false); + activeAccounts.erase(connection); + } + + if(activeGameRooms.count(connection)) + { + database->setGameRoomStatus(activeGameRooms.at(connection), LobbyRoomState::CLOSED); + activeGameRooms.erase(connection); + } + + if(activeProxies.count(connection)) + { + const auto & otherConnection = activeProxies.at(connection); + + if (otherConnection) + otherConnection->close(); + + activeProxies.erase(connection); + activeProxies.erase(otherConnection); + } + + broadcastActiveAccounts(); + broadcastActiveGameRooms(); +} + +void LobbyServer::onPacketReceived(const NetworkConnectionPtr & connection, const std::vector & message) +{ + // proxy connection - no processing, only redirect + if(activeProxies.count(connection)) + { + auto lockedPtr = activeProxies.at(connection); + if(lockedPtr) + return lockedPtr->sendPacket(message); + + logGlobal->info("Received unexpected message for inactive proxy!"); + } + + JsonNode json(message.data(), message.size()); + + // TODO: check for json parsing errors + // TODO: validate json based on received message type + + std::string messageType = json["type"].String(); + + // communication messages from vcmiclient + if(activeAccounts.count(connection)) + { + std::string accountName = activeAccounts.at(connection); + logGlobal->info("%s: Received message of type %s", accountName, messageType); + + if(messageType == "sendChatMessage") + return receiveSendChatMessage(connection, json); + + if(messageType == "openGameRoom") + return receiveOpenGameRoom(connection, json); + + if(messageType == "joinGameRoom") + return receiveJoinGameRoom(connection, json); + + if(messageType == "sendInvite") + return receiveSendInvite(connection, json); + + if(messageType == "declineInvite") + return receiveDeclineInvite(connection, json); + + logGlobal->warn("%s: Unknown message type: %s", accountName, messageType); + return; + } + + // communication messages from vcmiserver + if(activeGameRooms.count(connection)) + { + std::string roomName = activeGameRooms.at(connection); + logGlobal->info("%s: Received message of type %s", roomName, messageType); + + if(messageType == "leaveGameRoom") + return receiveLeaveGameRoom(connection, json); + + logGlobal->warn("%s: Unknown message type: %s", roomName, messageType); + return; + } + + logGlobal->info("(unauthorised): Received message of type %s", messageType); + + // unauthorized connections - permit only login or register attempts + if(messageType == "clientLogin") + return receiveClientLogin(connection, json); + + if(messageType == "clientRegister") + return receiveClientRegister(connection, json); + + if(messageType == "serverLogin") + return receiveServerLogin(connection, json); + + if(messageType == "clientProxyLogin") + return receiveClientProxyLogin(connection, json); + + if(messageType == "serverProxyLogin") + return receiveServerProxyLogin(connection, json); + + connection->close(); + logGlobal->info("(unauthorised): Unknown message type %s", messageType); +} + +void LobbyServer::receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = activeAccounts[connection]; + std::string messageText = json["messageText"].String(); + std::string messageTextClean = sanitizeChatMessage(messageText); + std::string displayName = database->getAccountDisplayName(accountID); + + if(messageTextClean.empty()) + return sendOperationFailed(connection, "No printable characters in sent message!"); + + database->insertChatMessage(accountID, "global", "english", messageText); + + for(const auto & otherConnection : activeAccounts) + sendChatMessage(otherConnection.first, "global", "english", accountID, displayName, messageText); +} + +void LobbyServer::receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string displayName = json["displayName"].String(); + std::string language = json["language"].String(); + + if(isAccountNameValid(displayName)) + return sendOperationFailed(connection, "Illegal account name"); + + if(database->isAccountNameExists(displayName)) + return sendOperationFailed(connection, "Account name already in use"); + + std::string accountCookie = boost::uuids::to_string(boost::uuids::random_generator()()); + std::string accountID = boost::uuids::to_string(boost::uuids::random_generator()()); + + database->insertAccount(accountID, displayName); + database->insertAccessCookie(accountID, accountCookie); + + sendAccountCreated(connection, accountID, accountCookie); +} + +void LobbyServer::receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + std::string language = json["language"].String(); + std::string version = json["version"].String(); + + if(!database->isAccountIDExists(accountID)) + return sendOperationFailed(connection, "Account not found"); + + auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie); + + if(clientCookieStatus == LobbyCookieStatus::INVALID) + return sendOperationFailed(connection, "Authentification failure"); + + database->updateAccountLoginTime(accountID); + database->setAccountOnline(accountID, true); + + std::string displayName = database->getAccountDisplayName(accountID); + + activeAccounts[connection] = accountID; + + sendLoginSuccess(connection, accountCookie, displayName); + sendChatHistory(connection, database->getRecentMessageHistory()); + + // send active game rooms list to new account + // and update acount list to everybody else including new account + broadcastActiveAccounts(); + sendMessage(connection, prepareActiveGameRooms()); +} + +void LobbyServer::receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string accountID = json["accountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + std::string version = json["version"].String(); + + auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie); + + if(clientCookieStatus == LobbyCookieStatus::INVALID) + { + sendOperationFailed(connection, "Invalid credentials"); + } + else + { + database->insertGameRoom(gameRoomID, accountID); + activeGameRooms[connection] = gameRoomID; + sendLoginSuccess(connection, accountCookie, {}); + broadcastActiveGameRooms(); + } +} + +void LobbyServer::receiveClientProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string accountID = json["accountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + + auto clientCookieStatus = database->getAccountCookieStatus(accountID, accountCookie); + + if(clientCookieStatus != LobbyCookieStatus::INVALID) + { + for(auto & proxyEntry : awaitingProxies) + { + if(proxyEntry.accountID != accountID) + continue; + if(proxyEntry.roomID != gameRoomID) + continue; + + proxyEntry.accountConnection = connection; + + auto gameRoomConnection = proxyEntry.roomConnection.lock(); + + if(gameRoomConnection) + { + activeProxies[gameRoomConnection] = connection; + activeProxies[connection] = gameRoomConnection; + } + return; + } + } + + sendOperationFailed(connection, "Invalid credentials"); + connection->close(); +} + +void LobbyServer::receiveServerProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string guestAccountID = json["guestAccountID"].String(); + std::string accountCookie = json["accountCookie"].String(); + + // FIXME: find host account ID and validate his cookie + //auto clientCookieStatus = database->getAccountCookieStatus(hostAccountID, accountCookie, accountCookieLifetime); + + //if(clientCookieStatus != LobbyCookieStatus::INVALID) + { + NetworkConnectionPtr targetAccount = findAccount(guestAccountID); + + if(targetAccount == nullptr) + { + sendOperationFailed(connection, "Invalid credentials"); + return; // unknown / disconnected account + } + + sendJoinRoomSuccess(targetAccount, gameRoomID, true); + + AwaitingProxyState proxy; + proxy.accountID = guestAccountID; + proxy.roomID = gameRoomID; + proxy.roomConnection = connection; + awaitingProxies.push_back(proxy); + return; + } + + //connection->close(); +} + +void LobbyServer::receiveOpenGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string hostAccountID = json["hostAccountID"].String(); + std::string accountID = activeAccounts[connection]; + + if(database->isPlayerInGameRoom(accountID)) + return sendOperationFailed(connection, "Player already in the room!"); + + std::string gameRoomID = database->getIdleGameRoom(hostAccountID); + if(gameRoomID.empty()) + return sendOperationFailed(connection, "Failed to find idle server to join!"); + + std::string roomType = json["roomType"].String(); + if(roomType != "public" && roomType != "private") + return sendOperationFailed(connection, "Invalid room type!"); + + if(roomType == "public") + database->setGameRoomStatus(gameRoomID, LobbyRoomState::PUBLIC); + if(roomType == "private") + database->setGameRoomStatus(gameRoomID, LobbyRoomState::PRIVATE); + + database->insertPlayerIntoGameRoom(accountID, gameRoomID); + broadcastActiveGameRooms(); + sendJoinRoomSuccess(connection, gameRoomID, false); +} + +void LobbyServer::receiveJoinGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string gameRoomID = json["gameRoomID"].String(); + std::string accountID = activeAccounts[connection]; + + if(database->isPlayerInGameRoom(accountID)) + return sendOperationFailed(connection, "Player already in the room!"); + + NetworkConnectionPtr targetRoom = findGameRoom(gameRoomID); + + if(targetRoom == nullptr) + return sendOperationFailed(connection, "Failed to find game room to join!"); + + auto roomStatus = database->getGameRoomStatus(gameRoomID); + + if(roomStatus != LobbyRoomState::PRIVATE && roomStatus != LobbyRoomState::PUBLIC) + return sendOperationFailed(connection, "Room does not accepts new players!"); + + if(roomStatus == LobbyRoomState::PRIVATE) + { + if(database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::INVITED) + return sendOperationFailed(connection, "You are not permitted to join private room without invite!"); + } + + if(database->getGameRoomFreeSlots(gameRoomID) == 0) + return sendOperationFailed(connection, "Room is already full!"); + + database->insertPlayerIntoGameRoom(accountID, gameRoomID); + sendAccountJoinsRoom(targetRoom, accountID); + //No reply to client - will be sent once match server establishes proxy connection with lobby + + broadcastActiveGameRooms(); +} + +void LobbyServer::receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + std::string gameRoomID = activeGameRooms[connection]; + + if(!database->isPlayerInGameRoom(accountID, gameRoomID)) + return sendOperationFailed(connection, "You are not in the room!"); + + database->deletePlayerFromGameRoom(accountID, gameRoomID); + + broadcastActiveGameRooms(); +} + +void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string senderName = activeAccounts[connection]; + std::string accountID = json["accountID"].String(); + std::string gameRoomID = database->getAccountGameRoom(senderName); + + auto targetAccount = findAccount(accountID); + + if(!targetAccount) + return sendOperationFailed(connection, "Invalid account to invite!"); + + if(!database->isPlayerInGameRoom(senderName)) + return sendOperationFailed(connection, "You are not in the room!"); + + if(database->isPlayerInGameRoom(accountID)) + return sendOperationFailed(connection, "This player is already in a room!"); + + if(database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::NOT_INVITED) + return sendOperationFailed(connection, "This player is already invited!"); + + database->insertGameRoomInvite(accountID, gameRoomID); + sendInviteReceived(targetAccount, senderName, gameRoomID); +} + +void LobbyServer::receiveDeclineInvite(const NetworkConnectionPtr & connection, const JsonNode & json) +{ + std::string accountID = activeAccounts[connection]; + std::string gameRoomID = json["gameRoomID"].String(); + + if(database->getAccountInviteStatus(accountID, gameRoomID) != LobbyInviteStatus::INVITED) + return sendOperationFailed(connection, "No active invite found!"); + + database->deleteGameRoomInvite(accountID, gameRoomID); +} + +LobbyServer::~LobbyServer() = default; + +LobbyServer::LobbyServer(const boost::filesystem::path & databasePath) + : database(std::make_unique(databasePath)) + , networkHandler(INetworkHandler::createHandler()) + , networkServer(networkHandler->createServerTCP(*this)) +{ +} + +void LobbyServer::start(uint16_t port) +{ + networkServer->start(port); +} + +void LobbyServer::run() +{ + networkHandler->run(); +} diff --git a/lobby/LobbyServer.h b/lobby/LobbyServer.h new file mode 100644 index 000000000..e61b0111c --- /dev/null +++ b/lobby/LobbyServer.h @@ -0,0 +1,92 @@ +/* + * LobbyServer.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/network/NetworkInterface.h" +#include "LobbyDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class LobbyDatabase; + +class LobbyServer final : public INetworkServerListener +{ + struct AwaitingProxyState + { + std::string accountID; + std::string roomID; + NetworkConnectionWeakPtr accountConnection; + NetworkConnectionWeakPtr roomConnection; + }; + + /// list of connected proxies. All messages received from (key) will be redirected to (value) connection + std::map activeProxies; + + /// list of half-established proxies from server that are still waiting for client to connect + std::vector awaitingProxies; + + /// list of logged in accounts (vcmiclient's) + std::map activeAccounts; + + /// list of currently logged in game rooms (vcmiserver's) + std::map activeGameRooms; + + std::unique_ptr database; + std::unique_ptr networkHandler; + std::unique_ptr networkServer; + + std::string sanitizeChatMessage(const std::string & inputString) const; + bool isAccountNameValid(const std::string & accountName) const; + + NetworkConnectionPtr findAccount(const std::string & accountID) const; + NetworkConnectionPtr findGameRoom(const std::string & gameRoomID) const; + + void onNewConnection(const NetworkConnectionPtr & connection) override; + void onDisconnected(const NetworkConnectionPtr & connection, const std::string & errorMessage) override; + void onPacketReceived(const NetworkConnectionPtr & connection, const std::vector & message) override; + + void sendMessage(const NetworkConnectionPtr & target, const JsonNode & json); + + void broadcastActiveAccounts(); + void broadcastActiveGameRooms(); + + JsonNode prepareActiveGameRooms(); + + void sendChatMessage(const NetworkConnectionPtr & target, const std::string & roomMode, const std::string & roomName, const std::string & accountID, const std::string & displayName, const std::string & messageText); + void sendAccountCreated(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & accountCookie); + void sendOperationFailed(const NetworkConnectionPtr & target, const std::string & reason); + void sendLoginSuccess(const NetworkConnectionPtr & target, const std::string & accountCookie, const std::string & displayName); + void sendChatHistory(const NetworkConnectionPtr & target, const std::vector &); + void sendAccountJoinsRoom(const NetworkConnectionPtr & target, const std::string & accountID); + void sendJoinRoomSuccess(const NetworkConnectionPtr & target, const std::string & gameRoomID, bool proxyMode); + void sendInviteReceived(const NetworkConnectionPtr & target, const std::string & accountID, const std::string & gameRoomID); + + void receiveClientRegister(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveClientLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveServerLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveClientProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveServerProxyLogin(const NetworkConnectionPtr & connection, const JsonNode & json); + + void receiveSendChatMessage(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveOpenGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveJoinGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveLeaveGameRoom(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveSendInvite(const NetworkConnectionPtr & connection, const JsonNode & json); + void receiveDeclineInvite(const NetworkConnectionPtr & connection, const JsonNode & json); + +public: + explicit LobbyServer(const boost::filesystem::path & databasePath); + ~LobbyServer(); + + void start(uint16_t port); + void run(); +}; diff --git a/lobby/SQLiteConnection.cpp b/lobby/SQLiteConnection.cpp new file mode 100644 index 000000000..439ad839b --- /dev/null +++ b/lobby/SQLiteConnection.cpp @@ -0,0 +1,194 @@ +/* + * SQLiteConnection.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "SQLiteConnection.h" + +#include + +[[noreturn]] static void handleSQLiteError(sqlite3 * connection) +{ + const char * message = sqlite3_errmsg(connection); + throw std::runtime_error(std::string("SQLite error: ") + message); +} + +static void checkSQLiteError(sqlite3 * connection, int result) +{ + if(result != SQLITE_OK) + handleSQLiteError(connection); +} + +SQLiteStatement::SQLiteStatement(SQLiteInstance & instance, sqlite3_stmt * statement) + : m_instance(instance) + , m_statement(statement) +{ +} + +SQLiteStatement::~SQLiteStatement() +{ + sqlite3_finalize(m_statement); +} + +bool SQLiteStatement::execute() +{ + int result = sqlite3_step(m_statement); + + switch(result) + { + case SQLITE_DONE: + return false; + case SQLITE_ROW: + return true; + default: + checkSQLiteError(m_instance.m_connection, result); + return false; + } +} + +void SQLiteStatement::reset() +{ + int result = sqlite3_reset(m_statement); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::clear() +{ + int result = sqlite3_clear_bindings(m_statement); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const double & value) +{ + int result = sqlite3_bind_double(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const bool & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(value), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const uint8_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const uint16_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} +void SQLiteStatement::setBindSingle(size_t index, const uint32_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const int32_t & value) +{ + int result = sqlite3_bind_int(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const int64_t & value) +{ + int result = sqlite3_bind_int64(m_statement, static_cast(index), value); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const std::string & value) +{ + int result = sqlite3_bind_text(m_statement, static_cast(index), value.data(), static_cast(value.size()), SQLITE_STATIC); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::setBindSingle(size_t index, const char * value) +{ + int result = sqlite3_bind_text(m_statement, static_cast(index), value, -1, SQLITE_STATIC); + checkSQLiteError(m_instance.m_connection, result); +} + +void SQLiteStatement::getColumnSingle(size_t index, double & value) +{ + value = sqlite3_column_double(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, bool & value) +{ + value = sqlite3_column_int(m_statement, static_cast(index)) != 0; +} + +void SQLiteStatement::getColumnSingle(size_t index, uint8_t & value) +{ + value = static_cast(sqlite3_column_int(m_statement, static_cast(index))); +} + +void SQLiteStatement::getColumnSingle(size_t index, uint16_t & value) +{ + value = static_cast(sqlite3_column_int(m_statement, static_cast(index))); +} + +void SQLiteStatement::getColumnSingle(size_t index, int32_t & value) +{ + value = sqlite3_column_int(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, uint32_t & value) +{ + value = sqlite3_column_int(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, int64_t & value) +{ + value = sqlite3_column_int64(m_statement, static_cast(index)); +} + +void SQLiteStatement::getColumnSingle(size_t index, std::string & value) +{ + const auto * value_raw = sqlite3_column_text(m_statement, static_cast(index)); + value = reinterpret_cast(value_raw); +} + +SQLiteInstancePtr SQLiteInstance::open(const boost::filesystem::path & db_path, bool allow_write) +{ + int flags = allow_write ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : SQLITE_OPEN_READONLY; + + sqlite3 * connection; + int result = sqlite3_open_v2(db_path.c_str(), &connection, flags, nullptr); + + if(result == SQLITE_OK) + return SQLiteInstancePtr(new SQLiteInstance(connection)); + + sqlite3_close(connection); + handleSQLiteError(connection); +} + +SQLiteInstance::SQLiteInstance(sqlite3 * connection) + : m_connection(connection) +{ +} + +SQLiteInstance::~SQLiteInstance() +{ + sqlite3_close(m_connection); +} + +SQLiteStatementPtr SQLiteInstance::prepare(const std::string & sql_text) +{ + sqlite3_stmt * statement; + int result = sqlite3_prepare_v2(m_connection, sql_text.data(), static_cast(sql_text.size()), &statement, nullptr); + + if(result == SQLITE_OK) + return SQLiteStatementPtr(new SQLiteStatement(*this, statement)); + + sqlite3_finalize(statement); + handleSQLiteError(m_connection); +} diff --git a/lobby/SQLiteConnection.h b/lobby/SQLiteConnection.h new file mode 100644 index 000000000..834caec88 --- /dev/null +++ b/lobby/SQLiteConnection.h @@ -0,0 +1,115 @@ +/* + * SQLiteConnection.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +using sqlite3 = struct sqlite3; +using sqlite3_stmt = struct sqlite3_stmt; + +class SQLiteInstance; +class SQLiteStatement; + +using SQLiteInstancePtr = std::unique_ptr; +using SQLiteStatementPtr = std::unique_ptr; + +class SQLiteStatement : boost::noncopyable +{ +public: + friend class SQLiteInstance; + + bool execute(); + void reset(); + void clear(); + + ~SQLiteStatement(); + + template + void executeOnce(const Args &... args) + { + setBinds(args...); + execute(); + reset(); + } + + template + void setBinds(const Args &... args) + { + setBindSingle(1, args...); // The leftmost SQL parameter has an index of 1 + } + + template + void getColumns(Args &... args) + { + getColumnSingle(0, args...); // The leftmost column of the result set has the index 0 + } + +private: + void setBindSingle(size_t index, const double & value); + void setBindSingle(size_t index, const bool & value); + void setBindSingle(size_t index, const uint8_t & value); + void setBindSingle(size_t index, const uint16_t & value); + void setBindSingle(size_t index, const uint32_t & value); + void setBindSingle(size_t index, const int32_t & value); + void setBindSingle(size_t index, const int64_t & value); + void setBindSingle(size_t index, const std::string & value); + void setBindSingle(size_t index, const char * value); + + void getColumnSingle(size_t index, double & value); + void getColumnSingle(size_t index, bool & value); + void getColumnSingle(size_t index, uint8_t & value); + void getColumnSingle(size_t index, uint16_t & value); + void getColumnSingle(size_t index, uint32_t & value); + void getColumnSingle(size_t index, int32_t & value); + void getColumnSingle(size_t index, int64_t & value); + void getColumnSingle(size_t index, std::string & value); + + template + void getColumnSingle(size_t index, std::chrono::duration & value) + { + int64_t durationValue = 0; + getColumnSingle(index, durationValue); + value = std::chrono::duration(durationValue); + } + + SQLiteStatement(SQLiteInstance & instance, sqlite3_stmt * statement); + + template + void setBindSingle(size_t index, T const & arg, const Args &... args) + { + setBindSingle(index, arg); + setBindSingle(index + 1, args...); + } + + template + void getColumnSingle(size_t index, T & arg, Args &... args) + { + getColumnSingle(index, arg); + getColumnSingle(index + 1, args...); + } + + SQLiteInstance & m_instance; + sqlite3_stmt * m_statement; +}; + +class SQLiteInstance : boost::noncopyable +{ +public: + friend class SQLiteStatement; + + static SQLiteInstancePtr open(const boost::filesystem::path & db_path, bool allow_write); + + ~SQLiteInstance(); + + SQLiteStatementPtr prepare(const std::string & statement); + +private: + explicit SQLiteInstance(sqlite3 * connection); + + sqlite3 * m_connection; +}; diff --git a/lobby/StdInc.cpp b/lobby/StdInc.cpp new file mode 100644 index 000000000..dd7f66cb8 --- /dev/null +++ b/lobby/StdInc.cpp @@ -0,0 +1,2 @@ +// Creates the precompiled header +#include "StdInc.h" diff --git a/lobby/StdInc.h b/lobby/StdInc.h new file mode 100644 index 000000000..d03216bdf --- /dev/null +++ b/lobby/StdInc.h @@ -0,0 +1,14 @@ +/* + * StdInc.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../Global.h" + +VCMI_LIB_USING_NAMESPACE diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 84e6a2be0..16019a65a 100644 --- a/mapeditor/Animation.cpp +++ b/mapeditor/Animation.cpp @@ -19,7 +19,7 @@ #include "../lib/vcmi_endian.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/ISimpleResourceLoader.h" -#include "../lib/JsonNode.h" +#include "../lib/json/JsonUtils.h" #include "../lib/CRandomGenerator.h" #include "../lib/VCMIDirs.h" @@ -62,7 +62,9 @@ class ImageLoader QImage * image; ui8 * lineStart; ui8 * position; - QPoint spriteSize, margins, fullSize; + QPoint spriteSize; + QPoint margins; + QPoint fullSize; public: //load size raw pixels from data inline void Load(size_t size, const ui8 * data); @@ -159,7 +161,7 @@ DefFile::DefFile(std::string Name): #endif // 0 //First 8 colors in def palette used for transparency - static QRgb H3Palette[8] = + constexpr std::array H3Palette = { qRgba(0, 0, 0, 0), // 100% - transparency qRgba(0, 0, 0, 32), // 75% - shadow border, @@ -301,7 +303,7 @@ std::shared_ptr DefFile::loadFrame(size_t frame, size_t group) const const ui32 BaseOffset = currentOffset; - std::shared_ptr img = std::make_shared(sprite.fullWidth, sprite.fullHeight, QImage::Format_Indexed8); + auto img = std::make_shared(sprite.fullWidth, sprite.fullHeight, QImage::Format_Indexed8); if(!img) throw std::runtime_error("Image memory cannot be allocated"); @@ -597,7 +599,7 @@ void Animation::init() std::unique_ptr textData(new ui8[stream->getSize()]); stream->read(textData.get(), stream->getSize()); - const JsonNode config((char*)textData.get(), stream->getSize()); + const JsonNode config(reinterpret_cast(textData.get()), stream->getSize()); initFromJson(config); } @@ -608,7 +610,7 @@ void Animation::initFromJson(const JsonNode & config) std::string basepath; basepath = config["basepath"].String(); - JsonNode base(JsonNode::JsonType::DATA_STRUCT); + JsonNode base; base["margins"] = config["margins"]; base["width"] = config["width"]; base["height"] = config["height"]; @@ -620,7 +622,7 @@ void Animation::initFromJson(const JsonNode & config) for(const JsonNode & frame : group["frames"].Vector()) { - JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); + JsonNode toAdd; JsonUtils::inherit(toAdd, base); toAdd["file"].String() = basepath + frame.String(); source[groupID].push_back(toAdd); @@ -635,7 +637,7 @@ void Animation::initFromJson(const JsonNode & config) if (source[group].size() <= frame) source[group].resize(frame+1); - JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); + JsonNode toAdd; JsonUtils::inherit(toAdd, base); toAdd["file"].String() = basepath + node["file"].String(); source[group][frame] = toAdd; diff --git a/mapeditor/Animation.h b/mapeditor/Animation.h index eb5342f0d..2e4556413 100644 --- a/mapeditor/Animation.h +++ b/mapeditor/Animation.h @@ -10,11 +10,14 @@ #pragma once //code is copied from vcmiclient/CAnimation.h with minimal changes -#include "../lib/JsonNode.h" #include "../lib/GameConstants.h" #include #include +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + /* * Base class for images, can be used for non-animation pictures as well */ diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 14c409be9..629534604 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -105,6 +105,8 @@ set(editor_FORMS ) set(editor_TS + translation/chinese.ts + translation/czech.ts translation/english.ts translation/french.ts translation/german.ts @@ -176,7 +178,7 @@ if(APPLE) set_property(GLOBAL PROPERTY AUTOGEN_TARGETS_FOLDER vcmieditor) endif() -target_link_libraries(vcmieditor ${VCMI_LIB_TARGET} Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) +target_link_libraries(vcmieditor vcmi Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network) target_include_directories(vcmieditor PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -186,8 +188,8 @@ enable_pch(vcmieditor) # Copy to build directory for easier debugging add_custom_command(TARGET vcmieditor POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/ - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/translation + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/translation ) install(TARGETS vcmieditor DESTINATION ${BIN_DIR}) diff --git a/mapeditor/StdInc.h b/mapeditor/StdInc.h index 4dc45a2ef..f877c4191 100644 --- a/mapeditor/StdInc.h +++ b/mapeditor/StdInc.h @@ -15,8 +15,8 @@ VCMI_LIB_USING_NAMESPACE -using NumericPointer = typename std::conditional::type; +using NumericPointer = typename std::conditional_t; template NumericPointer data_cast(Type * _pointer) diff --git a/mapeditor/graphics.cpp b/mapeditor/graphics.cpp index 147fa0577..73e80b2b3 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -28,7 +28,6 @@ #include "../CCallback.h" #include "../lib/CGeneralTextHandler.h" #include "BitmapHandler.h" -#include "../lib/JsonNode.h" #include "../lib/CStopWatch.h" #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" @@ -125,7 +124,7 @@ void Graphics::load() void Graphics::loadHeroAnimations() { - for(auto & elem : VLC->heroh->classes.objects) + for(auto & elem : VLC->heroclassesh->objects) { for(auto templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->getIndex())->getTemplates()) { @@ -184,7 +183,7 @@ std::shared_ptr Graphics::loadHeroFlagAnimation(const std::string & n {2,14}, {3,15} }; - std::shared_ptr anim = std::make_shared(name); + auto anim = std::make_shared(name); anim->preload(); for(const auto & rotation : rotations) @@ -207,7 +206,7 @@ std::shared_ptr Graphics::loadHeroAnimation(const std::string &name) {2,14}, {3,15} }; - std::shared_ptr anim = std::make_shared(name); + auto anim = std::make_shared(name); anim->preload(); diff --git a/mapeditor/graphics.h b/mapeditor/graphics.h index 009cf4399..1e568ff0e 100644 --- a/mapeditor/graphics.h +++ b/mapeditor/graphics.h @@ -22,10 +22,10 @@ class CGObjectInstance; class EntityService; class JsonNode; class ObjectTemplate; +class CHeroClass; VCMI_LIB_NAMESPACE_END -class CHeroClass; struct InfoAboutHero; struct InfoAboutTown; class Animation; diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp index 65740a54a..fcba00cf0 100644 --- a/mapeditor/inspector/armywidget.cpp +++ b/mapeditor/inspector/armywidget.cpp @@ -101,7 +101,7 @@ bool ArmyWidget::commitChanges() } } - army.setFormation(ui->formationTight->isChecked()); + army.setFormation(ui->formationTight->isChecked() ? EArmyFormation::TIGHT : EArmyFormation::LOOSE ); return isArmed; } diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index fe81dd27e..5ae80af4d 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -87,14 +87,6 @@ void Initializer::initialize(CGDwelling * o) if(!o) return; o->tempOwner = defaultPlayer; - - switch(o->ID) - { - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - o->initRandomObjectInfo(); - } } void Initializer::initialize(CGGarrison * o) @@ -150,7 +142,7 @@ void Initializer::initialize(CGHeroInstance * o) { if(t->heroClass->getId() == HeroClassID(o->subID)) { - o->type = t; + o->type = t.get(); break; } } @@ -194,7 +186,7 @@ void Initializer::initialize(CGArtifact * o) std::vector out; for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) { - if(VLC->spellh->getDefaultAllowed().at(spell->id)) + if(VLC->spellh->getDefaultAllowed().count(spell->id) != 0) { out.push_back(spell->id); } @@ -241,9 +233,9 @@ void Inspector::updateProperties(CGDwelling * o) { if(!o) return; - addProperty("Owner", o->tempOwner, false); + addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); - if(dynamic_cast(o->info)) + if (o->ID == Obj::RANDOM_DWELLING || o->ID == Obj::RANDOM_DWELLING_LVL) { auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); @@ -253,15 +245,15 @@ void Inspector::updateProperties(CGDwelling * o) void Inspector::updateProperties(CGLighthouse * o) { if(!o) return; - - addProperty("Owner", o->tempOwner, false); + + addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); } void Inspector::updateProperties(CGGarrison * o) { if(!o) return; - addProperty("Owner", o->tempOwner, false); + addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); addProperty("Removable units", o->removableUnits, false); } @@ -269,14 +261,14 @@ void Inspector::updateProperties(CGShipyard * o) { if(!o) return; - addProperty("Owner", o->tempOwner, false); + addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); } void Inspector::updateProperties(CGHeroPlaceholder * o) { if(!o) return; - addProperty("Owner", o->tempOwner, false); + addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); bool type = false; if(o->heroType.has_value()) @@ -306,7 +298,8 @@ void Inspector::updateProperties(CGHeroInstance * o) { if(!o) return; - addProperty("Owner", o->tempOwner, o->ID == Obj::PRISON); //field is not editable for prison + auto isPrison = o->ID == Obj::PRISON; + addProperty("Owner", o->tempOwner, new OwnerDelegate(controller, isPrison), isPrison); //field is not editable for prison addProperty("Experience", o->exp, false); addProperty("Hero class", o->type ? o->type->heroClass->getNameTranslated() : "", true); @@ -327,7 +320,7 @@ void Inspector::updateProperties(CGHeroInstance * o) auto * delegate = new InspectorDelegate; for(int i = 0; i < VLC->heroh->objects.size(); ++i) { - if(controller.map()->allowedHeroes.at(i)) + if(controller.map()->allowedHeroes.count(HeroTypeID(i)) != 0) { if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex())) { @@ -364,7 +357,7 @@ void Inspector::updateProperties(CGArtifact * o) auto * delegate = new InspectorDelegate; for(auto spell : VLC->spellh->objects) { - if(controller.map()->allowedSpells.at(spell->id)) + if(controller.map()->allowedSpells.count(spell->id) != 0) delegate->options.push_back({QObject::tr(spell->getNameTranslated().c_str()), QVariant::fromValue(int(spell->getId()))}); } addProperty("Spell", VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); @@ -376,9 +369,9 @@ void Inspector::updateProperties(CGMine * o) { if(!o) return; - addProperty("Owner", o->tempOwner, false); + addProperty("Owner", o->tempOwner, new OwnerDelegate(controller), false); addProperty("Resource", o->producedResource); - addProperty("Productivity", o->producedQuantity, false); + addProperty("Productivity", o->producedQuantity); } void Inspector::updateProperties(CGResource * o) @@ -482,12 +475,7 @@ void Inspector::updateProperties() addProperty("IsStatic", factory->isStaticObject()); } - auto * delegate = new InspectorDelegate(); - delegate->options.push_back({QObject::tr("neutral"), QVariant::fromValue(PlayerColor::NEUTRAL.getNum())}); - for(int p = 0; p < controller.map()->players.size(); ++p) - if(controller.map()->players[p].canAnyonePlay()) - delegate->options.push_back({QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[p]), QVariant::fromValue(PlayerColor(p).getNum())}); - addProperty("Owner", obj->tempOwner, delegate, true); + addProperty("Owner", obj->tempOwner, new OwnerDelegate(controller), true); UPDATE_OBJ_PROPERTIES(CArmedInstance); UPDATE_OBJ_PROPERTIES(CGResource); @@ -641,12 +629,12 @@ void Inspector::setProperty(CGDwelling * o, const QString & key, const QVariant if(key == "Same as town") { - if(auto * info = dynamic_cast(o->info)) - { - info->instanceId = ""; - if(CGTownInstance * town = data_cast(value.toLongLong())) - info->instanceId = town->instanceName; - } + if (!o->randomizationInfo.has_value()) + o->randomizationInfo = CGDwellingRandomizationInfo(); + + o->randomizationInfo->instanceId = ""; + if(CGTownInstance * town = data_cast(value.toLongLong())) + o->randomizationInfo->instanceId = town->instanceName; } } @@ -935,3 +923,12 @@ void InspectorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QStyledItemDelegate::setModelData(editor, model, index); } } + +OwnerDelegate::OwnerDelegate(MapController & controller, bool addNeutral) +{ + if(addNeutral) + options.push_back({QObject::tr("neutral"), QVariant::fromValue(PlayerColor::NEUTRAL.getNum()) }); + for(int p = 0; p < controller.map()->players.size(); ++p) + if(controller.map()->players[p].canAnyonePlay()) + options.push_back({QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[p]), QVariant::fromValue(PlayerColor(p).getNum()) }); +} diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 432e40ab0..1bb962555 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -128,8 +128,7 @@ protected: { itemKey = keyItems[key]; table->setItem(table->row(itemKey), 1, itemValue); - if(delegate) - table->setItemDelegateForRow(table->row(itemKey), delegate); + table->setItemDelegateForRow(table->row(itemKey), delegate); } else { @@ -139,8 +138,7 @@ protected: table->setRowCount(row + 1); table->setItem(row, 0, itemKey); table->setItem(row, 1, itemValue); - if(delegate) - table->setItemDelegateForRow(row, delegate); + table->setItemDelegateForRow(row, delegate); ++row; } itemKey->setFlags(restricted ? Qt::NoItemFlags : Qt::ItemIsEnabled); @@ -173,3 +171,11 @@ public: QList> options; }; + + +class OwnerDelegate : public InspectorDelegate +{ + Q_OBJECT +public: + OwnerDelegate(MapController & controller, bool addNeutral = true); +}; \ No newline at end of file diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index 97e22f586..982138741 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -54,7 +54,7 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!controller.map()->allowedArtifact[i]) + if(controller.map()->allowedArtifact.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); ui->lArtifacts->addItem(item); } @@ -66,7 +66,7 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!controller.map()->allowedSpells[i]) + if(controller.map()->allowedSpells.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); ui->lSpells->addItem(item); } @@ -82,7 +82,7 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par for(auto & s : NSecondarySkill::levels) widget->addItem(QString::fromStdString(s)); - if(!controller.map()->allowedAbilities[i]) + if(controller.map()->allowedAbilities.count(i) == 0) { item->setFlags(item->flags() & ~Qt::ItemIsEnabled); widget->setEnabled(false); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 6ec62d84b..373588a3c 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -73,7 +73,7 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!map.allowedArtifact[i]) + if(map.allowedArtifact.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); w->addItem(item); } @@ -88,7 +88,7 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!map.allowedSpells[i]) + if(map.allowedSpells.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); w->addItem(item); } @@ -115,7 +115,7 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : for(auto & s : NSecondarySkill::levels) widget->addItem(QString::fromStdString(s)); - if(!map.allowedAbilities[i]) + if(map.allowedAbilities.count(i) == 0) { item->setFlags(item->flags() & ~Qt::ItemIsEnabled); widget->setEnabled(false); diff --git a/mapeditor/jsonutils.cpp b/mapeditor/jsonutils.cpp index a10129c14..7c8f946ac 100644 --- a/mapeditor/jsonutils.cpp +++ b/mapeditor/jsonutils.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "jsonutils.h" +#include "../lib/json/JsonNode.h" + static QVariantMap JsonToMap(const JsonMap & json) { QVariantMap map; @@ -94,7 +96,7 @@ QVariant JsonFromFile(QString filename) } else { - JsonNode node(data.data(), data.size()); + JsonNode node(reinterpret_cast(data.data()), data.size()); return toVariant(node); } } @@ -120,7 +122,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); - file << toJson(object).toJson(); + file << toJson(object).toString(); } } diff --git a/mapeditor/jsonutils.h b/mapeditor/jsonutils.h index 6dd7e7bbf..791711eb0 100644 --- a/mapeditor/jsonutils.h +++ b/mapeditor/jsonutils.h @@ -10,10 +10,11 @@ #pragma once #include -#include "../lib/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; + namespace JsonUtils { QVariant toVariant(const JsonNode & node); diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index aaf5b19f5..b50353c0d 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -177,11 +177,11 @@ MainWindow::MainWindow(QWidget* parent) : logGlobal->info("The log file will be saved to %s", logPath); //init - preinitDLL(::console, false, extractionOptions.extractArchives); + preinitDLL(::console, extractionOptions.extractArchives); // Initialize logging based on settings logConfig->configure(); - logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); + logGlobal->debug("settings = %s", settings.toJsonNode().toString()); // Some basic data validation to produce better error messages in cases of incorrect install auto testFile = [](std::string filename, std::string message) -> bool @@ -347,7 +347,7 @@ std::unique_ptr MainWindow::openMapInternal(const QString & filenameSelect if(!modList.empty()) throw ModIncompatibility(modList); - return mapService.loadMap(resId); + return mapService.loadMap(resId, nullptr); } else throw std::runtime_error("Corrupted map"); @@ -538,14 +538,15 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust auto picture = animation.getImage(0); if(picture && picture->width() && picture->height()) { - qreal xscale = qreal(128) / qreal(picture->width()), yscale = qreal(128) / qreal(picture->height()); + qreal xscale = static_cast(128) / static_cast(picture->width()); + qreal yscale = static_cast(128) / static_cast(picture->height()); qreal scale = std::min(xscale, yscale); painter.scale(scale, scale); painter.drawImage(QPoint(0, 0), *picture); } //create object to extract name - std::unique_ptr temporaryObj(factory->create(templ)); + std::unique_ptr temporaryObj(factory->create(nullptr, templ)); QString translated = useCustomName ? QString::fromStdString(temporaryObj->getObjectName().c_str()) : subGroupName; itemType->setText(translated); @@ -585,7 +586,7 @@ void MainWindow::loadObjectsTree() //adding terrains for(auto & terrain : VLC->terrainTypeHandler->objects) { - QPushButton *b = new QPushButton(QString::fromStdString(terrain->getNameTranslated())); + auto *b = new QPushButton(QString::fromStdString(terrain->getNameTranslated())); ui->terrainLayout->addWidget(b); connect(b, &QPushButton::clicked, this, [this, terrain]{ terrainButtonClicked(terrain->getId()); }); @@ -600,7 +601,7 @@ void MainWindow::loadObjectsTree() //adding roads for(auto & road : VLC->roadTypeHandler->objects) { - QPushButton *b = new QPushButton(QString::fromStdString(road->getNameTranslated())); + auto *b = new QPushButton(QString::fromStdString(road->getNameTranslated())); ui->roadLayout->addWidget(b); connect(b, &QPushButton::clicked, this, [this, road]{ roadOrRiverButtonClicked(road->getIndex(), true); }); } @@ -609,7 +610,7 @@ void MainWindow::loadObjectsTree() //adding rivers for(auto & river : VLC->riverTypeHandler->objects) { - QPushButton *b = new QPushButton(QString::fromStdString(river->getNameTranslated())); + auto *b = new QPushButton(QString::fromStdString(river->getNameTranslated())); ui->riverLayout->addWidget(b); connect(b, &QPushButton::clicked, this, [this, river]{ roadOrRiverButtonClicked(river->getIndex(), false); }); } diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index 919267557..dc67f1eb5 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -174,7 +174,8 @@ private: ObjectBrowserProxyModel * objectBrowser = nullptr; QGraphicsScene * scenePreview; - QString filename, lastSavingDir; + QString filename; + QString lastSavingDir; bool unsaved = false; QStandardItemModel objectsModel; diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 4912541de..e768b3130 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -94,24 +94,6 @@ void MapController::repairMap(CMap * map) const if(!map) return; - //there might be extra skills, arts and spells not imported from map - if(VLC->skillh->getDefaultAllowed().size() > map->allowedAbilities.size()) - { - map->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size()); - } - if(VLC->arth->getDefaultAllowed().size() > map->allowedArtifact.size()) - { - map->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size()); - } - if(VLC->spellh->getDefaultAllowed().size() > map->allowedSpells.size()) - { - map->allowedSpells.resize(VLC->spellh->getDefaultAllowed().size()); - } - if(VLC->heroh->getDefaultAllowed().size() > map->allowedHeroes.size()) - { - map->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size()); - } - //make sure events/rumors has name to have proper identifiers int emptyNameId = 1; for(auto & e : map->events) @@ -149,7 +131,12 @@ void MapController::repairMap(CMap * map) const //fix hero instance if(auto * nih = dynamic_cast(obj.get())) { - map->allowedHeroes.at(nih->subID) = true; + // All heroes present on map or in prisons need to be allowed to rehire them after they are defeated + + // FIXME: How about custom scenarios where defeated hero cannot be hired again? + + map->allowedHeroes.insert(nih->getHeroType()); + auto type = VLC->heroh->objects[nih->subID]; assert(type->heroClass); //TODO: find a way to get proper type name @@ -166,7 +153,7 @@ void MapController::repairMap(CMap * map) const } if(obj->ID != Obj::RANDOM_HERO) - nih->type = type; + nih->type = type.get(); if(nih->ID == Obj::HERO) //not prison nih->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); @@ -216,8 +203,15 @@ void MapController::repairMap(CMap * map) const auto a = ArtifactUtils::createScroll(*RandomGeneratorUtil::nextItem(out, CRandomGenerator::getDefault())); art->storedArtifact = a; } - else - map->allowedArtifact.at(art->subID) = true; + } + //fix mines + if(auto * mine = dynamic_cast(obj.get())) + { + if(!mine->isAbandoned()) + { + mine->producedResource = GameResID(mine->subID); + mine->producedQuantity = mine->defaultResProduction(); + } } } } @@ -290,6 +284,8 @@ void MapController::resetMapHandler() void MapController::commitTerrainChange(int level, const TerrainId & terrain) { + static const int terrainDecorationPercentageLevel = 10; + std::vector v(_scenes[level]->selectionTerrainView.selection().begin(), _scenes[level]->selectionTerrainView.selection().end()); if(v.empty()) @@ -299,7 +295,7 @@ void MapController::commitTerrainChange(int level, const TerrainId & terrain) _scenes[level]->selectionTerrainView.draw(); _map->getEditManager()->getTerrainSelection().setSelection(v); - _map->getEditManager()->drawTerrain(terrain, &CRandomGenerator::getDefault()); + _map->getEditManager()->drawTerrain(terrain, terrainDecorationPercentageLevel, &CRandomGenerator::getDefault()); for(auto & t : v) _scenes[level]->terrainView.setDirty(t); @@ -623,7 +619,7 @@ ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) if(obj->ID == Obj::HERO) continue; //stub! - auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); + auto handler = obj->getObjectHandler(); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 18d660a3a..7b8a246eb 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -16,7 +16,7 @@ #include "../lib/modding/CModInfo.h" VCMI_LIB_NAMESPACE_BEGIN -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; class EditorObstaclePlacer; VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index 8d4ff8b30..f5db55ba0 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -18,10 +18,10 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/ObjectTemplate.h" +#include "../lib/mapObjects/MiscObjects.h" #include "../lib/CHeroHandler.h" #include "../lib/CTownHandler.h" #include "../lib/GameConstants.h" -#include "../lib/JsonDetail.h" const int tileSize = 32; @@ -100,7 +100,8 @@ void MapHandler::drawTerrainTile(QPainter & painter, int x, int y, int z) if(terrainImages.at(terrainName).size() <= tinfo.terView) return; - bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + bool hflip = (rotation == 1 || rotation == 3); + bool vflip = (rotation == 2 || rotation == 3); painter.drawImage(x * tileSize, y * tileSize, terrainImages.at(terrainName)[tinfo.terView]->mirrored(hflip, vflip)); } @@ -114,7 +115,8 @@ void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) auto roadName = tinfoUpper->roadType->getJsonKey(); QRect source(0, tileSize / 2, tileSize, tileSize / 2); ui8 rotation = (tinfoUpper->extTileFlags >> 4) % 4; - bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + bool hflip = (rotation == 1 || rotation == 3); + bool vflip = (rotation == 2 || rotation == 3); if(roadImages.at(roadName).size() > tinfoUpper->roadDir) { painter.drawImage(QPoint(x * tileSize, y * tileSize), roadImages.at(roadName)[tinfoUpper->roadDir]->mirrored(hflip, vflip), source); @@ -123,10 +125,11 @@ void MapHandler::drawRoad(QPainter & painter, int x, int y, int z) if(tinfo.roadType) //print road from this tile { - auto roadName = tinfo.roadType->getJsonKey();; + auto roadName = tinfo.roadType->getJsonKey(); QRect source(0, 0, tileSize, tileSize / 2); ui8 rotation = (tinfo.extTileFlags >> 4) % 4; - bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + bool hflip = (rotation == 1 || rotation == 3); + bool vflip = (rotation == 2 || rotation == 3); if(roadImages.at(roadName).size() > tinfo.roadDir) { painter.drawImage(QPoint(x * tileSize, y * tileSize + tileSize / 2), roadImages.at(roadName)[tinfo.roadDir]->mirrored(hflip, vflip), source); @@ -148,7 +151,8 @@ void MapHandler::drawRiver(QPainter & painter, int x, int y, int z) return; ui8 rotation = (tinfo.extTileFlags >> 2) % 4; - bool hflip = (rotation == 1 || rotation == 3), vflip = (rotation == 2 || rotation == 3); + bool hflip = (rotation == 1 || rotation == 3); + bool vflip = (rotation == 2 || rotation == 3); painter.drawImage(x * tileSize, y * tileSize, riverImages.at(riverName)[tinfo.riverDir]->mirrored(hflip, vflip)); } diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp index c15c52cd9..3ea3dcbe6 100644 --- a/mapeditor/mapsettings/abstractsettings.cpp +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -128,10 +128,8 @@ JsonNode AbstractSettings::conditionToJson(const EventCondition & event) JsonNode result; result["condition"].Integer() = event.condition; result["value"].Integer() = event.value; - result["objectType"].Integer() = event.objectType; - result["objectSubytype"].Integer() = event.objectSubtype; + result["objectType"].String() = event.objectType.toString(); result["objectInstanceName"].String() = event.objectInstanceName; - result["metaType"].Integer() = (ui8)event.metaType; { auto & position = result["position"].Vector(); position.resize(3); diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index a15599e84..74f405232 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -35,23 +35,23 @@ void GeneralSettings::initialize(MapController & c) //set difficulty switch(controller->map()->difficulty) { - case 0: + case EMapDifficulty::EASY: ui->diffRadio1->setChecked(true); break; - case 1: + case EMapDifficulty::NORMAL: ui->diffRadio2->setChecked(true); break; - case 2: + case EMapDifficulty::HARD: ui->diffRadio3->setChecked(true); break; - case 3: + case EMapDifficulty::EXPERT: ui->diffRadio4->setChecked(true); break; - case 4: + case EMapDifficulty::IMPOSSIBLE: ui->diffRadio5->setChecked(true); break; }; @@ -67,11 +67,11 @@ void GeneralSettings::update() controller->map()->levelLimit = 0; //set difficulty - if(ui->diffRadio1->isChecked()) controller->map()->difficulty = 0; - if(ui->diffRadio2->isChecked()) controller->map()->difficulty = 1; - if(ui->diffRadio3->isChecked()) controller->map()->difficulty = 2; - if(ui->diffRadio4->isChecked()) controller->map()->difficulty = 3; - if(ui->diffRadio5->isChecked()) controller->map()->difficulty = 4; + if(ui->diffRadio1->isChecked()) controller->map()->difficulty = EMapDifficulty::EASY; + if(ui->diffRadio2->isChecked()) controller->map()->difficulty = EMapDifficulty::NORMAL; + if(ui->diffRadio3->isChecked()) controller->map()->difficulty = EMapDifficulty::HARD; + if(ui->diffRadio4->isChecked()) controller->map()->difficulty = EMapDifficulty::EXPERT; + if(ui->diffRadio5->isChecked()) controller->map()->difficulty = EMapDifficulty::IMPOSSIBLE; } void GeneralSettings::on_heroLevelLimitCheck_toggled(bool checked) diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index e19962734..dc679b685 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -156,7 +156,7 @@ void LoseConditions::update() case 0: { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; + cond.objectType = Obj(Obj::TOWN); assert(loseTypeWidget); int townIdx = loseTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[townIdx]->pos; @@ -171,7 +171,7 @@ void LoseConditions::update() case 1: { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; + cond.objectType = Obj(Obj::HERO); assert(loseTypeWidget); int townIdx = loseTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[townIdx]->pos; diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 5b410cb56..978916e77 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -30,36 +30,36 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : show(); - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + for(auto objectPtr : VLC->skillh->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedAbilities[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedAbilities.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listAbilities->addItem(item); } - for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) + for(auto objectPtr : VLC->spellh->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->spellh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedSpells[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedSpells.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listSpells->addItem(item); } - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + for(auto objectPtr : VLC->arth->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedArtifact[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedArtifact.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listArts->addItem(item); } - for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) + for(auto objectPtr : VLC->heroh->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->heroh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedHeroes[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedHeroes.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listHeroes->addItem(item); } @@ -78,12 +78,14 @@ MapSettings::~MapSettings() void MapSettings::on_pushButton_clicked() { - auto updateMapArray = [](const QListWidget * widget, std::vector & arr) + auto updateMapArray = [](const QListWidget * widget, auto & arr) { - for(int i = 0; i < arr.size(); ++i) + arr.clear(); + for(int i = 0; i < widget->count(); ++i) { auto * item = widget->item(i); - arr[i] = item->checkState() == Qt::Checked; + if (item->checkState() == Qt::Checked) + arr.emplace(i); } }; diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp index 5926542e5..330cb9c82 100644 --- a/mapeditor/mapsettings/modsettings.cpp +++ b/mapeditor/mapsettings/modsettings.cpp @@ -78,9 +78,8 @@ void ModSettings::initialize(MapController & c) auto pieces = qmodName.split("."); assert(pieces.size() > 1); - QString qs; - for(int i = 0; i < pieces.size() - 1; ++i) - qs += pieces[i]; + pieces.pop_back(); + auto qs = pieces.join("."); if(addedMods.count(qs)) { diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index dc20d4db0..dd3f6e234 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -13,6 +13,7 @@ #include "ui_translations.h" #include "../../lib/Languages.h" #include "../../lib/CGeneralTextHandler.h" +#include "../../lib/mapObjects/CGObjectInstance.h" #include "../../lib/VCMI_Lib.h" void Translations::cleanupRemovedItems(CMap & map) @@ -23,7 +24,7 @@ void Translations::cleanupRemovedItems(CMap & map) for(auto & translations : map.translations.Struct()) { - auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT); + JsonNode updateTranslations; for(auto & s : translations.second.Struct()) { for(auto part : QString::fromStdString(s.first).split('.')) @@ -43,7 +44,7 @@ void Translations::cleanupRemovedItems(CMap & map, const std::string & pattern) { for(auto & translations : map.translations.Struct()) { - auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT); + JsonNode updateTranslations; for(auto & s : translations.second.Struct()) { if(s.first.find(pattern) == std::string::npos) @@ -170,7 +171,7 @@ void Translations::on_supportedCheck_toggled(bool checked) } ui->translationsTable->blockSignals(true); ui->translationsTable->setRowCount(0); - translation = JsonNode(JsonNode::JsonType::DATA_NULL); + translation.clear(); ui->translationsTable->blockSignals(false); ui->translationsTable->setEnabled(false); } diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index f1db91643..5b4efe9e6 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -219,7 +219,7 @@ void VictoryConditions::update() case 0: { EventCondition cond(EventCondition::HAVE_ARTIFACT); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = ArtifactID(victoryTypeWidget->currentData().toInt()); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); specialVictory.onFulfill.appendTextID("core.genrltxt.280"); specialVictory.trigger = EventExpression(cond); @@ -229,7 +229,7 @@ void VictoryConditions::update() case 1: { EventCondition cond(EventCondition::HAVE_CREATURES); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = CreatureID(victoryTypeWidget->currentData().toInt()); cond.value = victoryValueWidget->text().toInt(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); specialVictory.onFulfill.appendTextID("core.genrltxt.276"); @@ -240,7 +240,7 @@ void VictoryConditions::update() case 2: { EventCondition cond(EventCondition::HAVE_RESOURCES); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = GameResID(victoryTypeWidget->currentData().toInt()); cond.value = victoryValueWidget->text().toInt(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); specialVictory.onFulfill.appendTextID("core.genrltxt.278"); @@ -251,7 +251,7 @@ void VictoryConditions::update() case 3: { EventCondition cond(EventCondition::HAVE_BUILDING); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = BuildingID(victoryTypeWidget->currentData().toInt()); int townIdx = victorySelectWidget->currentData().toInt(); if(townIdx > -1) cond.position = controller->map()->objects[townIdx]->pos; @@ -264,7 +264,7 @@ void VictoryConditions::update() case 4: { EventCondition cond(EventCondition::CONTROL); assert(victoryTypeWidget); - cond.objectType = Obj::TOWN; + cond.objectType = Obj(Obj::TOWN); int townIdx = victoryTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[townIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); @@ -276,7 +276,7 @@ void VictoryConditions::update() case 5: { EventCondition cond(EventCondition::DESTROY); assert(victoryTypeWidget); - cond.objectType = Obj::HERO; + cond.objectType = Obj(Obj::HERO); int heroIdx = victoryTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[heroIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); @@ -288,7 +288,7 @@ void VictoryConditions::update() case 6: { EventCondition cond(EventCondition::TRANSPORT); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = ArtifactID(victoryTypeWidget->currentData().toInt()); int townIdx = victorySelectWidget->currentData().toInt(); if(townIdx > -1) cond.position = controller->map()->objects[townIdx]->pos; @@ -301,7 +301,7 @@ void VictoryConditions::update() case 7: { EventCondition cond(EventCondition::DESTROY); assert(victoryTypeWidget); - cond.objectType = Obj::MONSTER; + cond.objectType = Obj(Obj::MONSTER); int monsterIdx = victoryTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[monsterIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 52cb9f373..5e37b48ff 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -494,7 +494,8 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) //key: y position of tile //value.first: x position of left tile //value.second: x postiion of right tile - std::map> selectionRangeMapX, selectionRangeMapY; + std::map> selectionRangeMapX; + std::map> selectionRangeMapY; for(auto & t : sc->selectionTerrainView.selection()) { auto pairIter = selectionRangeMapX.find(t.y); @@ -516,7 +517,8 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) } } - std::set selectionByX, selectionByY; + std::set selectionByX; + std::set selectionByY; std::vector finalSelection; for(auto & selectionRange : selectionRangeMapX) { @@ -591,7 +593,7 @@ void MapView::dragEnterEvent(QDragEnterEvent * event) auto factory = VLC->objtypeh->getHandlerFor(objId, objSubId); auto templ = factory->getTemplates()[templateId]; controller->discardObject(sc->level); - controller->createObject(sc->level, factory->create(templ)); + controller->createObject(sc->level, factory->create(nullptr, templ)); } } diff --git a/mapeditor/objectbrowser.cpp b/mapeditor/objectbrowser.cpp index beb9850be..2a4d73f07 100644 --- a/mapeditor/objectbrowser.cpp +++ b/mapeditor/objectbrowser.cpp @@ -128,7 +128,7 @@ void ObjectBrowser::startDrag(Qt::DropActions supportedActions) if(!mimeData) return; - QDrag *drag = new QDrag(this); + auto *drag = new QDrag(this); drag->setMimeData(mimeData); drag->exec(supportedActions); } diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index 97a869f39..226b3249a 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -48,7 +48,7 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) //load factions for(auto idx : VLC->townh->getAllowedFactions()) { - CFaction * faction = VLC->townh->objects.at(idx); + const CFaction * faction = VLC->townh->objects.at(idx); auto * item = new QListWidgetItem(QString::fromStdString(faction->getNameTranslated())); item->setData(Qt::UserRole, QVariant::fromValue(idx.getNum())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); diff --git a/mapeditor/resourceExtractor/ResourceConverter.cpp b/mapeditor/resourceExtractor/ResourceConverter.cpp index a6481ed58..54dc3bc22 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.cpp +++ b/mapeditor/resourceExtractor/ResourceConverter.cpp @@ -12,7 +12,6 @@ #include "ResourceConverter.h" -#include "../lib/JsonNode.h" #include "../lib/VCMIDirs.h" #include "../lib/filesystem/Filesystem.h" @@ -71,7 +70,7 @@ void ResourceConverter::splitDefFile(const std::string & fileName, const boost:: { if(CResourceHandler::get()->existsResource(ResourcePath("SPRITES/" + fileName))) { - std::unique_ptr anim = std::make_unique(fileName); + auto anim = std::make_unique(fileName); anim->preload(); anim->exportBitmaps(pathToQString(sourceFolder)); diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 33e8997e8..2bf5fd170 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -312,7 +312,8 @@ void TerrainLayer::draw(bool onlyDirty) if(onlyDirty) { - std::set forRedrawing(dirty), neighbours; + std::set forRedrawing(dirty); + std::set neighbours; for(auto & t : dirty) { for(auto & tt : int3::getDirs()) diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index 92add8ede..fb640de22 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -86,7 +86,9 @@ signals: void selectionMade(bool anythingSlected); private: - std::set area, areaAdd, areaErase; + std::set area; + std::set areaAdd; + std::set areaErase; void onSelection(); }; @@ -227,6 +229,9 @@ public: int viewportHeight() const {return h;} private: - int x = 0, y = 0, w = 1, h = 1; + int x = 0; + int y = 0; + int w = 1; + int h = 1; }; diff --git a/mapeditor/translation/chinese.ts b/mapeditor/translation/chinese.ts new file mode 100644 index 000000000..5c63b949a --- /dev/null +++ b/mapeditor/translation/chinese.ts @@ -0,0 +1,1831 @@ + + + + + ArmyWidget + + + Army settings + 部队设置 + + + + Wide formation + 松散队形 + + + + Tight formation + 紧凑队形 + + + + EventSettings + + + Form + 窗体 + + + + Timed events + 计时事件 + + + + Add + 添加 + + + + Remove + 移除 + + + + New event + 新事件 + + + + GeneralSettings + + + Form + 窗体 + + + + Map name + 地图名称 + + + + Map description + 地图描述 + + + + Limit maximum heroes level + 限制英雄最大等级 + + + + Difficulty + 难度 + + + + GeneratorProgress + + + Generating map + 生成地图中 + + + + HeroSkillsWidget + + + Hero skills + 英雄技能 + + + + + + + TextLabel + 文本标签 + + + + Add + 添加 + + + + Remove + 移除 + + + + Skill + 技能 + + + + Level + 等级 + + + + Customize skills + 自定义技能 + + + + LoseConditions + + + Form + 窗体 + + + + Defeat message + 失败信息 + + + + 7 days without town + 无城镇超过7天 + + + + Parameters + 参数 + + + + No special loss + 没有特殊失败条件 + + + + Lose castle + 失去城镇 + + + + Lose hero + 失去英雄 + + + + Time expired + 超过时限 + + + + Days without town + 无城镇超过特定天数 + + + + MainWindow + + + VCMI Map Editor + VCMI地图编辑器 + + + + File + 文件 + + + + Map + 地图 + + + + Edit + 编辑 + + + + View + 视图 + + + + Player + 玩家 + + + + Toolbar + 工具栏 + + + + Minimap + 小地图 + + + + Map Objects View + 地图物体视图 + + + + Browser + 浏览器 + + + + Inspector + 检视器 + + + + Property + 属性 + + + + Value + + + + + Tools + 工具 + + + + Painting + 绘制 + + + + Terrains + 地形 + + + + Roads + 道路 + + + + Rivers + 河流 + + + + Preview + 预览 + + + + Open + 打开 + + + + Save + 保存 + + + + New + 新建 + + + + Save as... + 另存为 + + + + Ctrl+Shift+S + Ctrl+Shift+S + + + + U/G + 地上/地下 + + + + + View underground + 查看地下 + + + + Pass + 可通行 + + + + Cut + 剪切 + + + + Copy + 复制 + + + + Paste + 粘贴 + + + + Fill + 填充 + + + + Fills the selection with obstacles + 填充障碍物到选定区域 + + + + Grid + 网格 + + + + General + 通用 + + + + Map title and description + 地图标题与描述 + + + + Players settings + 玩家设置 + + + + + Undo + 撤销 + + + + Redo + 重做 + + + + Erase + 擦除 + + + + Neutral + 中立 + + + + Validate + 有效性验证 + + + + + + + Update appearance + 更新外观 + + + + Recreate obstacles + 重建障碍物 + + + + Player 1 + 玩家1 + + + + Player 2 + 玩家2 + + + + Player 3 + 玩家3 + + + + Player 4 + 玩家4 + + + + Player 5 + 玩家5 + + + + Player 6 + 玩家6 + + + + Player 7 + 玩家7 + + + + Player 8 + 玩家8 + + + + Export as... + 导出为 + + + + Translations + 翻译 + + + + Ctrl+T + Ctrl+T + + + + + h3m converter + h3m转换器 + + + + Lock + 锁定 + + + + Lock objects on map to avoid unnecessary changes + 锁定地图上的物体防止误操作 + + + + Ctrl+L + Ctrl+L + + + + Unlock + 解锁 + + + + Unlock all objects on the map + 解锁地图上的所有物体 + + + + Ctrl+Shift+L + Ctrl+Shift+L + + + + Zoom in + 放大 + + + + Ctrl+= + Ctrl+= + + + + Zoom out + 缩小 + + + + Ctrl+- + Ctrl+- + + + + Zoom reset + 重置缩放 + + + + Ctrl+Shift+= + Ctrl+Shift+= + + + + Confirmation + 确认 + + + + Unsaved changes will be lost, are you sure? + 未保存的改动会丢失,你确定要这么做吗? + + + + Open map + 打开地图 + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + 所有支持的地图类型(*.vmap *.h3m);;VCMI地图(*.vmap);;英雄无敌3地图(*.h3m) + + + + Save map + 保存地图 + + + + VCMI maps (*.vmap) + VCMI地图(*.vmap) + + + + Type + 类型 + + + + View surface + 查看地上 + + + + No objects selected + 未选择任何物体 + + + + This operation is irreversible. Do you want to continue? + 此操作无法被撤销,你确定要继续么? + + + + Errors occurred. %1 objects were not updated + 发生错误!%1 物体未完成更新 + + + + Save to image + 保存为图片 + + + + Select maps to convert + 选择待转换的地图 + + + + HoMM3 maps(*.h3m) + 英雄无敌3地图文件(*.h3m) + + + + Choose directory to save converted maps + 选择保存转换地图的目录 + + + + Operation completed + 操作完成 + + + + Successfully converted %1 maps + 成功转换 %1 地图 + + + + Failed to convert the map. Abort operation + 转换地图失败,操作终止 + + + + MapSettings + + + Map settings + 地图设置 + + + + General + 通用 + + + + Mods + 模组 + + + + Events + 事件 + + + + Victory + 胜利条件 + + + + Loss + 失败 + + + + Timed + 计时器 + + + + Rumors + 传闻 + + + + Abilities + 能力 + + + + Spells + 魔法 + + + + Artifacts + 宝物 + + + + Heroes + 英雄 + + + + Ok + 确定 + + + + MapView + + + Can't place object + 无法放置物体 + + + + MessageWidget + + + Message + 消息 + + + + ModSettings + + + Form + 窗体 + + + + Mandatory mods to play this map + 地图依赖的模组 + + + + Mod name + 模组名称 + + + + Version + 版本 + + + + Automatic assignment + 自动分配 + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + 按照放置在地图上的物体设置依赖模组,如果你设置了模组相关的奖励、部队等,可能会出现问题。 + + + + Map objects mods + 地图物体使用的模组 + + + + Set all mods having a game content as mandatory + 将游戏用到的全部模组设置为依赖 + + + + Full content mods + 所有模组 + + + + PlayerParams + + + Human/CPU + 人类/电脑 + + + + CPU only + 仅电脑可用 + + + + Team + 队伍 + + + + Main town + 主城 + + + + Color + 颜色 + + + + ... + ... + + + + Random faction + 随机种族 + + + + Generate hero at main + 在主城生成英雄 + + + + (default) + (默认) + + + + Player ID: %1 + 玩家ID: %1 + + + + PlayerSettings + + + Player settings + 玩家设置 + + + + Players + 玩家 + + + + 1 + 1 + + + + Ok + 确定 + + + + PortraitWidget + + + Portrait + 头像 + + + + + ... + ... + + + + Default + 默认 + + + + QObject + + + Beginner + 初级 + + + + Advanced + 中级 + + + + Expert + 高级 + + + + Compliant + 屈服的 + + + + Friendly + 友善的 + + + + Aggressive + 好斗的 + + + + Hostile + 有敌意的 + + + + Savage + 野蛮的 + + + + + neutral + 中立 + + + + UNFLAGGABLE + 没有旗帜 + + + + QuestWidget + + + Mission goal + 任务目标 + + + + Day of week + 每周天数 + + + + Days passed + 起始天数 + + + + Hero level + 英雄等级 + + + + Hero experience + 英雄经验 + + + + Spell points + 魔法值 + + + + % + % + + + + Kill hero/monster + 击败英雄/怪物 + + + + ... + ... + + + + Primary skills + 主属性 + + + + Attack + 攻击力 + + + + Defence + 防御力 + + + + Spell power + 力量 + + + + Knowledge + 知识 + + + + Resources + 资源 + + + + Artifacts + 宝物 + + + + Spells + 魔法 + + + + Skills + 技能 + + + + Creatures + 生物 + + + + Add + 添加 + + + + Remove + 移除 + + + + Heroes + 英雄 + + + + Hero classes + 英雄类型 + + + + Players + 玩家 + + + + None + + + + + Day %1 + 第 %1 日 + + + + RewardsWidget + + + Rewards + 奖励 + + + + + + + Add + 添加 + + + + + + + Remove + 移除 + + + + Visit mode + 访问模式 + + + + Select mode + 选择模式 + + + + On select text + 选择时展示的文本 + + + + Can refuse + 可以拒绝 + + + + Reset parameters + 重置参数 + + + + Period + 周期 + + + + days + + + + + Reset visitors + 重置访问者 + + + + Reset rewards + 重置奖励 + + + + Window type + 窗口类型 + + + + Event info + 事件信息 + + + + Message to be displayed on granting of this reward + 给予奖励时展示的文本 + + + + Reward + 奖励 + + + + + Hero level + 英雄等级 + + + + + Hero experience + 英雄经验 + + + + + Spell points + 魔法值 + + + + + + + % + % + + + + Overflow + 溢出 + + + + Movement + 移动力 + + + + Remove object + 移除物体 + + + + + Primary skills + 主属性 + + + + + Attack + 攻击力 + + + + + Defence + 防御力 + + + + + Spell power + 力量 + + + + + Knowledge + 知识 + + + + + Resources + 资源 + + + + + Artifacts + 宝物 + + + + + Spells + 魔法 + + + + + Skills + 技能 + + + + + Creatures + 生物 + + + + Bonuses + 状态 + + + + + Duration + 持续时间 + + + + + Type + 类型 + + + + + Value + + + + + Cast + 施法 + + + + Cast an adventure map spell + 在冒险地图施放法术 + + + + Spell + 魔法 + + + + Magic school level + 法术等级 + + + + Limiter + 限制 + + + + Day of week + 每周天数 + + + + Days passed + 起始天数 + + + + Heroes + 英雄 + + + + Hero classes + 英雄类型 + + + + Players + 玩家 + + + + None + + + + + Day %1 + %1 天 + + + + + Reward %1 + 奖励 %1 + + + + RumorSettings + + + Form + 窗体 + + + + Tavern rumors + 酒馆传闻 + + + + Add + 添加 + + + + Remove + 移除 + + + + New rumor + 新传闻 + + + + TimedEvent + + + Timed event + 计时事件 + + + + Event name + 事件名 + + + + Type event message text + 输入事件信息文本 + + + + affects human + 人类玩家生效 + + + + affects AI + AI玩家生效 + + + + Day of first occurance + 首次发生天数 + + + + Repeat after (0 = no repeat) + 重复周期 (0 = 不重复) + + + + Affected players + 生效玩家 + + + + Resources + 资源 + + + + type + 类型 + + + + qty + 数量 + + + + Ok + 确定 + + + + TownBulidingsWidget + + + Buildings + 建筑 + + + + Translations + + + Map translations + 地图翻译 + + + + Language + 语言 + + + + Suppported + 已支持 + + + + String ID + 字符串标识 + + + + Text + 文本 + + + + + Remove translation + 移除翻译 + + + + Default language cannot be removed + 默认语言无法被移除 + + + + All existing text records for this language will be removed. Continue? + 此语言的所有文本记录将被移除,确定继续吗? + + + + Validator + + + Map validation results + 地图验证结果 + + + + Map is not loaded + 未加载地图 + + + + No factions allowed for player %1 + 玩家 %1 没有可使用的种族 + + + + No players allowed to play this map + 该地图未设置任何玩家 + + + + Map is allowed for one player and cannot be started + 该地图只有一个玩家,无法开始游戏 + + + + No human players allowed to play this map + 该地图缺少人类玩家 + + + + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner + 部队实例 %1 没有旗帜,需要设置为中立或任一玩家所有 + + + + Object %1 is assigned to non-playable player %2 + 物体 %1 属于无法游戏的玩家 %2 + + + + Town %1 has undefined owner %2 + 城镇 %1 拥有者 %2 未定义 + + + + Prison %1 must be a NEUTRAL + 监狱 %1 需要为中立 + + + + Hero %1 must have an owner + 英雄 %1 需要有一个所有者 + + + + Hero %1 is prohibited by map settings + 英雄 %1 被地图设置禁止 + + + + Hero %1 has duplicate on map + 英雄 %1 在地图上有多个 + + + + Hero %1 has an empty type and must be removed + 英雄 %1 属于空类型,必须移除 + + + + Spell scroll %1 is prohibited by map settings + 魔法卷轴 %1 被地图设置禁止 + + + + Spell scroll %1 doesn't have instance assigned and must be removed + 魔法卷轴 %1 未和任一实例关联,需要被移除 + + + + Artifact %1 is prohibited by map settings + 宝物 %1 被地图设置禁止 + + + + Player %1 doesn't have any starting town + 玩家 %1 没有一座起始城镇 + + + + Map name is not specified + 地图名字为空 + + + + Map description is not specified + 地图描述为空 + + + + Map contains object from mod "%1", but doesn't require it + 地图包含模组 %1 的物体,但没有声明依赖该模组 + + + + Exception occurs during validation: %1 + 在验证地图期间发生异常:%1 + + + + Unknown exception occurs during validation + 在验证地图期间发生未知异常 + + + + VictoryConditions + + + Form + 窗体 + + + + Victory message + 胜利信息 + + + + Only for human players + 仅人类玩家有效 + + + + Allow standard victory + 允许常规胜利 + + + + Parameters + 参数 + + + + No special victory + 无特殊胜利条件 + + + + Capture artifact + 获取宝物 + + + + Hire creatures + 雇佣生物 + + + + Accumulate resources + 获得资源 + + + + Construct building + 建造神器 + + + + Capture town + 占领城镇 + + + + Defeat hero + 击败英雄 + + + + Transport artifact + 运送宝物 + + + + Kill monster + 击杀怪物 + + + + WindowNewMap + + + Create new map + 创建新地图 + + + + Map size + 地图大小 + + + + Two level map + 双层地图 + + + + Height + 高度 + + + + Width + 宽度 + + + + S (36x36) + 小(36x36) + + + + M (72x72) + 中(72x72) + + + + L (108x108) + 大(108x108) + + + + XL (144x144) + 特大(144x144) + + + + Random map + 随机地图 + + + + Players + 玩家 + + + + 0 + 0 + + + + Human/Computer + 人类/电脑 + + + + + + + Random + 随机 + + + + Computer only + 仅电脑可用 + + + + Human teams + 人类队伍 + + + + Computer teams + 电脑队伍 + + + + Monster strength + 怪物强度 + + + + Weak + + + + + + Normal + 普通 + + + + Strong + + + + + Water content + 水域 + + + + None + + + + + Islands + 岛屿 + + + + Template + 模版 + + + + Custom seed + 自定义种子 + + + + Generate random map + 生成随机地图 + + + + Ok + 确定 + + + + Cancel + 取消 + + + + No template + 缺少模版 + + + + No template for parameters scecified. Random map cannot be generated. + 未指定任一模版作为参数,随机地图无法生成。 + + + + RMG failure + 随机地图生成失败 + + + + main + + + Filepath of the map to open. + 要打开的地图所在的文件路径。 + + + + Extract original H3 archives into a separate folder. + 将原始H3文件解压到特定目录。 + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + 数据文件解压后,将TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 und Un44切分为独立的PNG文件。 + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + 数据文件解压后,将每一张图片(Images目录)从pcx格式转化为png格式。 + + + + Delete original files, for the ones split / converted. + 当切分/转换完成后,原始文件将被删除。 + + + diff --git a/mapeditor/translation/czech.ts b/mapeditor/translation/czech.ts new file mode 100644 index 000000000..024430f97 --- /dev/null +++ b/mapeditor/translation/czech.ts @@ -0,0 +1,1831 @@ + + + + + ArmyWidget + + + Army settings + Nastavení armády + + + + Wide formation + Široká formace + + + + Tight formation + Úzká formace + + + + EventSettings + + + Form + Formulář + + + + Timed events + Načasované události + + + + Add + Přidat + + + + Remove + Odebrat + + + + New event + Nová událost + + + + GeneralSettings + + + Form + Formulář + + + + Map name + Název mapy + + + + Map description + Popis mapy + + + + Limit maximum heroes level + Omezit max. úroveň hrdinů + + + + Difficulty + Obtížnost + + + + GeneratorProgress + + + Generating map + Generování mapy + + + + HeroSkillsWidget + + + Hero skills + Schopnosti hrdiny + + + + + + + TextLabel + + + + + Add + Přidat + + + + Remove + Odebrat + + + + Skill + Dovednost + + + + Level + Úroveň + + + + Customize skills + Přizpůsobit schopnosti + + + + LoseConditions + + + Form + Formulář + + + + Defeat message + Zpráva prohry + + + + 7 days without town + 7 dní bez města + + + + Parameters + Parametry + + + + No special loss + Bez speciální prohry + + + + Lose castle + Ztráta města + + + + Lose hero + Ztráta hrdiny + + + + Time expired + Vypršení času + + + + Days without town + Dny bez města + + + + MainWindow + + + VCMI Map Editor + Editor map VCMI + + + + File + Soubor + + + + Map + Mapa + + + + Edit + Upravit + + + + View + Zobrazit + + + + Player + Hráč + + + + Toolbar + Panel nástrojů + + + + Minimap + Minimapa + + + + Map Objects View + Zobrazení objektů mapy + + + + Browser + Prohlížeč + + + + Inspector + Inspektor + + + + Property + Vlastnost + + + + Value + Hodnota + + + + Tools + Nástroje + + + + Painting + Malování + + + + Terrains + Krajiny + + + + Roads + Cesty + + + + Rivers + Řeky + + + + Preview + Náhled + + + + Open + Otevřít + + + + Save + Uložit + + + + New + Nový + + + + Save as... + Uložit jako... + + + + Ctrl+Shift+S + Ctrl+Shift+S + + + + U/G + P/Z + + + + + View underground + Zobrazit podzemí + + + + Pass + Průchodnost + + + + Cut + Vyjmout + + + + Copy + Kopírovat + + + + Paste + Vložit + + + + Fill + Vyplnit + + + + Fills the selection with obstacles + Vyplní výběr překážkami + + + + Grid + Mřížka + + + + General + Všeobecné + + + + Map title and description + Název a popis mapy + + + + Players settings + Hráčské nastavení + + + + + Undo + Zpět + + + + Redo + Znovu + + + + Erase + Smazat + + + + Neutral + Neutrální + + + + Validate + Posoudit + + + + + + + Update appearance + Aktualizovat vzhled + + + + Recreate obstacles + Přetvořit překážky + + + + Player 1 + Hráč 1 + + + + Player 2 + Hráč 2 + + + + Player 3 + Hráč 3 + + + + Player 4 + Hráč 4 + + + + Player 5 + Hráč 5 + + + + Player 6 + Hráč 6 + + + + Player 7 + Hráč 7 + + + + Player 8 + Hráč 8 + + + + Export as... + Exportovat jako... + + + + Translations + Překlady + + + + Ctrl+T + Ctrl+T + + + + + h3m converter + Převodník h3m + + + + Lock + Zamknout + + + + Lock objects on map to avoid unnecessary changes + Zamknout objekty na mapě pro zabránění nadbytečných změn + + + + Ctrl+L + Ctrl+L + + + + Unlock + Odemknout + + + + Unlock all objects on the map + Odemknout objekty na mapě + + + + Ctrl+Shift+L + Ctrl+Shift+L + + + + Zoom in + Přiblížit + + + + Ctrl+= + Ctrl+= + + + + Zoom out + Oddálit + + + + Ctrl+- + Ctrl+- + + + + Zoom reset + Zrušit přiblížení + + + + Ctrl+Shift+= + Ctrl+Shift+= + + + + Confirmation + Potvrzení + + + + Unsaved changes will be lost, are you sure? + Neuložené změny budou ztraceny, jste si jisti? + + + + Open map + Otevřít mapu + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + Všechny podporované mapy (*.vmap *.h3m);; Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) + + + + Save map + Uložit mapu + + + + VCMI maps (*.vmap) + Mapy VCMI (*.vmap) + + + + Type + Druh + + + + View surface + Zobrazit povrch + + + + No objects selected + Nejsou vybrány žádné objekty + + + + This operation is irreversible. Do you want to continue? + Tento úkon je nezvratný. Chcete pokračovat? + + + + Errors occurred. %1 objects were not updated + Nastaly chyby. Nebylo aktualizováno %1 objektů + + + + Save to image + Uložit do obrázku + + + + Select maps to convert + Vyberte mapy pro převod + + + + HoMM3 maps(*.h3m) + Mapy HoMM3 (*.h3m) + + + + Choose directory to save converted maps + Vyberte složku pro uložení převedených map + + + + Operation completed + Operace dokončena + + + + Successfully converted %1 maps + Úspěšně převedeno %1 map + + + + Failed to convert the map. Abort operation + Převod map selhal. Úkon zrušen + + + + MapSettings + + + Map settings + Nastavení mapy + + + + General + Obecné + + + + Mods + Modifikace + + + + Events + Události + + + + Victory + Vítězství + + + + Loss + Prohra + + + + Timed + Načasované + + + + Rumors + Klepy + + + + Abilities + Schopnosti + + + + Spells + Kouzla + + + + Artifacts + Artefakty + + + + Heroes + Hrdinové + + + + Ok + Dobře + + + + MapView + + + Can't place object + Nelze umístit objekt + + + + MessageWidget + + + Message + Zpráva + + + + ModSettings + + + Form + Formulář + + + + Mandatory mods to play this map + Potřebné modifikace pro hraní této mapy + + + + Mod name + Název modifikace + + + + Version + Verze + + + + Automatic assignment + Automatické přiřazení + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Nastavit potřebné modifikace v závislosti na umístěných objektech na mapě. Tato metoda může způsobit problém, pokud máte přizpůsobené odměny, posátky atd. z modifikací + + + + Map objects mods + Modifikace objektů mapy + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + + Human/CPU + Člověk/CPU + + + + CPU only + Pouze CPU + + + + Team + Tým + + + + Main town + Hlavní město + + + + Color + Barva + + + + ... + ... + + + + Random faction + Náhodná frakce + + + + Generate hero at main + + + + + (default) + (výchozí) + + + + Player ID: %1 + ID hráče: %1 + + + + PlayerSettings + + + Player settings + Hráčské nastavení + + + + Players + Hráči + + + + 1 + 1 + + + + Ok + Dobře + + + + PortraitWidget + + + Portrait + + + + + + ... + ... + + + + Default + Výchozí + + + + QObject + + + Beginner + Začátečník + + + + Advanced + Pokročilý + + + + Expert + Expert + + + + Compliant + Ochotná + + + + Friendly + Přátelská + + + + Aggressive + Agresivní + + + + Hostile + Nepřátelská + + + + Savage + Brutální + + + + + neutral + neutrální + + + + UNFLAGGABLE + NEOZNAČITELNÝ + + + + QuestWidget + + + Mission goal + Cíl mise + + + + Day of week + Den týdne + + + + Days passed + Uběhlých dní + + + + Hero level + Úroveň hrdiny + + + + Hero experience + Zkušenosti hrdiny + + + + Spell points + Magická energie + + + + % + % + + + + Kill hero/monster + Zabít hrdinu/příšeru + + + + ... + ... + + + + Primary skills + Základní schopnosti + + + + Attack + Útok + + + + Defence + Obrana + + + + Spell power + Síla kouzel + + + + Knowledge + Znalosti + + + + Resources + Zdroje + + + + Artifacts + Artefakty + + + + Spells + Kouzla + + + + Skills + Dovednosti + + + + Creatures + Jednotky + + + + Add + Přidat + + + + Remove + Odebrat + + + + Heroes + Hrdinové + + + + Hero classes + Třídy hrdinů + + + + Players + Hráči + + + + None + Žádný + + + + Day %1 + Den %1 + + + + RewardsWidget + + + Rewards + Odměny + + + + + + + Add + Přidat + + + + + + + Remove + Odebrat + + + + Visit mode + Režim návštěvy + + + + Select mode + Režim výběru + + + + On select text + + + + + Can refuse + Lze odmítnout + + + + Reset parameters + + + + + Period + Cyklus + + + + days + dní + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + Druh okna + + + + Event info + Informace události + + + + Message to be displayed on granting of this reward + Zobrazená zpráva při udělení odměny + + + + Reward + Odměna + + + + + Hero level + Úroveň hrdiny + + + + + Hero experience + Zkušenosti hrdiny + + + + + Spell points + Magická energie + + + + + + + % + % + + + + Overflow + + + + + Movement + Pohyb + + + + Remove object + Odstranit objekt + + + + + Primary skills + Hlavní dovednosti + + + + + Attack + Útok + + + + + Defence + Obrana + + + + + Spell power + Síla kouzel + + + + + Knowledge + Znalosti + + + + + Resources + Zdroje + + + + + Artifacts + Artefakty + + + + + Spells + Kouzla + + + + + Skills + Dovednosti + + + + + Creatures + Jednotky + + + + Bonuses + Bonusy + + + + + Duration + Délka + + + + + Type + Druh + + + + + Value + Hodnota + + + + Cast + Seslat kouzlo + + + + Cast an adventure map spell + Seslat kouzlo mapy světa + + + + Spell + Kouzlo + + + + Magic school level + Úroveň školy magie + + + + Limiter + Omezovač + + + + Day of week + Den týdne + + + + Days passed + Uběhlých dní + + + + Heroes + Hrdinové + + + + Hero classes + Třídy hrdinů + + + + Players + Hráči + + + + None + Žádný + + + + Day %1 + Den %1 + + + + + Reward %1 + Odměna %1 + + + + RumorSettings + + + Form + Formulář + + + + Tavern rumors + Městské klepy + + + + Add + Přidat + + + + Remove + Odebrat + + + + New rumor + Nový klep + + + + TimedEvent + + + Timed event + Načasovaná událost + + + + Event name + Název události + + + + Type event message text + Zadejte text zprávy události + + + + affects human + ovlivňuje lidi + + + + affects AI + ovlivňuje AI + + + + Day of first occurance + Den prvního výskytu + + + + Repeat after (0 = no repeat) + Opakovat po (0 = bez opak.) + + + + Affected players + Ovlivnění hráči + + + + Resources + Zdroje + + + + type + druh + + + + qty + množství + + + + Ok + Dobře + + + + TownBulidingsWidget + + + Buildings + Budovy + + + + Translations + + + Map translations + Překlady mapy + + + + Language + Jazyk + + + + Suppported + Podporovaný + + + + String ID + ID řetězce + + + + Text + Text + + + + + Remove translation + Odebrat překlad + + + + Default language cannot be removed + Výchozí jazyk nemůže být odstraněn + + + + All existing text records for this language will be removed. Continue? + Všechny textové záznamy pro tento jazyk budou odstraněny. Pokračovat? + + + + Validator + + + Map validation results + Výsledky posudku mapy + + + + Map is not loaded + Mapa není načtena + + + + No factions allowed for player %1 + + + + + No players allowed to play this map + Žádní hráči nejsou dovoleni hrát tuto mapu + + + + Map is allowed for one player and cannot be started + Mapa je pouze pro jednoho hráče na nemůže být spuštěna + + + + No human players allowed to play this map + Žádní lidští hráči nejsou dovoleni hrát tuto mapu + + + + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner + + + + + Object %1 is assigned to non-playable player %2 + Objekt %1 je přiřazen nehratelnému hráči %2 + + + + Town %1 has undefined owner %2 + Město %1 nemá definovaného vlastníka %2 + + + + Prison %1 must be a NEUTRAL + Vězení %1 musí být NEUTRÁLNÍ + + + + Hero %1 must have an owner + Hrdina %1 musí mít vlastníka + + + + Hero %1 is prohibited by map settings + Hrdina %1 je zakázaný nastavením mapy + + + + Hero %1 has duplicate on map + Hrdina %1 má na mapě dvojníka + + + + Hero %1 has an empty type and must be removed + + + + + Spell scroll %1 is prohibited by map settings + Kouzlo %1 je zakázáno nastavením mapy + + + + Spell scroll %1 doesn't have instance assigned and must be removed + + + + + Artifact %1 is prohibited by map settings + Artefakt %1 je zakázán nastavením mapy + + + + Player %1 doesn't have any starting town + Hráč %1 nemá žádné počáteční město + + + + Map name is not specified + Mapa nemá název + + + + Map description is not specified + Mapa nemá popis + + + + Map contains object from mod "%1", but doesn't require it + Mapa obsahuje objekt z modifikace "%1". ale nevyžaduje ji + + + + Exception occurs during validation: %1 + Při posudku nastala výjimka: %1 + + + + Unknown exception occurs during validation + Nasta neznámá výjimka při posudku + + + + VictoryConditions + + + Form + Formulář + + + + Victory message + Zpráva vítězství + + + + Only for human players + Jen pro lidské hráče + + + + Allow standard victory + Povolit standardní výhru + + + + Parameters + Parametry + + + + No special victory + Bez speciálního vítězství + + + + Capture artifact + Získat artefakt + + + + Hire creatures + Najmout bojovníky + + + + Accumulate resources + Nashromáždit zdroje + + + + Construct building + Postavit budovu + + + + Capture town + Získat město + + + + Defeat hero + Porazit hrdinu + + + + Transport artifact + Přesunout artefakt + + + + Kill monster + Zabít příšeru + + + + WindowNewMap + + + Create new map + Vytvořit novou mapu + + + + Map size + Velikost mapy + + + + Two level map + Dvě úrovně + + + + Height + Výška + + + + Width + Šířka + + + + S (36x36) + S (36x36) + + + + M (72x72) + M (72x72) + + + + L (108x108) + L (108x108) + + + + XL (144x144) + XL (144x144) + + + + Random map + Náhodná mapa + + + + Players + Hráči + + + + 0 + 0 + + + + Human/Computer + Hráč/počítač + + + + + + + Random + Náhodně + + + + Computer only + Pouze počítač + + + + Human teams + Lidské týmy + + + + Computer teams + Počítačové týmy + + + + Monster strength + Síla příšer + + + + Weak + Slabá + + + + + Normal + Normální + + + + Strong + Silná + + + + Water content + Obsah vody + + + + None + Žádná + + + + Islands + Ostrovy + + + + Template + Šablona + + + + Custom seed + Vlastní semínko + + + + Generate random map + Vygenerovat náhodnou mapu + + + + Ok + Dobře + + + + Cancel + Zrušit + + + + No template + Bez šablony + + + + No template for parameters scecified. Random map cannot be generated. + Žádná šablona pro vybrané parametry. Náhodná mapa nemůže být vygenerována. + + + + RMG failure + Chyba RMG + + + + main + + + Filepath of the map to open. + Cesta k souboru mapy pro otevření. + + + + Extract original H3 archives into a separate folder. + Rozbalit originální archivy H3 do zvláštní složky. + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + Z rozbaleného archivu rozdělí TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 a Un44 do jednotlivých PNG. + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + Z rozbaleného archivu převede jednoduché obrázky (nalezené ve složce Images) z .pcx do png. + + + + Delete original files, for the ones split / converted. + + + + diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 3e5d669d1..f9597d2a0 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings - + Wide formation - + Tight formation + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + + + + + Map description + + + + + Limit maximum heroes level + + + + + Difficulty + + + GeneratorProgress @@ -27,6 +83,95 @@ + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + + + + + 7 days without town + + + + + Parameters + + + + + No special loss + + + + + Lose castle + + + + + Lose hero + + + + + Time expired + + + + + Days without town + + + MainWindow @@ -40,546 +185,499 @@ - + Map - + Edit - + View - + Player - + Toolbar - + Minimap - + Map Objects View - + Browser - + Inspector - + Property - + Value - - Terrains View + + Tools - - Brush + + Painting - + Terrains - + Roads - + Rivers - + + Preview + + + + Open - + Save - + New - + Save as... - + Ctrl+Shift+S - + U/G - - + + View underground - + Pass - + Cut - + Copy - + Paste - + Fill - + Fills the selection with obstacles - + Grid - + General - + Map title and description - + Players settings - - + + Undo - + Redo - + Erase - + Neutral - + Validate - - - - + + + + Update appearance - + Recreate obstacles - + Player 1 - + Player 2 - + Player 3 - + Player 4 - + Player 5 - + Player 6 - + Player 7 - + Player 8 - + Export as... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation - + Unsaved changes will be lost, are you sure? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) - - + Save map - - + VCMI maps (*.vmap) - + Type - + View surface - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings - + General - - Map name - - - - - Map description - - - - - Limit maximum heroes level - - - - - Difficulty - - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events - + Victory - - Victory message - - - - - Only for human players - - - - - Allow standard victory - - - - - - Parameters - - - - + Loss - - 7 days without town + + Timed - - Defeat message + + Rumors - + Abilities - + Spells - + Artifacts - + Heroes - + Ok - - - No special victory - - - - - Capture artifact - - - - - Hire creatures - - - - - Accumulate resources - - - - - Construct building - - - - - Capture town - - - - - Defeat hero - - - - - Transport artifact - - - - - No special loss - - - - - Lose castle - - - - - Lose hero - - - - - Time expired - - - - - Days without town - - MapView - + Can't place object @@ -587,55 +685,108 @@ MessageWidget - + Message + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + PlayerParams - + Human/CPU - + CPU only - + Team - + Main town - + Color - + + ... + + + + Random faction - + Generate hero at main - + (default) - + Player ID: %1 @@ -663,45 +814,634 @@ + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + + + + + Spells + + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + + + + + Hero classes + + + + + Players + + + + + None + + + + + Day %1 + + RewardsWidget - + Rewards - - Remove selected + + + + + Add - Delete all + + + + Remove - - Add or change + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + + + + + + Spells + + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + + + + + + Value + + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + + + + + Hero classes + + + + + Players + + + + + None + + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok TownBulidingsWidget - + Buildings + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -710,116 +1450,189 @@ - + Map is not loaded - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - + Town %1 has undefined owner %2 - + Prison %1 must be a NEUTRAL - + Hero %1 must have an owner - + Hero %1 is prohibited by map settings - + Hero %1 has duplicate on map - + Hero %1 has an empty type and must be removed - + Spell scroll %1 is prohibited by map settings - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - + Player %1 doesn't have any starting town - + Map name is not specified - + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation + + VictoryConditions + + + Form + + + + + Victory message + + + + + Only for human players + + + + + Allow standard victory + + + + + Parameters + + + + + No special victory + + + + + Capture artifact + + + + + Hire creatures + + + + + Accumulate resources + + + + + Construct building + + + + + Capture town + + + + + Defeat hero + + + + + Transport artifact + + + + + Kill monster + + + WindowNewMap @@ -972,17 +1785,17 @@ - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -990,27 +1803,27 @@ main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 91cd9b88a..9b45cefb8 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Paramètres de l'armée - + Wide formation Formation large - + Tight formation Formation serrée + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nom de la carte + + + + Map description + Description de la carte + + + + Limit maximum heroes level + + + + + Difficulty + Difficulté + + GeneratorProgress @@ -27,6 +83,95 @@ Générer une carte + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Message de défaite + + + + 7 days without town + 7 jours sans ville + + + + Parameters + Paramètres + + + + No special loss + Aucune perte spéciale + + + + Lose castle + Perdre un château + + + + Lose hero + Perdre un héros + + + + Time expired + Délai expiré + + + + Days without town + Jours sans ville + + MainWindow @@ -40,546 +185,499 @@ Fichier - + Map Carte - + Edit Édition - + View Affichage - + Player Joueur - + Toolbar Barre d'outils - + Minimap Mini-carte - + Map Objects View Vue des objets cartographiques - + Browser Navigateur - + Inspector Inspecteur - + Property Propriété - + Value Valeur - - Terrains View - Vue des terrains + + Tools + - - Brush - Pinceau + + Painting + - + Terrains Terrains - + Roads Routes - + Rivers Rivières - + + Preview + + + + Open Ouvrir - + Save Enregistrer - + New Nouveau - + Save as... Enregistrer sous... - + Ctrl+Shift+S Ctrl+Maj+S - + U/G Sous-sol/Surface - - + + View underground Voir le sous-sol - + Pass Passage - + Cut Couper - + Copy Copier - + Paste Coller - + Fill Remplir - + Fills the selection with obstacles Remplir la sélection d'obstacles - + Grid Grille - + General Général - + Map title and description Titre et description de la carte - + Players settings Paramètres des joueurs - - + + Undo Annuler - + Redo Rétablir - + Erase Effacer - + Neutral Neutre - + Validate Valider - - - - + + + + Update appearance Mettre à jour l'apparence - + Recreate obstacles Recréer des obstacles - + Player 1 Joueur 1 - + Player 2 Joueur 2 - + Player 3 Joueur 3 - + Player 4 Joueur 4 - + Player 5 Joueur 5 - + Player 6 Joueur 6 - + Player 7 Joueur 7 - + Player 8 Joueur 8 - + Export as... Exporter sous... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Confirmation - + Unsaved changes will be lost, are you sure? Des modifications non sauvegardées vont être perdues. Êtes-vous sûr ? - - - - Failed to open map - - - Cannot open map from this folder - - - - + Open map Ouvrir la carte - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Toutes les cartes prises en charge (*.vmap *.h3m);;Cartes VCMI (*.vmap);;Cartes HoMM3 (*.h3m) - - + Save map Enregistrer la carte - - + VCMI maps (*.vmap) Cartes VCMI (*.vmap) - + Type Type - + View surface Afficher la surface - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Paramètres de la carte - + General Général - - Map name - Nom de la carte - - - - Map description - Description de la carte - - - - Limit maximum heroes level - - - - - Difficulty - Difficulté - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events Événements - + Victory Victoire - - Victory message - Message de victoire - - - - Only for human players - Uniquement pour les joueurs humains - - - - Allow standard victory - Autoriser la victoire standard - - - - - Parameters - Paramètres - - - + Loss Perte - - 7 days without town - 7 jours sans ville + + Timed + - - Defeat message - Message de défaite + + Rumors + - + Abilities Capacités - + Spells Sorts - + Artifacts Artefacts - + Heroes Héros - + Ok OK - - - No special victory - Pas de victoire spéciale - - - - Capture artifact - Récupérer l'artefact - - - - Hire creatures - Engagez des créatures - - - - Accumulate resources - Accumuler des ressources - - - - Construct building - Construire un bâtiment - - - - Capture town - Conquérir une ville - - - - Defeat hero - Battre un héros - - - - Transport artifact - Transporter un artefact - - - - No special loss - Aucune perte spéciale - - - - Lose castle - Perdre un château - - - - Lose hero - Perdre un héros - - - - Time expired - Délai expiré - - - - Days without town - Jours sans ville - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Message - PlayerParams + ModSettings - No team - Aucune équipe + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Human/Ordinateur - + CPU only Ordinateur uniquement - + Team Équipe - + Main town Ville principale - + Color - + + ... + + + + Random faction Faction aléatoire - + Generate hero at main Générer un héros dans le principal - + (default) (par défaut) - + Player ID: %1 Identifiant du joueur : %1 @@ -667,45 +814,634 @@ OK + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Objectif de la mission + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefacts + + + + Spells + Sorts + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Héros + + + + Hero classes + + + + + Players + Joueurs + + + + None + Aucune + + + + Day %1 + + RewardsWidget - + Rewards Récompenses - - Remove selected - Supprimer ce qui est sélectionné + + + + + Add + - Delete all - Tout supprimer + + + + Remove + - - Add or change - Ajouter ou modifier + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefacts + + + + + Spells + Sorts + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Type + + + + + Value + Valeur + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Héros + + + + Hero classes + + + + + Players + Joueurs + + + + None + Aucune + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + OK TownBulidingsWidget - + Buildings Bâtiments + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Résultats de la validation de la carte - + Map is not loaded Aucune carte n'est chargée - + No factions allowed for player %1 - + No players allowed to play this map Aucun joueur autorisé à jouer sur cette carte - + Map is allowed for one player and cannot be started La carte est autorisée pour un joueur et ne peut pas être démarrée - + No human players allowed to play this map Aucun joueur humain n'est autorisé à jouer sur cette carte - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner L'instance blindée %1 est IMMARQUABLE mais doit avoir un propriétaire NEUTRE ou joueur - + Object %1 is assigned to non-playable player %2 L'objet %1 est attribué au joueur non jouable %2 - + Town %1 has undefined owner %2 La ville %1 a le propriétaire indéfini %2 - + Prison %1 must be a NEUTRAL La prison %1 doit être NEUTRE - + Hero %1 must have an owner Le héros %1 doit avoir un propriétaire - + Hero %1 is prohibited by map settings Le héros %1 est interdit par les paramètres de la carte - + Hero %1 has duplicate on map Le héros %1 a un doublon sur la carte - + Hero %1 has an empty type and must be removed Le héros %1 a un type vide et doit être supprimé - + Spell scroll %1 is prohibited by map settings Le défilement des sorts %1 est interdit par les paramètres de la carte - + Spell scroll %1 doesn't have instance assigned and must be removed Le parchemin de sort %1 n'a pas d'instance assignée et doit être supprimé - + Artifact %1 is prohibited by map settings L'artefact %1 est interdit par les paramètres de la carte - + Player %1 doesn't have any starting town Le joueur %1 n'a pas de ville de départ - + Map name is not specified Le nom de la carte n'est pas spécifié - + Map description is not specified La description de la carte n'est pas spécifiée - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 Une exception se produit lors de la validation : %1 - + Unknown exception occurs during validation Une exception inconnue se produit lors de la validation + + VictoryConditions + + + Form + + + + + Victory message + Message de victoire + + + + Only for human players + Uniquement pour les joueurs humains + + + + Allow standard victory + Autoriser la victoire standard + + + + Parameters + Paramètres + + + + No special victory + Pas de victoire spéciale + + + + Capture artifact + Récupérer l'artefact + + + + Hire creatures + Engagez des créatures + + + + Accumulate resources + Accumuler des ressources + + + + Construct building + Construire un bâtiment + + + + Capture town + Conquérir une ville + + + + Defeat hero + Battre un héros + + + + Transport artifact + Transporter un artefact + + + + Kill monster + + + WindowNewMap @@ -976,17 +1785,17 @@ Annuler - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,27 +1803,27 @@ main - + Filepath of the map to open. Chemin du fichier de la carte à ouvrir. - + Extract original H3 archives into a separate folder. Extraire les archives H3 d'origine dans un dossier séparé. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. À partir d'une archive extraite, il divise TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 et Un44 en fichiers PNG individuels. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. À partir d'une archive extraite, convertit des images uniques (trouvées dans le dossier Images) de .pcx en png. - + Delete original files, for the ones split / converted. Supprimer les fichiers d'origine, pour ceux fractionnés/convertis. diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 07b7bac38..92f21f2a9 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Einstellungen der Armee - + Wide formation Breite Formation - + Tight formation Enge Formation + + EventSettings + + + Form + Formular + + + + Timed events + Zeitlich begrenzte Ereignisse + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + New event + Neues Ereignis + + + + GeneralSettings + + + Form + Formular + + + + Map name + Kartenname + + + + Map description + Kartenbeschreibung + + + + Limit maximum heroes level + Maximales Level des Helden begrenzen + + + + Difficulty + Schwierigkeit + + GeneratorProgress @@ -27,6 +83,95 @@ Karte generieren + + HeroSkillsWidget + + + Hero skills + Helden-Fertigkeiten + + + + + + + TextLabel + TextLabel + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Skill + Fertigkeiten + + + + Level + Level + + + + Customize skills + Fertigkeiten anpassen + + + + LoseConditions + + + Form + Formular + + + + Defeat message + Niederlage-Nachricht + + + + 7 days without town + 7 Tage ohne Stadt + + + + Parameters + Parameter + + + + No special loss + Keine besondere Niederlage + + + + Lose castle + Schloss verlieren + + + + Lose hero + Held verlieren + + + + Time expired + Zeit abgelaufen + + + + Days without town + Tage ohne Stadt + + MainWindow @@ -40,556 +185,499 @@ Datei - + Map Karte - + Edit Bearbeiten - + View Ansicht - + Player Spieler - + Toolbar Werkzeugleiste - + Minimap Minikarte - + Map Objects View Kartenobjekte-Ansicht - + Browser Browser - + Inspector Inspektor - + Property Eigenschaft - + Value Wert - - Terrains View - Terrain-Ansicht + + Tools + Werkzeuge - - Brush - Pinsel + + Painting + Malen - + Terrains Terrains - + Roads Straßen - + Rivers Flüsse - + + Preview + Vorschau + + + Open Öffnen - + Save Speichern - + New Neu - + Save as... Speichern unter... - + Ctrl+Shift+S Strg+Shift+S - + U/G U/G - - + + View underground Ansicht Untergrund - + Pass Passierbar - + Cut Ausschneiden - + Copy Kopieren - + Paste Einfügen - + Fill Füllen - + Fills the selection with obstacles Füllt die Auswahl mit Hindernissen - + Grid Raster - + General Allgemein - + Map title and description Titel und Beschreibung der Karte - + Players settings Spieler-Einstellungen - - + + Undo Rückgängig - + Redo Wiederholen - + Erase Löschen - + Neutral Neutral - + Validate Validieren - - - - + + + + Update appearance Aussehen aktualisieren - + Recreate obstacles Hindernisse neu erschaffen - + Player 1 Spieler 1 - + Player 2 Spieler 2 - + Player 3 Spieler 3 - + Player 4 Spieler 4 - + Player 5 Spieler 5 - + Player 6 Spieler 6 - + Player 7 Spieler 7 - + Player 8 Spieler 8 - + Export as... Exportieren als... - + + Translations + Übersetzungen + + + + Ctrl+T + Strg+T + + + + + h3m converter + h3m-Konverter + + + + Lock + Sperren + + + + Lock objects on map to avoid unnecessary changes + Objekte auf der Karte sperren, um unnötige Änderungen zu vermeiden + + + + Ctrl+L + Strg+L + + + + Unlock + Entsperren + + + + Unlock all objects on the map + Entsperre alle Objekte auf der Karte + + + + Ctrl+Shift+L + Strg+Umschalt+L + + + + Zoom in + Heranzoomen + + + + Ctrl+= + Strg+= + + + + Zoom out + Herauszoomen + + + + Ctrl+- + Strg+- + + + + Zoom reset + Zoom zurücksetzen + + + + Ctrl+Shift+= + Strg+Umschalt+= + + + Confirmation Bestätigung - + Unsaved changes will be lost, are you sure? Ungespeicherte Änderungen gehen verloren, sind sie sicher? - - Failed to open map - Öffnen der Karte fehlgeschlagen - - - - Confirmation - - - - - Unsaved changes will be lost, are you sure? - - - - - Cannot open map from this folder - Kann keine Karte aus diesem Ordner öffnen - - - + Open map Karte öffnen - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Alle unterstützten Karten (*.vmap *.h3m);;VCMI-Karten (*.vmap);;HoMM3-Karten (*.h3m) - - + Save map Karte speichern - - + VCMI maps (*.vmap) VCMI-Karten (*.vmap) - + Type Typ - + View surface Oberfläche anzeigen - + No objects selected Keine Objekte selektiert - + This operation is irreversible. Do you want to continue? Diese Operation ist unumkehrbar. Möchten sie fortsetzen? - + Errors occurred. %1 objects were not updated Fehler sind aufgetreten. %1 Objekte konnten nicht aktualisiert werden - + Save to image Als Bild speichern + + + Select maps to convert + Zu konvertierende Karten auswählen + + + + HoMM3 maps(*.h3m) + HoMM3-Karten (*.h3m) + + + + Choose directory to save converted maps + Verzeichnis zum Speichern der konvertierten Karten wählen + + + + Operation completed + Vorgang abgeschlossen + + + + Successfully converted %1 maps + Erfolgreiche Konvertierung von %1 Karten + + + + Failed to convert the map. Abort operation + Die Karte konnte nicht konvertiert werden. Vorgang abgebrochen + MapSettings - + Map settings Karteneinstellungen - + General Allgemein - - Map name - Kartenname - - - - Map description - Kartenbeschreibung - - - - Limit maximum heroes level - Maximales Level des Helden begrenzen - - - - Difficulty - Schwierigkeit - - - + Mods Mods - - Mandatory mods for playing this map - Notwendige Mods zum Spielen dieser Karte - - - - Mod name - Mod Name - - - - Version - Version - - - - Automatic assignment - Automatische Zuweisung - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Sie Belohnungen, Garnisonen usw. von Mods angepasst haben - - - - Map objects mods - Mods für Kartenobjekte - - - - Set all mods having a game content as mandatory - Alle Mods, die einen Spielinhalt haben, als notwendig festlegen - - - - Full content mods - Vollwertige Mods - - - + Events Ereignisse - + Victory Sieg - - Victory message - Sieg-Nachricht - - - - Only for human players - Nur für menschliche Spieler - - - - Allow standard victory - Standardsieg zulassen - - - - - Parameters - Parameter - - - + Loss Niederlage - - 7 days without town - 7 Tage ohne Stadt + + Timed + Zeitgesteuert - - Defeat message - Niederlage-Nachricht + + Rumors + Gerüchte - + Abilities Fähigkeiten - + Spells Zaubersprüche - + Artifacts Artefakte - + Heroes Helden - + Ok Ok - - - No special victory - Kein besonderer Sieg - - - - Capture artifact - Artefakt sammeln - - - - Hire creatures - Kreaturen anheuern - - - - Accumulate resources - Ressourcen ansammeln - - - - Construct building - Gebäude errichten - - - - Capture town - Stadt einnehmen - - - - Defeat hero - Held besiegen - - - - Transport artifact - Artefakt transportieren - - - - No special loss - Keine besondere Niederlage - - - - Lose castle - Schloss verlieren - - - - Lose hero - Held verlieren - - - - Time expired - Zeit abgelaufen - - - - Days without town - Tage ohne Stadt - MapView - + Can't place object Objekt kann nicht platziert werden @@ -597,59 +685,108 @@ MessageWidget - + Message Nachricht - PlayerParams + ModSettings - No team - Kein Team + + Form + Formular - + + Mandatory mods to play this map + Benötigte Mods zum Spielen dieser Karte + + + + Mod name + Mod Name + + + + Version + Version + + + + Automatic assignment + Automatische Zuweisung + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Belohnungen, Garnisonen usw. von Mods angepasst wurden + + + + Map objects mods + Mods für Kartenobjekte + + + + Set all mods having a game content as mandatory + Alle Mods, die einen Spielinhalt haben, als notwendig festlegen + + + + Full content mods + Vollwertige Mods + + + + PlayerParams + + Human/CPU Mensch/CPU - + CPU only Nur CPU - + Team Team - + Main town Hauptstadt - + Color Farbe - + + ... + ... + + + Random faction Zufällige Fraktion - + Generate hero at main Held am Hauptplatz generieren - + (default) (Standard) - + Player ID: %1 Spieler-ID: %1 @@ -677,45 +814,634 @@ Ok + + PortraitWidget + + + Portrait + Porträt + + + + + ... + ... + + + + Default + Standard + + + + QObject + + + Beginner + Anfänger + + + + Advanced + Fortgeschrittene + + + + Expert + Experte + + + + Compliant + Konform + + + + Friendly + Freundlich + + + + Aggressive + Aggressiv + + + + Hostile + Feindlich + + + + Savage + Wild + + + + + neutral + neutral + + + + UNFLAGGABLE + UNFLAGGBAR + + QuestWidget - + Mission goal Missionsziel + + + Day of week + Tag der Woche + + + + Days passed + Verstrichene Tage + + + + Hero level + Heldenstufe + + + + Hero experience + Heldenerfahrung + + + + Spell points + Zauberpunkte + + + + % + % + + + + Kill hero/monster + Held/Monster töten + + + + ... + ... + + + + Primary skills + Primäre Fähigkeiten + + + + Attack + Angriff + + + + Defence + Verteidigung + + + + Spell power + Zauberkraft + + + + Knowledge + Wissen + + + + Resources + Ressourcen + + + + Artifacts + Artefakte + + + + Spells + Zaubersprüche + + + + Skills + Fertigkeiten + + + + Creatures + Kreaturen + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Heroes + Helden + + + + Hero classes + Heldenklassen + + + + Players + Spieler + + + + None + Keine + + + + Day %1 + Tag %1 + RewardsWidget - + Rewards Belohnungen - - Remove selected - Ausgewähltes entfernen + + + + + Add + Hinzufügen - Delete all - Alle löschen + + + + Remove + Entfernen - - Add or change - Hinzufügen oder ändern + + Visit mode + Besuchsmodus + + + + Select mode + Modus auswählen + + + + On select text + + + + + Can refuse + Kann ablehnen + + + + Reset parameters + Parameter zurücksetzen + + + + Period + Zeitraum + + + + days + Tage + + + + Reset visitors + Besucher zurücksetzen + + + + Reset rewards + Belohnungen zurücksetzen + + + + Window type + Fenstertyp + + + + Event info + Ereignis-Infos + + + + Message to be displayed on granting of this reward + Nachricht, die bei der Gewährung dieser Belohnung angezeigt wird + + + + Reward + Belohnung + + + + + Hero level + Heldenstufe + + + + + Hero experience + Heldenerfahrung + + + + + Spell points + Zauberpunkte + + + + + + + % + % + + + + Overflow + Überlauf + + + + Movement + Bewegung + + + + Remove object + Objekt entfernen + + + + + Primary skills + Primäre Fähigkeiten + + + + + Attack + Angriff + + + + + Defence + Verteidigung + + + + + Spell power + Zauberkraft + + + + + Knowledge + Wissen + + + + + Resources + Ressourcen + + + + + Artifacts + Artefakte + + + + + Spells + Zaubersprüche + + + + + Skills + Fertigkeiten + + + + + Creatures + Kreaturen + + + + Bonuses + Boni + + + + + Duration + Dauer + + + + + Type + Typ + + + + + Value + Wert + + + + Cast + Wirken + + + + Cast an adventure map spell + Einen Abenteuerkarten-Zauber wirken + + + + Spell + Zauberspruch + + + + Magic school level + Stufe der Zauberschule + + + + Limiter + Begrenzer + + + + Day of week + Tag der Woche + + + + Days passed + Verstrichene Tage + + + + Heroes + Helden + + + + Hero classes + Heldenklassen + + + + Players + Spieler + + + + None + Keine + + + + Day %1 + Tag %1 + + + + + Reward %1 + Belohnung %1 + + + + RumorSettings + + + Form + Formular + + + + Tavern rumors + Tavernen-Gerüchte + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + New rumor + Neues Gerücht + + + + TimedEvent + + + Timed event + Zeitgesteuertes Ereignis + + + + Event name + Name des Ereignisses + + + + Type event message text + Ereignistext eingeben + + + + affects human + beeinflusst Menschen + + + + affects AI + beeinflusst KI + + + + Day of first occurance + Tag des ersten Auftretens + + + + Repeat after (0 = no repeat) + Wiederholung nach (0 = keine Wiederholung) + + + + Affected players + Betroffene Spieler + + + + Resources + Ressourcen + + + + type + Typ + + + + qty + anz. + + + + Ok + Ok TownBulidingsWidget - + Buildings Gebäude + + Translations + + + Map translations + Übersetzungen der Karte + + + + Language + Sprache + + + + Suppported + Unterstützt + + + + String ID + String-ID + + + + Text + Text + + + + + Remove translation + Übersetzung entfernen + + + + Default language cannot be removed + Standardsprache kann nicht entfernt werden + + + + All existing text records for this language will be removed. Continue? + Alle vorhandenen Textsätze für diese Sprache werden entfernt. Weiter? + + Validator @@ -724,116 +1450,189 @@ Ergebnisse der Kartenvalidierung - + Map is not loaded Karte ist nicht geladen - + No factions allowed for player %1 Keine Fraktionen für Spieler %1 erlaubt - + No players allowed to play this map Keine Spieler dürfen diese Karte spielen - + Map is allowed for one player and cannot be started Karte ist für einen Spieler erlaubt und kann nicht gestartet werden - + No human players allowed to play this map Keine menschlichen Spieler dürfen diese Karte spielen - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Gepanzerte Instanz %1 ist UNFLAGGABLE, muss aber NEUTRAL oder Spielerbesitzer haben - + Object %1 is assigned to non-playable player %2 Objekt %1 ist dem nicht spielbaren Spieler %2 zugewiesen - + Town %1 has undefined owner %2 Stadt %1 hat undefinierten Besitzer %2 - + Prison %1 must be a NEUTRAL Gefängnis %1 muss NEUTRAL sein - + Hero %1 must have an owner Held %1 muss einen Besitzer haben - + Hero %1 is prohibited by map settings Held %1 ist durch Karteneinstellungen verboten - + Hero %1 has duplicate on map Held %1 hat Duplikat auf Karte - + Hero %1 has an empty type and must be removed Held %1 hat einen leeren Typ und muss entfernt werden - + Spell scroll %1 is prohibited by map settings Zauberschriftrolle %1 ist durch Karteneinstellungen verboten - + Spell scroll %1 doesn't have instance assigned and must be removed Zauberschriftrolle %1 hat keine Instanz zugewiesen und muss entfernt werden - + Artifact %1 is prohibited by map settings Artefakt %1 ist durch Karteneinstellungen verboten - + Player %1 doesn't have any starting town Spieler %1 hat keine Startstadt - + Map name is not specified Kartenname ist nicht angegeben - + Map description is not specified Kartenbeschreibung ist nicht angegeben - + Map contains object from mod "%1", but doesn't require it Karte enthält Objekt aus Mod "%1", benötigt es aber nicht - + Exception occurs during validation: %1 Bei der Validierung ist eine Ausnahme aufgetreten: %1 - + Unknown exception occurs during validation Unbekannte Ausnahme trat während der Validierung auf + + VictoryConditions + + + Form + Formular + + + + Victory message + Siegesnachricht + + + + Only for human players + Nur für menschliche Spieler + + + + Allow standard victory + Standardsieg zulassen + + + + Parameters + Parameter + + + + No special victory + Kein besonderer Sieg + + + + Capture artifact + Artefakt sammeln + + + + Hire creatures + Kreaturen anheuern + + + + Accumulate resources + Ressourcen ansammeln + + + + Construct building + Gebäude errichten + + + + Capture town + Stadt einnehmen + + + + Defeat hero + Held besiegen + + + + Transport artifact + Artefakt transportieren + + + + Kill monster + Monster töten + + WindowNewMap @@ -986,17 +1785,17 @@ Abbrechen - + No template Kein Template - + No template for parameters scecified. Random map cannot be generated. Es wurde kein Template für Parameter erstellt. Zufällige Karte kann nicht generiert werden. - + RMG failure RMG-Fehler @@ -1004,27 +1803,27 @@ main - + Filepath of the map to open. Dateipfad der zu öffnenden Karte. - + Extract original H3 archives into a separate folder. Extrahieren Sie die Original-H3-Archive in einen separaten Ordner. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Aus einem extrahierten Archiv zerlegt es TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 und Un44 in einzelne PNGs. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Aus einem extrahierten Archiv werden einzelne Bilder (aus dem Ordner "Images") von .pcx in png konvertiert. - + Delete original files, for the ones split / converted. Löschen Sie die Originaldateien für die gesplitteten/konvertierten Dateien. diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index df426e4e4..be826cec3 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Ustawienia armii - + Wide formation Luźna formacja - + Tight formation Zwarta formacja + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nazwa mapy + + + + Map description + Opis mapy + + + + Limit maximum heroes level + Ogranicz maksymalny poziom bohaterów + + + + Difficulty + Poziom trudności + + GeneratorProgress @@ -27,6 +83,95 @@ Trwa generowanie mapy + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Komunikat o porażce + + + + 7 days without town + 7 dni bez miasta + + + + Parameters + Parametry + + + + No special loss + Bez specjalnych warunków porażki + + + + Lose castle + Utrata miasta + + + + Lose hero + Utrata bohatera + + + + Time expired + Upłynięcie czasu + + + + Days without town + Dni bez miasta + + MainWindow @@ -40,556 +185,499 @@ Plik - + Map Mapa - + Edit Edycja - + View Widok - + Player Gracz - + Toolbar Przybornik - + Minimap Minimapa - + Map Objects View Widok obiektów - + Browser Przeglądarka - + Inspector Inspektor - + Property Właściwość - + Value Wartość - - Terrains View - Widok terenów + + Tools + - - Brush - Pędzel + + Painting + - + Terrains Tereny - + Roads Drogi - + Rivers Rzeki - + + Preview + + + + Open Otwórz - + Save Zapisz - + New Nowy - + Save as... Zapisz jako... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Podziemia - - + + View underground Pokaż podziemia - + Pass Przejścia - + Cut Wytnij - + Copy Kopiuj - + Paste Wklej - + Fill Wypełnij - + Fills the selection with obstacles Wypełnia zaznaczony obszar przeszkodami - + Grid Siatka - + General Ogólne - + Map title and description Nazwa i opis mapy - + Players settings Ustawienia graczy - - + + Undo Cofnij - + Redo Przywróć - + Erase Wymaż - + Neutral Neutralny - + Validate Sprawdź - - - - + + + + Update appearance Aktualizuj wygląd - + Recreate obstacles Powtórnie stwórz przeszkody - + Player 1 Gracz 1 - + Player 2 Gracz 2 - + Player 3 Gracz 3 - + Player 4 Gracz 4 - + Player 5 Gracz 5 - + Player 6 Gracz 6 - + Player 7 Gracz 7 - + Player 8 Gracz 8 - + Export as... Eksportuj jako... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Potwierdzenie - + Unsaved changes will be lost, are you sure? Niezapisane zmiany zostaną utracone, jesteś pewny? - - Failed to open map - Nie udało się otworzyć mapy - - - - Confirmation - - - - - Unsaved changes will be lost, are you sure? - - - - - Cannot open map from this folder - Nie można otworzyć mapy z tego folderu - - - + Open map Otwórz mapę - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Wszystkie wspierane mapy (*.vmap *.h3m);;Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - - + Save map Zapisz mapę - - + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Typ - + View surface Pokaż powierzchnię - + No objects selected Brak wybranych obiektów - + This operation is irreversible. Do you want to continue? Ta operacja jest nieodwracalna. Czy chcesz kontynuować? - + Errors occurred. %1 objects were not updated Wystąpiły błędy. %1 obiektów nie zostało zaktualizowanych - + Save to image Zapisz jako obraz + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Ustawienia mapy - + General Ogólne - - Map name - Nazwa mapy - - - - Map description - Opis mapy - - - - Limit maximum heroes level - Ogranicz maksymalny poziom bohaterów - - - - Difficulty - Poziom trudności - - - + Mods Modyfikacje - - Mandatory mods for playing this map - Obowiązkowe modyfikacje do uruchomienia tej mapy - - - - Mod name - Nazwa modyfikacji - - - - Version - Wersja - - - - Automatic assignment - Automatyczne przypisanie - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji - - - - Map objects mods - Mody od nowych obiektów mapy - - - - Set all mods having a game content as mandatory - Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe - - - - Full content mods - Mody od złożonej zawartości - - - + Events Zdarzenia - + Victory Zwycięstwo - - Victory message - Komunikat zwycięstwa - - - - Only for human players - Dotyczy tylko graczy ludzkich - - - - Allow standard victory - Także standardowy warunek zwycięstwa - - - - - Parameters - Parametry - - - + Loss Porażka - - 7 days without town - 7 dni bez miasta + + Timed + - - Defeat message - Komunikat o porażce + + Rumors + - + Abilities Umiejętności - + Spells Zaklęcia - + Artifacts Artefakty - + Heroes Bohaterowie - + Ok Ok - - - No special victory - Bez specjalnych warunków zwycięstwa - - - - Capture artifact - Zdobądź artefakt - - - - Hire creatures - Zdobądź stworzenia - - - - Accumulate resources - Zbierz zasoby - - - - Construct building - Zbuduj budynek - - - - Capture town - Zdobądź miasto - - - - Defeat hero - Pokonaj bohatera - - - - Transport artifact - Przenieś artefakt - - - - No special loss - Bez specjalnych warunków porażki - - - - Lose castle - Utrata miasta - - - - Lose hero - Utrata bohatera - - - - Time expired - Upłynięcie czasu - - - - Days without town - Dni bez miasta - MapView - + Can't place object Nie można umieścić obiektu @@ -597,59 +685,108 @@ MessageWidget - + Message Wiadomość - PlayerParams + ModSettings - No team - Brak drużyny + + Form + - + + Mandatory mods to play this map + + + + + Mod name + Nazwa modyfikacji + + + + Version + Wersja + + + + Automatic assignment + Automatyczne przypisanie + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji + + + + Map objects mods + Mody od nowych obiektów mapy + + + + Set all mods having a game content as mandatory + Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe + + + + Full content mods + Mody od złożonej zawartości + + + + PlayerParams + + Human/CPU Człowiek/Komputer - + CPU only Tylko komputer - + Team Drużyna - + Main town Główne miasto - + Color Kolor - + + ... + + + + Random faction Losowe miasto - + Generate hero at main Generuj bohatera w głównym - + (default) (domyślny) - + Player ID: %1 ID gracza: %1 @@ -677,45 +814,634 @@ Ok + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Cel misji + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefakty + + + + Spells + Zaklęcia + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Bohaterowie + + + + Hero classes + + + + + Players + Gracze + + + + None + Brak + + + + Day %1 + + RewardsWidget - + Rewards Nagrody - - Remove selected - Usuń wybrane + + + + + Add + - Delete all - Usuń wszystkie + + + + Remove + - - Add or change - Dodaj lub zmień + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefakty + + + + + Spells + Zaklęcia + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Typ + + + + + Value + Wartość + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Bohaterowie + + + + Hero classes + + + + + Players + Gracze + + + + None + Brak + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Ok TownBulidingsWidget - + Buildings Budynki + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -724,116 +1450,189 @@ Wynik sprawdzenia mapy - + Map is not loaded Mapa nie została wczytana - + No factions allowed for player %1 Brak dozwolonych frakcji dla gracza %1 - + No players allowed to play this map Żaden gracz nie jest dozwolony do rozegrania tej mapy - + Map is allowed for one player and cannot be started Mapa jest dozwolona dla jednego gracza i nie może być rozpoczęta - + No human players allowed to play this map Żaden gracz ludzki nie został dozwolony by rozegrać tą mapę - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Obiekt z armią %1 jest nie do oflagowania, lecz musi mieć właściciela neutralnego lub gracza - + Object %1 is assigned to non-playable player %2 Obiekt %1 został przypisany do niegrywalnego gracza %2 - + Town %1 has undefined owner %2 Miasto %1 ma niezdefiniowanego właściciela %2 - + Prison %1 must be a NEUTRAL Więzienie %1 musi być neutralne - + Hero %1 must have an owner Bohater %1 musi mieć właściciela - + Hero %1 is prohibited by map settings Bohater %1 jest zabroniony przez ustawienia mapy - + Hero %1 has duplicate on map Bohater %1 posiada duplikat na mapie - + Hero %1 has an empty type and must be removed Bohater %1 jest pustego typu i musi zostać usunięty - + Spell scroll %1 is prohibited by map settings Zwój z zaklęciem %1 jest zabroniony przez ustawienia mapy - + Spell scroll %1 doesn't have instance assigned and must be removed Zwój z zaklęciem %1 nie ma przypisanej instancji i musi zostać usunięty - + Artifact %1 is prohibited by map settings Artefakt %1 jest zabroniony przez ustawienia mapy - + Player %1 doesn't have any starting town Gracz %1 nie ma żadnego startowego miasta - + Map name is not specified Nazwa mapy nie została ustawiona - + Map description is not specified Opis mapy nie został ustawiony - + Map contains object from mod "%1", but doesn't require it Mapa zawiera obiekt z modyfikacji %1 ale nie wymaga tej modyfikacji - + Exception occurs during validation: %1 Wystąpił wyjątek podczas walidacji: %1 - + Unknown exception occurs during validation Wystąpił nieznane wyjątek podczas walidacji + + VictoryConditions + + + Form + + + + + Victory message + Komunikat zwycięstwa + + + + Only for human players + Dotyczy tylko graczy ludzkich + + + + Allow standard victory + Także standardowy warunek zwycięstwa + + + + Parameters + Parametry + + + + No special victory + Bez specjalnych warunków zwycięstwa + + + + Capture artifact + Zdobądź artefakt + + + + Hire creatures + Zdobądź stworzenia + + + + Accumulate resources + Zbierz zasoby + + + + Construct building + Zbuduj budynek + + + + Capture town + Zdobądź miasto + + + + Defeat hero + Pokonaj bohatera + + + + Transport artifact + Przenieś artefakt + + + + Kill monster + + + WindowNewMap @@ -986,17 +1785,17 @@ Anuluj - + No template Brak szablonu - + No template for parameters scecified. Random map cannot be generated. Brak szablonu dla wybranych parametrów. Mapa losowa nie może zostać wygenerowana. - + RMG failure Niepowodzenie generatora map losowych @@ -1004,27 +1803,27 @@ main - + Filepath of the map to open. Lokalizacja pliku mapy do otworzenia. - + Extract original H3 archives into a separate folder. Wyodrębnij oryginalne archiwa H3 do osobnego folderu. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Z wyodrębnionego archiwum, rozdzielenie TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 i Un44 do poszczególnych plików PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Z wyodrębnionego archiwum, konwersja pojedynczych obrazków (znalezionych w folderze Images) z .pcx do .png. - + Delete original files, for the ones split / converted. Usuń oryginalne pliki, dla już rozdzielonych / skonwertowanych. diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index 25e4ae21b..dfb882407 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Настройки армии - + Wide formation Расширенная формация - + Tight formation Суженная формация + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Название карты + + + + Map description + Описание карты + + + + Limit maximum heroes level + + + + + Difficulty + Сложность + + GeneratorProgress @@ -27,6 +83,95 @@ Создание карты + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Сообщение о поражении + + + + 7 days without town + 7 дней без городов + + + + Parameters + Параметры + + + + No special loss + Нет специального поражения + + + + Lose castle + Потерять город + + + + Lose hero + Потерять героя + + + + Time expired + Не успеть ко времени + + + + Days without town + Провести без городов + + MainWindow @@ -40,546 +185,499 @@ Файл - + Map Карта - + Edit Правка - + View Вид - + Player Игрок - + Toolbar Панель инструментов - + Minimap Мини-карта - + Map Objects View Объекты карты - + Browser Навигатор - + Inspector Инспектор - + Property Свойство - + Value Значение - - Terrains View - Кисти земель + + Tools + - - Brush - Кисть + + Painting + - + Terrains Земли - + Roads Дороги - + Rivers Реки - + + Preview + + + + Open Открыть - + Save Сохранить - + New Создать - + Save as... Сохранить как - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/Н - - + + View underground Вид на подземелье - + Pass Проходимость - + Cut Вырезать - + Copy Копировать - + Paste Вставить - + Fill Заливка - + Fills the selection with obstacles Заливает выбранное препятствиями - + Grid Сетка - + General Общее - + Map title and description Название и описание карты - + Players settings Настройки игроков - - + + Undo Отменить - + Redo Повторить - + Erase Удалить - + Neutral Нейтральный - + Validate Проверить - - - - + + + + Update appearance Обновить вид - + Recreate obstacles Обновить препятствия - + Player 1 Игрок 1 - + Player 2 Игрок 2 - + Player 3 Игрок 3 - + Player 4 Игрок 4 - + Player 5 Игрок 5 - + Player 6 Игрок 6 - + Player 7 Игрок 7 - + Player 8 Игрок 8 - + Export as... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation - + Unsaved changes will be lost, are you sure? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map Открыть карту - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Все поддерживаемые карты (*.vmap *.h3m);;Карты VCMI (*.vmap);;Карты Героев III (*.h3m) - - + Save map Сохранить карту - - + VCMI maps (*.vmap) Карты VCMI (*.vmap) - + Type Тип - + View surface Вид на поверхность - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Настройки карты - + General Общее - - Map name - Название карты - - - - Map description - Описание карты - - - - Limit maximum heroes level - - - - - Difficulty - Сложность - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events События - + Victory Победа - - Victory message - Сообщение о победе - - - - Only for human players - Только для игроков-людей - - - - Allow standard victory - Разрешить стандартную победу - - - - - Parameters - Параметры - - - + Loss Поражение - - 7 days without town - 7 дней без городов + + Timed + - - Defeat message - Сообщение о поражении + + Rumors + - + Abilities Способности - + Spells Заклинания - + Artifacts Артефакты - + Heroes Герои - + Ok ОК - - - No special victory - Нет специальной победы - - - - Capture artifact - Взять артефакт - - - - Hire creatures - Нанять существ - - - - Accumulate resources - Собрать ресурсы - - - - Construct building - Построить - - - - Capture town - Захватить город - - - - Defeat hero - Победить героя - - - - Transport artifact - Переместить артефакт - - - - No special loss - Нет специального поражения - - - - Lose castle - Потерять город - - - - Lose hero - Потерять героя - - - - Time expired - Не успеть ко времени - - - - Days without town - Провести без городов - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Сообщение - PlayerParams + ModSettings - No team - Без команды + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Человек/ИИ - + CPU only Только ИИ - + Team Команда - + Main town Главный город - + Color - + + ... + + + + Random faction Случайная фракция - + Generate hero at main Создать героя - + (default) (по умолчанию) - + Player ID: %1 Игрок: %1 @@ -667,45 +814,634 @@ ОК + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Цель миссии + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Артефакты + + + + Spells + Заклинания + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Герои + + + + Hero classes + + + + + Players + + + + + None + Нет + + + + Day %1 + + RewardsWidget - + Rewards Награды - - Remove selected - Удалить выбранное + + + + + Add + - Delete all - Удалить все + + + + Remove + - - Add or change - Добавить/Изменить + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Артефакты + + + + + Spells + Заклинания + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Тип + + + + + Value + Значение + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Герои + + + + Hero classes + + + + + Players + + + + + None + Нет + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + ОК TownBulidingsWidget - + Buildings Постройки + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Результаты проверки карты - + Map is not loaded - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - + Town %1 has undefined owner %2 У города %1 неопределенный владелец %2 - + Prison %1 must be a NEUTRAL - + Hero %1 must have an owner - + Hero %1 is prohibited by map settings - + Hero %1 has duplicate on map - + Hero %1 has an empty type and must be removed - + Spell scroll %1 is prohibited by map settings - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - + Player %1 doesn't have any starting town - + Map name is not specified - + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation + + VictoryConditions + + + Form + + + + + Victory message + Сообщение о победе + + + + Only for human players + Только для игроков-людей + + + + Allow standard victory + Разрешить стандартную победу + + + + Parameters + Параметры + + + + No special victory + Нет специальной победы + + + + Capture artifact + Взять артефакт + + + + Hire creatures + Нанять существ + + + + Accumulate resources + Собрать ресурсы + + + + Construct building + Построить + + + + Capture town + Захватить город + + + + Defeat hero + Победить героя + + + + Transport artifact + Переместить артефакт + + + + Kill monster + + + WindowNewMap @@ -976,17 +1785,17 @@ Отмена - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,27 +1803,27 @@ main - + Filepath of the map to open. Путь к файлу карты для открытия. - + Extract original H3 archives into a separate folder. Распаковать архивы оригинальных Героев III в отдельную папку. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Разделение в распакованном архиве TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 и Un44 на отдельные PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Преобразование в расспакованном архиве изображений .pcx в .png. - + Delete original files, for the ones split / converted. Удалить оригиналы для преобразованных файлов. diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 8018b8000..3d8eb91c5 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Configuracion del Ejército - + Wide formation Formación amplia - + Tight formation Formación ajustada + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nombre del mapa + + + + Map description + Descripción del mapa + + + + Limit maximum heroes level + + + + + Difficulty + Dificultad + + GeneratorProgress @@ -27,6 +83,95 @@ Generando mapa + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Mensaje de derrota + + + + 7 days without town + 7 días sin ciudad + + + + Parameters + Parámetros + + + + No special loss + Sin pérdida especial + + + + Lose castle + Perder castillo + + + + Lose hero + Perder héroe + + + + Time expired + Expiró el tiempo + + + + Days without town + Días sin ciudad + + MainWindow @@ -40,546 +185,499 @@ Archivo - + Map Mapa - + Edit Editar - + View Ver - + Player Jugador - + Toolbar Barra de herramientas - + Minimap Miniatura del mapa - + Map Objects View Vista de Objetos del Mapa - + Browser Navegador - + Inspector Inspector - + Property Propiedad - + Value Valor - - Terrains View - Vista de Terrenos + + Tools + - - Brush - Pincel + + Painting + - + Terrains Terrenos - + Roads Caminos - + Rivers Ríos - + + Preview + + + + Open Abrir - + Save Guardar - + New Nuevo - + Save as... Guardar como... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Subterráneo/Superficie - - + + View underground Ver subterráneo - + Pass Pasar - + Cut Cortar - + Copy Copiar - + Paste Pegar - + Fill Rellenar - + Fills the selection with obstacles Rellena la selección con obstáculos - + Grid Rejilla - + General General - + Map title and description Título y descripción del mapa - + Players settings Configuración de jugadores - - + + Undo Deshacer - + Redo Rehacer - + Erase Borrar - + Neutral Neutral - + Validate Validar - - - - + + + + Update appearance Actualizar apariencia - + Recreate obstacles Recrear obstáculos - + Player 1 Jugador 1 - + Player 2 Jugador 2 - + Player 3 Jugador 3 - + Player 4 Jugador 4 - + Player 5 Jugador 5 - + Player 6 Jugador 6 - + Player 7 Jugador 7 - + Player 8 Jugador 8 - + Export as... Exportar como... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Confirmación - + Unsaved changes will be lost, are you sure? Los cambios no guardados se perderán. Está usted seguro ? - - Failed to open map - No se pudo abrir el mapa - - - - Cannot open map from this folder - No se puede abrir el mapa de esta carpeta - - - + Open map Abrir mapa - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Todos los mapas soportados (*.vmap *.h3m);;Mapas VCMI (*.vmap);;Mapas HoMM3 (*.h3m) - - + Save map Guardar mapa - - + VCMI maps (*.vmap) Mapas VCMI (*.vmap) - + Type Tipo - + View surface Ver superficie - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Configuración del mapa - + General General - - Map name - Nombre del mapa - - - - Map description - Descripción del mapa - - - - Limit maximum heroes level - - - - - Difficulty - Dificultad - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events Eventos - + Victory Victoria - - Victory message - Mensaje de victoria - - - - Only for human players - Solo para jugadores humanos - - - - Allow standard victory - Permitir victoria estándar - - - - - Parameters - Parámetros - - - + Loss Derrota - - 7 days without town - 7 días sin ciudad + + Timed + - - Defeat message - Mensaje de derrota + + Rumors + - + Abilities Habilidades - + Spells Hechizos - + Artifacts Artefactos - + Heroes Héroes - + Ok Aceptar - - - No special victory - Sin victoria especial - - - - Capture artifact - Capturar artefacto - - - - Hire creatures - Contratar criaturas - - - - Accumulate resources - Acumular recursos - - - - Construct building - Construir edificio - - - - Capture town - Capturar ciudad - - - - Defeat hero - Vencer héroe - - - - Transport artifact - Transportar artefacto - - - - No special loss - Sin pérdida especial - - - - Lose castle - Perder castillo - - - - Lose hero - Perder héroe - - - - Time expired - Expiró el tiempo - - - - Days without town - Días sin ciudad - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Mensaje - PlayerParams + ModSettings - No team - Sin equipo + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Humano/CPU - + CPU only Sólo CPU - + Team Equipo - + Main town Ciudad principal - + Color - + + ... + + + + Random faction Facción aleatoria - + Generate hero at main Generar héroe en la ciudad principal - + (default) (predeterminado) - + Player ID: %1 ID de jugador: %1 @@ -667,45 +814,634 @@ Aceptar + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Objetivo de la misión + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefactos + + + + Spells + Hechizos + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Héroes + + + + Hero classes + + + + + Players + Jugadores + + + + None + Ninguno + + + + Day %1 + + RewardsWidget - + Rewards Recompensas - - Remove selected - Eliminar seleccionado + + + + + Add + - Delete all - Borrar todo + + + + Remove + - - Add or change - Añadir o modificar + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefactos + + + + + Spells + Hechizos + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Tipo + + + + + Value + Valor + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Héroes + + + + Hero classes + + + + + Players + Jugadores + + + + None + Ninguno + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Aceptar TownBulidingsWidget - + Buildings Edificios + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Resultados de la validación del mapa - + Map is not loaded No se ha cargado ningún mapa - + No factions allowed for player %1 - + No players allowed to play this map No hay jugadores autorizados a jugar en este mapa - + Map is allowed for one player and cannot be started El mapa está autorizado para un jugador y no se puede iniciar - + No human players allowed to play this map Ningún jugador humano puede jugar en este mapa - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner La instancia protegida %1 NOSEPUEDEMARCAR, pero debe tener un propietario NEUTRAL o jugador - + Object %1 is assigned to non-playable player %2 El artículo %1 está asignado al jugador no jugable %2 - + Town %1 has undefined owner %2 La ciudad %1 no tiene un propietario definido %2 - + Prison %1 must be a NEUTRAL %1 prisión debe ser NEUTRA - + Hero %1 must have an owner El héroe %1 debe tener un propietario - + Hero %1 is prohibited by map settings El héroe %1 está prohibido por la configuración del mapa - + Hero %1 has duplicate on map El héroe %1 tiene un duplicado en el mapa - + Hero %1 has an empty type and must be removed El héroe %1 tiene un tipo vacío y debe eliminarse - + Spell scroll %1 is prohibited by map settings %1 desplazamiento de hechizos está prohibido por la configuración del mapa - + Spell scroll %1 doesn't have instance assigned and must be removed Pergamino ortográfico %1 no tiene una instancia asignada y debe eliminarse - + Artifact %1 is prohibited by map settings El artefacto %1 está prohibido por la configuración del mapa - + Player %1 doesn't have any starting town El jugador %1 no tiene ciudad inicial - + Map name is not specified No se especifica el nombre del mapa - + Map description is not specified No se especifica la descripción del mapa - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 Se produce una excepción durante la validación: %1 - + Unknown exception occurs during validation Se produce una excepción desconocida durante la validación + + VictoryConditions + + + Form + + + + + Victory message + Mensaje de victoria + + + + Only for human players + Solo para jugadores humanos + + + + Allow standard victory + Permitir victoria estándar + + + + Parameters + Parámetros + + + + No special victory + Sin victoria especial + + + + Capture artifact + Capturar artefacto + + + + Hire creatures + Contratar criaturas + + + + Accumulate resources + Acumular recursos + + + + Construct building + Construir edificio + + + + Capture town + Capturar ciudad + + + + Defeat hero + Vencer héroe + + + + Transport artifact + Transportar artefacto + + + + Kill monster + + + WindowNewMap @@ -976,17 +1785,17 @@ Cancelar - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,27 +1803,27 @@ main - + Filepath of the map to open. Ruta del archivo del mapa a abrir. - + Extract original H3 archives into a separate folder. Extraer archivos originales de H3 en una carpeta separada. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Desde un archivo extraído, separa TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 y Un44 en imágenes PNG individuales. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Desde un archivo extraído, convierte imágenes individuales (encontradas en la carpeta Imágenes) de .pcx a png. - + Delete original files, for the ones split / converted. Eliminar archivos originales, por los que se han separado / convertido. diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index 2233f658e..ae2f61a24 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Налаштування армії - + Wide formation Широка формація - + Tight formation Щільна формація + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Назва мапи + + + + Map description + Опис мапи + + + + Limit maximum heroes level + Обмежити максимальний рівень героїв + + + + Difficulty + Складність + + GeneratorProgress @@ -27,6 +83,95 @@ Побудова мапи + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Повідомлення про програш + + + + 7 days without town + 7 днів без міста + + + + Parameters + Параметри + + + + No special loss + Немає особливої поразки + + + + Lose castle + Втратити місто + + + + Lose hero + Втратити героя + + + + Time expired + Закінчився час + + + + Days without town + Дні без міста + + MainWindow @@ -40,556 +185,499 @@ Файл - + Map Мапа - + Edit Редагування - + View Вигляд - + Player Гравець - + Toolbar Панель інструментів - + Minimap Мінімапа - + Map Objects View Перегляд об'єктів мапи - + Browser Навігатор - + Inspector Інспектор - + Property Властивість - + Value Значення - - Terrains View - Перегляд поверхні + + Tools + - - Brush - Кисть + + Painting + - + Terrains Землі - + Roads Шляхи - + Rivers Річки - + + Preview + + + + Open Відкрити - + Save Зберегти - + New Створити - + Save as... Зберегти як... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/З - - + + View underground Дивитись підземелля - + Pass Прохідність - + Cut Вирізати - + Copy Скопіювати - + Paste Вставити - + Fill Заповнити - + Fills the selection with obstacles Заповнити перешкодами - + Grid Сітка - + General Загальний - + Map title and description Назва та опис мапи - + Players settings Налаштування гравців - - + + Undo Відмінити - + Redo Повторити - + Erase Стерти - + Neutral Нейтральний - + Validate Перевірити - - - - + + + + Update appearance Оновити вигляд - + Recreate obstacles Оновити перешкоди - + Player 1 Гравець 1 - + Player 2 Гравець 2 - + Player 3 Гравець 3 - + Player 4 Гравець 4 - + Player 5 Гравець 5 - + Player 6 Гравець 6 - + Player 7 Гравець 7 - + Player 8 Гравець 8 - + Export as... Експортувати як... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation - + Unsaved changes will be lost, are you sure? - - Failed to open map - - - - - Cannot open map from this folder - - - - - Confirmation - - - - - Unsaved changes will be lost, are you sure? - - - - + Open map Відкрити мапу - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Всі підтримувані мапи (*.vmap *.h3m);;Мапи VCMI (*.vmap);;Мапи HoMM3 (*.h3m) - - + Save map Зберегти мапу - - + VCMI maps (*.vmap) Мапи VCMI - + Type Тип - + View surface Дивитись поверхню - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Налаштування мапи - + General Загальний - - Map name - Назва мапи - - - - Map description - Опис мапи - - - - Limit maximum heroes level - Обмежити максимальний рівень героїв - - - - Difficulty - Складність - - - + Mods Модифікації - - Mandatory mods for playing this map - Модифікації необхідні для гри на мапи - - - - Mod name - Назва модифікації - - - - Version - Версія - - - - Automatic assignment - Автоматичне визначення - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Встановити необхідні модифікації на основі об'єктів, розміщених на мапі. Цей метод може викликати проблеми, якщо у вас є налаштовані нагороди, гарнізони тощо з модів - - - - Map objects mods - Моди з об'єктами мапи - - - - Set all mods having a game content as mandatory - Встановити усі моди з ігровим контентом як обов'язкові - - - - Full content mods - Усі модифікації - - - + Events Події - + Victory Перемога - - Victory message - Повідомлення про перемогу - - - - Only for human players - Тільки для гравців-людей - - - - Allow standard victory - Дозволити типову перемогу - - - - - Parameters - Параметри - - - + Loss Програш - - 7 days without town - 7 днів без міста + + Timed + - - Defeat message - Повідомлення про програш + + Rumors + - + Abilities Уміння - + Spells Закляття - + Artifacts Артефакти - + Heroes Герої - + Ok Підтвердити - - - No special victory - Немає особливої перемоги - - - - Capture artifact - Отримати артефакт - - - - Hire creatures - Найняти істот - - - - Accumulate resources - Накопичити ресурси - - - - Construct building - Побудувати будівлю - - - - Capture town - Захопити місто - - - - Defeat hero - Перемогти героя - - - - Transport artifact - Доставити артефакт - - - - No special loss - Немає особливої поразки - - - - Lose castle - Втратити місто - - - - Lose hero - Втратити героя - - - - Time expired - Закінчився час - - - - Days without town - Дні без міста - MapView - + Can't place object @@ -597,55 +685,108 @@ MessageWidget - + Message Повідомлення + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + Назва модифікації + + + + Version + Версія + + + + Automatic assignment + Автоматичне визначення + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Встановити необхідні модифікації на основі об'єктів, розміщених на мапі. Цей метод може викликати проблеми, якщо у вас є налаштовані нагороди, гарнізони тощо з модів + + + + Map objects mods + Моди з об'єктами мапи + + + + Set all mods having a game content as mandatory + Встановити усі моди з ігровим контентом як обов'язкові + + + + Full content mods + Усі модифікації + + PlayerParams - + Human/CPU Людина/Комп'ютер - + CPU only Тільки комп'ютер - + Team Команда - + Main town Головне місто - + Color Колір - + + ... + + + + Random faction Випадкова фракція - + Generate hero at main Згенерувати героя - + (default) (за замовчуванням) - + Player ID: %1 Гравець %1 @@ -673,45 +814,634 @@ Підтвердити + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Мета місії + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Артефакти + + + + Spells + Закляття + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Герої + + + + Hero classes + + + + + Players + + + + + None + Відсутня + + + + Day %1 + + RewardsWidget - + Rewards Винагороди - - Remove selected - Видалити вибране + + + + + Add + - Delete all - Видалити усі + + + + Remove + - - Add or change - Додати або змінити + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Артефакти + + + + + Spells + Закляття + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Тип + + + + + Value + Значення + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Герої + + + + Hero classes + + + + + Players + + + + + None + Відсутня + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Підтвердити TownBulidingsWidget - + Buildings Будівлі + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -720,116 +1450,189 @@ Результати валідації карти - + Map is not loaded - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - + Town %1 has undefined owner %2 Місто %1 має невизначеного володаря %2 - + Prison %1 must be a NEUTRAL - + Hero %1 must have an owner - + Hero %1 is prohibited by map settings - + Hero %1 has duplicate on map - + Hero %1 has an empty type and must be removed - + Spell scroll %1 is prohibited by map settings - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - + Player %1 doesn't have any starting town - + Map name is not specified - + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation + + VictoryConditions + + + Form + + + + + Victory message + Повідомлення про перемогу + + + + Only for human players + Тільки для гравців-людей + + + + Allow standard victory + Дозволити типову перемогу + + + + Parameters + Параметри + + + + No special victory + Немає особливої перемоги + + + + Capture artifact + Отримати артефакт + + + + Hire creatures + Найняти істот + + + + Accumulate resources + Накопичити ресурси + + + + Construct building + Побудувати будівлю + + + + Capture town + Захопити місто + + + + Defeat hero + Перемогти героя + + + + Transport artifact + Доставити артефакт + + + + Kill monster + + + WindowNewMap @@ -982,17 +1785,17 @@ Скасувати - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -1000,27 +1803,27 @@ main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index 0d37f5242..dbd6df278 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Cài đặt quân - + Wide formation Đội hình rộng - + Tight formation Đội hình kín + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Tên bản đồ + + + + Map description + Mô tả bản đồ + + + + Limit maximum heroes level + Giới hạn cấp tướng tối đa + + + + Difficulty + Độ khó + + GeneratorProgress @@ -27,6 +83,95 @@ Tạo bản đồ + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Thông báo thất bại + + + + 7 days without town + 7 ngày không có thành + + + + Parameters + Tham số + + + + No special loss + Không có thất bại đặc biệt + + + + Lose castle + Mất thành + + + + Lose hero + Mất tướng + + + + Time expired + Hết thời gian + + + + Days without town + Số ngày không có thành + + MainWindow @@ -40,546 +185,499 @@ Tập tin - + Map Bản đồ - + Edit Hiệu chỉnh - + View Xem - + Player Người chơi - + Toolbar Thanh công cụ - + Minimap Bản đồ nhỏ - + Map Objects View Xem đối tượng bản đồ - + Browser Duyệt - + Inspector Giám định - + Property Đặc tính - + Value Giá trị - - Terrains View - Xem địa hình - - - - Brush - Quét - - - + Terrains Địa hình - + Roads Đường - + Rivers Sông - + Open Mở - + Save Lưu - + New Tạo mới - - Save as - Lưu vào + + Tools + - + + Painting + + + + + Preview + + + + + Save as... + + + + Ctrl+Shift+S Ctrl+Shift+S - + U/G U/G - - + + View underground Xem hang ngầm - + Pass Đi qua - + Cut Cắt - + Copy Sao chép - + Paste Dán - + Fill Làm đầy - + Fills the selection with obstacles Làm đầy vùng chọn với vật cản - + Grid Đường kẻ - + General Chung - + Map title and description Tên bản đồ và mô tả - + Players settings Cài đặt người chơi - - + + Undo Hoàn tác - + Redo Làm lại - + Erase Xóa - + Neutral Trung lập - + Validate Hiệu lực - - - - + + + + Update appearance Cập nhật hiện thị - + Recreate obstacles Tạo lại vật cản - + Player 1 Người chơi 1 - + Player 2 Người chơi 2 - + Player 3 Người chơi 3 - + Player 4 Người chơi 4 - + Player 5 Người chơi 5 - + Player 6 Người chơi 6 - + Player 7 Người chơi 7 - + Player 8 Người chơi 8 - + Export as... Xuất thành... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Xác nhận - + Unsaved changes will be lost, are you sure? Thay đổi chưa lưu sẽ bị mất, bạn có chắc chắn? - - Failed to open map - Không thể mở bản đồ - - - - Cannot open map from this folder - Không thể mở bản đồ từ thư mục này - - - + Open map Mở bản đồ - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Tất cả bản đồ hỗ trợ (*.vmap *.h3m);;Bản đồ VCMI (*.vmap);;Bản đồ HoMM3 (*.h3m) - - + Save map Lưu bản đồ - - + VCMI maps (*.vmap) Bản đồ VCMI (*.vmap) - + Type Loại - + View surface Xem bề mặt - + No objects selected Không mục tiêu được chọn - + This operation is irreversible. Do you want to continue? Thao tác này không thể đảo ngược. Bạn muốn tiếp tục? - + Errors occurred. %1 objects were not updated Xảy ra lỗi. %1 mục tiêu không được cập nhật - + Save to image Lưu thành ảnh + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Cài đặt bản đồ - + General Chung - - Map name - Tên bản đồ - - - - Map description - Mô tả bản đồ - - - - Limit maximum heroes level - Giới hạn cấp tướng tối đa - - - - Difficulty - Độ khó - - - + Mods Bản sửa đổi - - Mandatory mods for playing this map - Bản sửa đổi cần để chơi bản đồ này - - - - Mod name - Tên bản sửa đổi - - - - Version - Phiên bản - - - - Automatic assignment - Gán tự động - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Tập bản sửa đổi dựa vào vật thể đặt trên bản đồ. Phương pháp này có thể có vấn đề nếu bạn tùy chỉnh phần thưởng, lính đồn trú... từ các bản sửa đổi - - - - Map objects mods - Bản sửa đổi vật thể trên bản đồ - - - - Set all mods having a game content as mandatory - Tập bản sửa đổi cần cho nội dung trò chơi - - - - Full content mods - Bản sửa đổi nội dung đầy đủ - - - + Events Sự kiện - + Victory Chiến thắng - - Victory message - Thông báo chiến thắng - - - - Only for human players - Chỉ cho người - - - - Allow standard victory - Cho phép chiến thắng thông thường - - - - - Parameters - Tham số - - - + Loss Thất bại - - 7 days without town - 7 ngày không có thành + + Timed + - - Defeat message - Thông báo thất bại + + Rumors + - + Abilities Năng lực - + Spells Phép - + Artifacts Vật phẩm - + Heroes Tướng - + Ok Đồng ý - - - No special victory - Không có chiến thắng đặc biệt - - - - Capture artifact - Đoạt vật phẩm - - - - Hire creatures - Thuê quái - - - - Accumulate resources - Cộng dồn tài nguyên - - - - Construct building - Xây công trình - - - - Capture town - Đoạt thành - - - - Defeat hero - Đánh bại tướng - - - - Transport artifact - Vận chuyển vật phẩm - - - - No special loss - Không có thất bại đặc biệt - - - - Lose castle - Mất thành - - - - Lose hero - Mất tướng - - - - Time expired - Hết thời gian - - - - Days without town - Số ngày không có thành - MapView - + Can't place object Không thể đặt vật thể @@ -587,55 +685,108 @@ MessageWidget - + Message Thông báo + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + Tên bản sửa đổi + + + + Version + Phiên bản + + + + Automatic assignment + Gán tự động + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Tập bản sửa đổi dựa vào vật thể đặt trên bản đồ. Phương pháp này có thể có vấn đề nếu bạn tùy chỉnh phần thưởng, lính đồn trú... từ các bản sửa đổi + + + + Map objects mods + Bản sửa đổi vật thể trên bản đồ + + + + Set all mods having a game content as mandatory + Tập bản sửa đổi cần cho nội dung trò chơi + + + + Full content mods + Bản sửa đổi nội dung đầy đủ + + PlayerParams - + Human/CPU Người/Máy - + CPU only Chỉ máy - + Team Phe - + Main town Thành chính - + Color Màu - + + ... + + + + Random faction Thành ngẫu nhiên - + Generate hero at main Tạo tướng ban đầu - + (default) (mặc định) - + Player ID: %1 ID người chơi: %1 @@ -663,45 +814,634 @@ Đồng ý + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Mục tiêu nhiệm vụ + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Vật phẩm + + + + Spells + Phép + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Tướng + + + + Hero classes + + + + + Players + Người chơi + + + + None + Không + + + + Day %1 + + RewardsWidget - + Rewards Phần thưởng - - Remove selected - Bỏ chọn + + + + + Add + - Delete all - Xóa tất cả + + + + Remove + - - Add or change - Thêm hoặc sửa + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Vật phẩm + + + + + Spells + Phép + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Loại + + + + + Value + Giá trị + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Tướng + + + + Hero classes + + + + + Players + Người chơi + + + + None + Không + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Đồng ý TownBulidingsWidget - + Buildings Công trình + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -710,116 +1450,189 @@ Kết quả kiểm định bản đồ - + Map is not loaded Bản đồ không thể tải - + No factions allowed for player %1 Không có tộc được phép cho người chơi %1 - + No players allowed to play this map Không có người chơi được phép chơi bản đồ này - + Map is allowed for one player and cannot be started Bản đồ cho phép 1 người chơi nhưng không thể bắt đầu - + No human players allowed to play this map Không có người nào được phép chơi bản đồ này - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Thực thể %1 không gắn cờ nhưng phải có quái trung lập hoặc người chơi sở hữu - + Object %1 is assigned to non-playable player %2 Vật thể %1 được gán cho người không thể chơi %2 - + Town %1 has undefined owner %2 Thành %1 có chủ nhân không xác định %2 - + Prison %1 must be a NEUTRAL Nhà giam %1 phải trung lập - + Hero %1 must have an owner Tướng %1 phải có chủ - + Hero %1 is prohibited by map settings Tướng %1 bị cấm bởi bản đồ - + Hero %1 has duplicate on map Tướng %1 bị trùng trên bản đồ - + Hero %1 has an empty type and must be removed Tướng %1 có kiểu rỗng và phải được xóa - + Spell scroll %1 is prohibited by map settings Cuộn phép %1 bị cấm bởi bản đồ - + Spell scroll %1 doesn't have instance assigned and must be removed Cuộn phép %1 không có đối tượng được gán và phải được xóa - + Artifact %1 is prohibited by map settings Vật phẩm %1 bị cấm bởi bản đồ - + Player %1 doesn't have any starting town Người chơi %1 không có thành khởi đầu nào - + Map name is not specified Tên bản đồ không có - + Map description is not specified Mô tả bản đồ không có - + Map contains object from mod "%1", but doesn't require it Bản đồ chứa đối tượng từ bản mở rộng "%1", nhưng bản mở rộng đó không được yêu cầu - + Exception occurs during validation: %1 Ngoại lệ xuất hiện trong quá trình phê chuẩn: %1 - + Unknown exception occurs during validation Ngoại lệ chưa biết xuất hiện trong quá trình phê chuẩn: %1 + + VictoryConditions + + + Form + + + + + Victory message + Thông báo chiến thắng + + + + Only for human players + Chỉ cho người + + + + Allow standard victory + Cho phép chiến thắng thông thường + + + + Parameters + Tham số + + + + No special victory + Không có chiến thắng đặc biệt + + + + Capture artifact + Đoạt vật phẩm + + + + Hire creatures + Thuê quái + + + + Accumulate resources + Cộng dồn tài nguyên + + + + Construct building + Xây công trình + + + + Capture town + Đoạt thành + + + + Defeat hero + Đánh bại tướng + + + + Transport artifact + Vận chuyển vật phẩm + + + + Kill monster + + + WindowNewMap @@ -972,17 +1785,17 @@ Hủy - + No template Không dùng mẫu - + No template for parameters scecified. Random map cannot be generated. Không có mẫu cho tham số chỉ định. Bản đồ ngẫu nhiên không thể tạo - + RMG failure Tạo bản đồ ngẫu nhiên thất bại @@ -990,27 +1803,27 @@ main - + Filepath of the map to open. Đường dẫn bản đồ - + Extract original H3 archives into a separate folder. Giải nén dữ liệu H3 gốc vào 1 thư mục riêng. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Từ dữ liệu giải nén, chia TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 và Un44 thành những hình PNG riêng lẻ. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Từ dữ liệu giải nén, chuyển đổi các hình đơn (được tìm thấy trong thư mục Images) từ .pcx sang .png. - + Delete original files, for the ones split / converted. Xóa các tập tin gốc đã được phân chia / chuyển đổi. diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 25783058d..8ec6f51ef 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -78,7 +78,7 @@ std::list Validator::validate(const CMap * map) if(!hplayers) issues.emplace_back(tr("No human players allowed to play this map"), true); - std::set allHeroesOnMap; //used to find hero duplicated + std::set allHeroesOnMap; //used to find hero duplicated //checking all objects in the map for(auto o : map->objects) @@ -125,7 +125,7 @@ std::list Validator::validate(const CMap * map) } if(ins->type) { - if(!map->allowedHeroes[ins->type->getId().getNum()]) + if(map->allowedHeroes.count(ins->getHeroType()) == 0) issues.emplace_back(QString(tr("Hero %1 is prohibited by map settings")).arg(ins->type->getNameTranslated().c_str()), false); if(!allHeroesOnMap.insert(ins->type).second) @@ -142,15 +142,15 @@ std::list Validator::validate(const CMap * map) { if(ins->storedArtifact) { - if(!map->allowedSpells[ins->storedArtifact->getScrollSpellID()]) - issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toSpell(VLC->spells())->getNameTranslated().c_str()), false); + if(map->allowedSpells.count(ins->storedArtifact->getScrollSpellID()) == 0) + issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toEntity(VLC->spells())->getNameTranslated().c_str()), false); } else issues.emplace_back(QString(tr("Spell scroll %1 doesn't have instance assigned and must be removed")).arg(ins->instanceName.c_str()), true); } else { - if(ins->ID == Obj::ARTIFACT && !map->allowedArtifact[ins->subID]) + if(ins->ID == Obj::ARTIFACT && map->allowedArtifact.count(ins->getArtifact()) == 0) { issues.emplace_back(QString(tr("Artifact %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false); } diff --git a/mapeditor/vcmieditor.desktop b/mapeditor/vcmieditor.desktop index 9996b973f..115cf3145 100644 --- a/mapeditor/vcmieditor.desktop +++ b/mapeditor/vcmieditor.desktop @@ -1,10 +1,16 @@ [Desktop Entry] Type=Application Name=VCMI Map Editor -GenericName=Strategy Game Engine -Comment=Map editor for open engine of Heroes of Might and Magic 3 +Name[cs]=Editor map VCMI +Name[de]=VCMI Karteneditor +GenericName=Strategy Game Map Editor +GenericName[cs]=Editor map strategické hry +GenericName[de]=Karteneditor für Strategiespiel +Comment=Map editor for the open-source recreation of Heroes of Might & Magic III +Comment[cs]=Editor map enginu s otevřeným kódem pro Heroes of Might and Magic III +Comment[de]=Karteneditor für den Open-Source-Nachbau von Heroes of Might and Magic III Icon=vcmieditor Exec=vcmieditor Categories=Game;StrategyGame; -Version=1.0 -Keywords=heroes;homm3; +Version=1.1 +Keywords=heroes of might and magic;heroes;homm;homm3;strategy; diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index 68e3b70a7..cc673b4bd 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -231,7 +231,7 @@ void generateRandomMap(CMapGenerator & gen, MainWindow * window) std::unique_ptr generateEmptyMap(CMapGenOptions & options) { - std::unique_ptr map(new CMap); + auto map = std::make_unique(nullptr); map->version = EMapFormat::VCMI; map->width = options.getWidth(); map->height = options.getHeight(); @@ -281,7 +281,7 @@ void WindowNewMap::on_okButton_clicked() if(ui->checkSeed->isChecked() && !ui->lineSeed->text().isEmpty()) seed = ui->lineSeed->text().toInt(); - CMapGenerator generator(mapGenOptions, seed); + CMapGenerator generator(mapGenOptions, nullptr, seed); auto progressBarWnd = new GeneratorProgress(generator, this); progressBarWnd->show(); @@ -332,7 +332,7 @@ void WindowNewMap::on_humanCombo_activated(int index) ui->humanCombo->setCurrentIndex(humans); } - mapGenOptions.setPlayerCount(humans); + mapGenOptions.setHumanOrCpuPlayerCount(humans); int teams = mapGenOptions.getTeamCount(); if(teams > humans - 1) @@ -361,8 +361,10 @@ void WindowNewMap::on_humanCombo_activated(int index) void WindowNewMap::on_cpuCombo_activated(int index) { - int humans = mapGenOptions.getPlayerCount(); + int humans = mapGenOptions.getHumanOrCpuPlayerCount(); int cpu = ui->cpuCombo->currentData().toInt(); + + // FIXME: Use mapGenOption method only to calculate actual number of players for current template if(cpu > PlayerColor::PLAYER_LIMIT_I - humans) { cpu = PlayerColor::PLAYER_LIMIT_I - humans; @@ -455,7 +457,7 @@ void WindowNewMap::on_checkSeed_toggled(bool checked) void WindowNewMap::on_humanTeamsCombo_activated(int index) { - int humans = mapGenOptions.getPlayerCount(); + int humans = mapGenOptions.getHumanOrCpuPlayerCount(); int teams = ui->humanTeamsCombo->currentData().toInt(); if(teams >= humans) { diff --git a/scripting/erm/CMakeLists.txt b/scripting/erm/CMakeLists.txt index 99da95142..6e070e832 100644 --- a/scripting/erm/CMakeLists.txt +++ b/scripting/erm/CMakeLists.txt @@ -15,7 +15,7 @@ set(lib_HDRS ) add_library(vcmiERM SHARED ${lib_SRCS} ${lib_HDRS}) -target_link_libraries(vcmiERM Boost::boost ${VCMI_LIB_TARGET}) +target_link_libraries(vcmiERM Boost::boost vcmi) vcmi_set_output_dir(vcmiERM "scripting") enable_pch(vcmiERM) diff --git a/scripting/erm/ERMInterpreter.cpp b/scripting/erm/ERMInterpreter.cpp index 757d0697d..0d60743e7 100644 --- a/scripting/erm/ERMInterpreter.cpp +++ b/scripting/erm/ERMInterpreter.cpp @@ -321,7 +321,7 @@ namespace ERMConverter { ParamIO ret; ret.isInput = true; - ret.name = (std::visit(LVL1IexpToVar(), cmp)).str();; + ret.name = (std::visit(LVL1IexpToVar(), cmp)).str(); return ret; } @@ -1377,7 +1377,7 @@ struct ScriptScanner { if(std::holds_alternative(cmd)) //TCommand { - Tcommand tcmd = std::get(cmd); + auto tcmd = std::get(cmd); struct Visitor { void operator()(const ERM::Ttrigger& t) const @@ -1423,7 +1423,7 @@ bool ERMInterpreter::isATrigger( const ERM::TLine & line ) { if(std::holds_alternative(line)) { - TVExp vexp = std::get(line); + auto vexp = std::get(line); if(vexp.children.empty()) return false; @@ -1442,7 +1442,7 @@ bool ERMInterpreter::isATrigger( const ERM::TLine & line ) } else if(std::holds_alternative(line)) { - TERMline ermline = std::get(line); + auto ermline = std::get(line); return std::holds_alternative(ermline) && isCMDATrigger( std::get(ermline) ); } else @@ -1511,10 +1511,10 @@ ERM::TTriggerBase & ERMInterpreter::retrieveTrigger(ERM::TLine & line) { if(std::holds_alternative(line)) { - ERM::TERMline &tl = std::get(line); + auto &tl = std::get(line); if(std::holds_alternative(tl)) { - ERM::Tcommand &tcm = std::get(tl); + auto &tcm = std::get(tl); if(std::holds_alternative(tcm.cmd)) { return std::get(tcm.cmd); diff --git a/scripting/erm/ERMParser.h b/scripting/erm/ERMParser.h index 7697793ee..f4d8090cd 100644 --- a/scripting/erm/ERMParser.h +++ b/scripting/erm/ERMParser.h @@ -94,7 +94,8 @@ namespace ERM struct TArithmeticOp { - TIexp lhs, rhs; + TIexp lhs; + TIexp rhs; char opcode; }; @@ -145,7 +146,8 @@ namespace ERM struct TComparison { std::string compSign; - TIexp lhs, rhs; + TIexp lhs; + TIexp rhs; }; struct Tcondition; diff --git a/scripting/lua/CMakeLists.txt b/scripting/lua/CMakeLists.txt index a376c62c7..efc9c0242 100644 --- a/scripting/lua/CMakeLists.txt +++ b/scripting/lua/CMakeLists.txt @@ -83,7 +83,7 @@ set(lib_HDRS ) add_library(vcmiLua SHARED ${lib_SRCS} ${lib_HDRS}) -target_link_libraries(vcmiLua Boost::boost luajit::luajit ${VCMI_LIB_TARGET}) +target_link_libraries(vcmiLua Boost::boost luajit::luajit vcmi) vcmi_set_output_dir(vcmiLua "scripting") enable_pch(vcmiLua) diff --git a/scripting/lua/LuaScriptModule.cpp b/scripting/lua/LuaScriptModule.cpp index fb3ef8a76..99ac829e4 100644 --- a/scripting/lua/LuaScriptModule.cpp +++ b/scripting/lua/LuaScriptModule.cpp @@ -17,7 +17,7 @@ #define strcpy_s(a, b, c) strncpy(a, c, b) #endif -static const char *g_cszAiName = "Lua interpreter"; +static const char * const g_cszAiName = "Lua interpreter"; VCMI_LIB_NAMESPACE_BEGIN diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 2eda9ac47..47aacb458 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -19,7 +19,7 @@ #include "api/Registry.h" -#include "../../lib/JsonNode.h" +#include "../../lib/json/JsonNode.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/battle/IBattleInfoCallback.h" #include "../../lib/CGameInfoCallback.h" @@ -347,8 +347,8 @@ void LuaContext::pop(JsonNode & value) break; case LUA_TTABLE: { - JsonNode asVector(JsonNode::JsonType::DATA_VECTOR); - JsonNode asStruct(JsonNode::JsonType::DATA_STRUCT); + JsonNode asVector; + JsonNode asStruct; lua_pushnil(L); /* first key */ diff --git a/scripting/lua/LuaSpellEffect.cpp b/scripting/lua/LuaSpellEffect.cpp index c039567f3..91db47955 100644 --- a/scripting/lua/LuaSpellEffect.cpp +++ b/scripting/lua/LuaSpellEffect.cpp @@ -18,6 +18,7 @@ #include "../../lib/battle/Unit.h" #include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/json/JsonUtils.h" #include "../../lib/serializer/JsonSerializeFormat.h" static const std::string APPLICABLE_GENERAL = "applicable"; @@ -75,7 +76,7 @@ bool LuaSpellEffect::applicable(Problem & problem, const Mechanics * m) const if(response.getType() != JsonNode::JsonType::DATA_BOOL) { logMod->error("Invalid API response from script %s.", script->getName()); - logMod->debug(response.toJson(true)); + logMod->debug(response.toCompactString()); return false; } return response.Bool(); @@ -97,12 +98,12 @@ bool LuaSpellEffect::applicable(Problem & problem, const Mechanics * m, const Ef for(const auto & dest : target) { JsonNode targetData; - targetData.Vector().push_back(JsonUtils::intNode(dest.hexValue.hex)); + targetData.Vector().emplace_back(dest.hexValue.hex); if(dest.unitValue) - targetData.Vector().push_back(JsonUtils::intNode(dest.unitValue->unitId())); + targetData.Vector().emplace_back(dest.unitValue->unitId()); else - targetData.Vector().push_back(JsonUtils::intNode(-1)); + targetData.Vector().emplace_back(-1); requestP.Vector().push_back(targetData); } @@ -115,7 +116,7 @@ bool LuaSpellEffect::applicable(Problem & problem, const Mechanics * m, const Ef if(response.getType() != JsonNode::JsonType::DATA_BOOL) { logMod->error("Invalid API response from script %s.", script->getName()); - logMod->debug(response.toJson(true)); + logMod->debug(response.toCompactString()); return false; } return response.Bool(); @@ -140,12 +141,12 @@ void LuaSpellEffect::apply(ServerCallback * server, const Mechanics * m, const E for(const auto & dest : target) { JsonNode targetData; - targetData.Vector().push_back(JsonUtils::intNode(dest.hexValue.hex)); + targetData.Vector().emplace_back(dest.hexValue.hex); if(dest.unitValue) - targetData.Vector().push_back(JsonUtils::intNode(dest.unitValue->unitId())); + targetData.Vector().emplace_back(dest.unitValue->unitId()); else - targetData.Vector().push_back(JsonUtils::intNode(-1)); + targetData.Vector().emplace_back(-1); requestP.Vector().push_back(targetData); } diff --git a/scripting/lua/LuaSpellEffect.h b/scripting/lua/LuaSpellEffect.h index effb620e7..54b11d4c2 100644 --- a/scripting/lua/LuaSpellEffect.h +++ b/scripting/lua/LuaSpellEffect.h @@ -35,7 +35,7 @@ public: LuaSpellEffectFactory(const Script * script_); virtual ~LuaSpellEffectFactory(); - virtual Effect * create() const override; + Effect * create() const override; private: const Script * script; diff --git a/scripting/lua/LuaStack.cpp b/scripting/lua/LuaStack.cpp index 18dc12803..71f4798c3 100644 --- a/scripting/lua/LuaStack.cpp +++ b/scripting/lua/LuaStack.cpp @@ -11,7 +11,7 @@ #include "LuaStack.h" -#include "../../lib/JsonNode.h" +#include "../../lib/json/JsonNode.h" #include "../../lib/int3.h" VCMI_LIB_NAMESPACE_BEGIN @@ -183,8 +183,8 @@ bool LuaStack::tryGet(int position, JsonNode & value) return tryGet(position, value.String()); case LUA_TTABLE: { - JsonNode asVector(JsonNode::JsonType::DATA_VECTOR); - JsonNode asStruct(JsonNode::JsonType::DATA_STRUCT); + JsonNode asVector; + JsonNode asStruct; lua_pushnil(L); /* first key */ diff --git a/scripting/lua/LuaStack.h b/scripting/lua/LuaStack.h index 7963c1d77..3f615c0fe 100644 --- a/scripting/lua/LuaStack.h +++ b/scripting/lua/LuaStack.h @@ -26,13 +26,13 @@ namespace detail template struct IsRegularClass { - static constexpr auto value = std::is_class::value && !std::is_base_of::value; + static constexpr auto value = std::is_class_v && !std::is_base_of_v; }; template struct IsIdClass { - static constexpr auto value = std::is_class::value && std::is_base_of::value; + static constexpr auto value = std::is_class_v && std::is_base_of_v; }; } @@ -61,13 +61,13 @@ public: pushNil(); } - template::value && !std::is_same::value, int>::type = 0> + template && !std::is_same_v, int> = 0> void push(const T value) { pushInteger(static_cast(value)); } - template::value, int>::type = 0> + template, int> = 0> void push(const T value) { pushInteger(static_cast(value)); @@ -75,13 +75,13 @@ public: void push(const int3 & value); - template::value, int>::type = 0> + template::value, int> = 0> void push(const T & value) { pushInteger(static_cast(value.getNum())); } - template::value, int>::type = 0> + template::value, int> = 0> void push(T * value) { using UData = T *; @@ -107,7 +107,7 @@ public: lua_setmetatable(L, -2); } - template::value, int>::type = 0> + template::value, int> = 0> void push(std::shared_ptr value) { using UData = std::shared_ptr; @@ -133,7 +133,7 @@ public: lua_setmetatable(L, -2); } - template::value, int>::type = 0> + template::value, int> = 0> void push(std::unique_ptr && value) { using UData = std::unique_ptr; @@ -163,7 +163,7 @@ public: bool tryGet(int position, bool & value); - template::value && !std::is_same::value, int>::type = 0> + template && !std::is_same_v, int> = 0> bool tryGet(int position, T & value) { lua_Integer temp; @@ -178,7 +178,7 @@ public: } } - template::value, int>::type = 0> + template::value, int> = 0> bool tryGet(int position, T & value) { lua_Integer temp; @@ -193,7 +193,7 @@ public: } } - template::value, int>::type = 0> + template, int> = 0> bool tryGet(int position, T & value) { lua_Integer temp; @@ -213,10 +213,10 @@ public: bool tryGet(int position, double & value); bool tryGet(int position, std::string & value); - template::value && std::is_const::value, int>::type = 0> + template::value && std::is_const_v, int> = 0> STRONG_INLINE bool tryGet(int position, T * & value) { - using NCValue = typename std::remove_const::type; + using NCValue = typename std::remove_const_t; using UData = NCValue *; using CUData = T *; @@ -224,16 +224,16 @@ public: return tryGetCUData(position, value); } - template::value && !std::is_const::value, int>::type = 0> + template::value && !std::is_const_v, int> = 0> STRONG_INLINE bool tryGet(int position, T * & value) { return tryGetUData(position, value); } - template::value && std::is_const::value, int>::type = 0> + template::value && std::is_const_v, int> = 0> STRONG_INLINE bool tryGet(int position, std::shared_ptr & value) { - using NCValue = typename std::remove_const::type; + using NCValue = typename std::remove_const_t; using UData = std::shared_ptr; using CUData = std::shared_ptr; @@ -241,7 +241,7 @@ public: return tryGetCUData, UData, CUData>(position, value); } - template::value && !std::is_const::value, int>::type = 0> + template::value && !std::is_const_v, int> = 0> STRONG_INLINE bool tryGet(int position, std::shared_ptr & value) { return tryGetUData>(position, value); diff --git a/scripting/lua/LuaWrapper.h b/scripting/lua/LuaWrapper.h index 83071b3b3..a6911bb6d 100644 --- a/scripting/lua/LuaWrapper.h +++ b/scripting/lua/LuaWrapper.h @@ -124,7 +124,7 @@ template class OpaqueWrapper : public RegistarBase { public: - using ObjectType = typename std::remove_cv::type; + using ObjectType = typename std::remove_cv_t; using UDataType = ObjectType *; using CUDataType = const ObjectType *; @@ -163,7 +163,7 @@ template class SharedWrapper : public RegistarBase { public: - using ObjectType = typename std::remove_cv::type; + using ObjectType = typename std::remove_cv_t; using UDataType = std::shared_ptr; using CustomRegType = detail::CustomRegType; @@ -208,7 +208,7 @@ template class UniqueOpaqueWrapper : public api::Registar { public: - using ObjectType = typename std::remove_cv::type; + using ObjectType = typename std::remove_cv_t; using UDataType = std::unique_ptr; using CustomRegType = detail::CustomRegType; diff --git a/scripting/lua/api/BonusSystem.cpp b/scripting/lua/api/BonusSystem.cpp index 2ce613ab0..f15acebec 100644 --- a/scripting/lua/api/BonusSystem.cpp +++ b/scripting/lua/api/BonusSystem.cpp @@ -11,7 +11,7 @@ #include "BonusSystem.h" -#include "../../../lib/JsonNode.h" +#include "../../../lib/json/JsonNode.h" #include "../../../lib/bonuses/BonusList.h" #include "../../../lib/bonuses/Bonus.h" diff --git a/scripting/lua/api/BonusSystem.h b/scripting/lua/api/BonusSystem.h index 657243670..ebaabc1e0 100644 --- a/scripting/lua/api/BonusSystem.h +++ b/scripting/lua/api/BonusSystem.h @@ -44,7 +44,7 @@ public: static int toJsonNode(lua_State * L); protected: - virtual void adjustStaticTable(lua_State * L) const override; + void adjustStaticTable(lua_State * L) const override; }; class BonusListProxy : public SharedWrapper @@ -56,7 +56,7 @@ public: static std::shared_ptr index(std::shared_ptr self, int key); protected: - virtual void adjustMetatable(lua_State * L) const override; + void adjustMetatable(lua_State * L) const override; }; class BonusBearerProxy : public OpaqueWrapper diff --git a/scripting/lua/api/GameCb.cpp b/scripting/lua/api/GameCb.cpp index 7227f403b..0aa3cd0e6 100644 --- a/scripting/lua/api/GameCb.cpp +++ b/scripting/lua/api/GameCb.cpp @@ -29,7 +29,6 @@ VCMI_REGISTER_CORE_SCRIPT_API(GameCbProxy, "Game"); const std::vector GameCbProxy::REGISTER_CUSTOM = { {"getDate", LuaMethodWrapper::invoke, false}, - {"isAllowed", LuaMethodWrapper::invoke, false}, {"getPlayer", LuaMethodWrapper::invoke, false}, {"getHero", LuaMethodWrapper::invoke, false}, diff --git a/scripting/lua/api/Registry.cpp b/scripting/lua/api/Registry.cpp index 0b86a3d42..845ae4544 100644 --- a/scripting/lua/api/Registry.cpp +++ b/scripting/lua/api/Registry.cpp @@ -22,7 +22,7 @@ Registry::Registry() = default; Registry * Registry::get() { - static std::unique_ptr Instance = std::unique_ptr(new Registry()); + static auto Instance = std::unique_ptr(new Registry()); return Instance.get(); } @@ -53,7 +53,7 @@ TypeRegistry::TypeRegistry() TypeRegistry * TypeRegistry::get() { - static std::unique_ptr Instance = std::unique_ptr(new TypeRegistry()); + static auto Instance = std::unique_ptr(new TypeRegistry()); return Instance.get(); } diff --git a/scripting/lua/api/events/SubscriptionRegistryProxy.h b/scripting/lua/api/events/SubscriptionRegistryProxy.h index b84002051..b4f34f93d 100644 --- a/scripting/lua/api/events/SubscriptionRegistryProxy.h +++ b/scripting/lua/api/events/SubscriptionRegistryProxy.h @@ -42,7 +42,7 @@ public: using EventType = typename EventProxy::ObjectType; using RegistryType = ::events::SubscriptionRegistry; - static_assert(std::is_base_of<::events::Event, EventType>::value, "Invalid template parameter"); + static_assert(std::is_base_of_v<::events::Event, EventType>, "Invalid template parameter"); static int subscribeBefore(lua_State * L) { diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 7a0e39c98..2cb20219b 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -27,6 +27,7 @@ #include "../lib/CCreatureSet.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" +#include "../lib/CPlayerState.h" #include "../lib/CSoundBase.h" #include "../lib/CThreadHelper.h" #include "../lib/CTownHandler.h" @@ -47,20 +48,22 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" +#include "../lib/mapObjects/CGMarket.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapObjects/MiscObjects.h" #include "../lib/modding/ModIncompatibility.h" #include "../lib/networkPacks/StackLocation.h" #include "../lib/pathfinder/CPathfinder.h" #include "../lib/pathfinder/PathfinderOptions.h" #include "../lib/pathfinder/TurnInfo.h" -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/registerTypes/RegisterTypesServerPacks.h" #include "../lib/rmg/CMapGenOptions.h" -#include "../lib/serializer/CTypeList.h" -#include "../lib/serializer/Cast.h" +#include "../lib/serializer/CSaveFile.h" +#include "../lib/serializer/CLoadFile.h" #include "../lib/serializer/Connection.h" -#include "../lib/serializer/JsonSerializer.h" #include "../lib/spells/CSpellHandler.h" @@ -79,7 +82,7 @@ template class CApplyOnGH; class CBaseForGHApply { public: - virtual bool applyOnGH(CGameHandler * gh, CGameState * gs, void * pack) const =0; + virtual bool applyOnGH(CGameHandler * gh, CGameState * gs, CPack * pack) const =0; virtual ~CBaseForGHApply(){} template static CBaseForGHApply *getApplier(const U * t=nullptr) { @@ -90,7 +93,7 @@ public: template class CApplyOnGH : public CBaseForGHApply { public: - bool applyOnGH(CGameHandler * gh, CGameState * gs, void * pack) const override + bool applyOnGH(CGameHandler * gh, CGameState * gs, CPack * pack) const override { T *ptr = static_cast(pack); try @@ -113,7 +116,7 @@ template <> class CApplyOnGH : public CBaseForGHApply { public: - bool applyOnGH(CGameHandler * gh, CGameState * gs, void * pack) const override + bool applyOnGH(CGameHandler * gh, CGameState * gs, CPack * pack) const override { logGlobal->error("Cannot apply on GH plain CPack!"); assert(0); @@ -187,25 +190,21 @@ void CGameHandler::levelUpHero(const CGHeroInstance * hero) sps.val = 1; sendAndApply(&sps); - PrepareHeroLevelUp pre; - pre.heroId = hero->id; - sendAndApply(&pre); - HeroLevelUp hlu; hlu.player = hero->tempOwner; hlu.heroId = hero->id; hlu.primskill = primarySkill; - hlu.skills = pre.skills; + hlu.skills = hero->getLevelUpProposedSecondarySkills(heroPool->getHeroSkillsRandomGenerator(hero->getHeroType())); if (hlu.skills.size() == 0) { sendAndApply(&hlu); levelUpHero(hero); } - else if (hlu.skills.size() == 1) + else if (hlu.skills.size() == 1 || !hero->getOwner().isValidPlayer()) { sendAndApply(&hlu); - levelUpHero(hero, pre.skills.front()); + levelUpHero(hero, hlu.skills.front()); } else if (hlu.skills.size() > 1) { @@ -257,11 +256,12 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) break; case ECommander::HEALTH: scp.accumulatedBonus.type = BonusType::STACK_HEALTH; - scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_BASE; + scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_ALL; //TODO: check how it accumulates in original WoG with artifacts such as vial of life blood, elixir of life etc. break; case ECommander::DAMAGE: scp.accumulatedBonus.type = BonusType::CREATURE_DAMAGE; - scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_BASE; + scp.accumulatedBonus.subtype = BonusCustomSubtype::creatureDamageBoth; + scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_ALL; break; case ECommander::SPEED: scp.accumulatedBonus.type = BonusType::STACKS_SPEED; @@ -368,52 +368,60 @@ void CGameHandler::expGiven(const CGHeroInstance *hero) // levelUpHero(hero); } -void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs) +void CGameHandler::giveExperience(const CGHeroInstance * hero, TExpType amountToGain) { - if (which == PrimarySkill::EXPERIENCE) // Check if scenario limit reached - { - if (gs->map->levelLimit != 0) - { - TExpType expLimit = VLC->heroh->reqExp(gs->map->levelLimit); - TExpType resultingExp = abs ? val : hero->exp + val; - if (resultingExp > expLimit) - { - // set given experience to max possible, but don't decrease if hero already over top - abs = true; - val = std::max(expLimit, hero->exp); + TExpType maxExp = VLC->heroh->reqExp(VLC->heroh->maxSupportedLevel()); + TExpType currExp = hero->exp; - InfoWindow iw; - iw.player = hero->tempOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 1); //can gain no more XP - iw.text.replaceRawString(hero->getNameTranslated()); - sendAndApply(&iw); - } - } + if (gs->map->levelLimit != 0) + maxExp = VLC->heroh->reqExp(gs->map->levelLimit); + + TExpType canGainExp = 0; + if (maxExp > currExp) + canGainExp = maxExp - currExp; + + if (amountToGain > canGainExp) + { + // set given experience to max possible, but don't decrease if hero already over top + amountToGain = canGainExp; + + InfoWindow iw; + iw.player = hero->tempOwner; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 1); //can gain no more XP + iw.text.replaceRawString(hero->getNameTranslated()); + sendAndApply(&iw); } + SetPrimSkill sps; + sps.id = hero->id; + sps.which = PrimarySkill::EXPERIENCE; + sps.abs = false; + sps.val = amountToGain; + sendAndApply(&sps); + + //hero may level up + if (hero->commander && hero->commander->alive) + { + //FIXME: trim experience according to map limit? + SetCommanderProperty scp; + scp.heroid = hero->id; + scp.which = SetCommanderProperty::EXPERIENCE; + scp.amount = amountToGain; + sendAndApply (&scp); + CBonusSystemNode::treeHasChanged(); + } + + expGiven(hero); +} + +void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs) +{ SetPrimSkill sps; sps.id = hero->id; sps.which = which; sps.abs = abs; sps.val = val; sendAndApply(&sps); - - //only for exp - hero may level up - if (which == PrimarySkill::EXPERIENCE) - { - if (hero->commander && hero->commander->alive) - { - //FIXME: trim experience according to map limit? - SetCommanderProperty scp; - scp.heroid = hero->id; - scp.which = SetCommanderProperty::EXPERIENCE; - scp.amount = val; - sendAndApply (&scp); - CBonusSystemNode::treeHasChanged(); - } - - expGiven(hero); - } } void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs) @@ -437,7 +445,10 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh void CGameHandler::handleClientDisconnection(std::shared_ptr c) { if(lobby->getState() == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) + { + assert(0); // game should have shut down before reaching this point! return; + } for(auto & playerConnections : connections) { @@ -463,12 +474,12 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) PackageApplied applied; applied.player = pack->player; applied.result = succesfullyApplied; - applied.packType = typeList.getTypeID(pack); + applied.packType = CTypeList::getInstance().getTypeID(pack); applied.requestID = pack->requestID; pack->c->sendPack(&applied); }; - CBaseForGHApply * apply = applier->getApplier(typeList.getTypeID(pack)); //and appropriate applier object + CBaseForGHApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //and appropriate applier object if(isBlockedByQueries(pack, pack->player)) { sendPackageResponse(false); @@ -493,11 +504,6 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) vstd::clear_pointer(pack); } - -CGameHandler::CGameHandler() - : turnTimerHandler(*this) -{} - CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) @@ -511,7 +517,6 @@ CGameHandler::CGameHandler(CVCMIServer * lobby) , turnTimerHandler(*this) { QID = 1; - IObjectInterface::cb = this; applier = std::make_shared>(); registerTypesServerPacks(*applier); @@ -522,6 +527,7 @@ CGameHandler::~CGameHandler() { delete spellEnv; delete gs; + gs = nullptr; } void CGameHandler::reinitScripting() @@ -540,7 +546,7 @@ void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTrack } CMapService mapService; gs = new CGameState(); - gs->preInit(VLC); + gs->preInit(VLC, this); logGlobal->info("Gamestate created!"); gs->init(&mapService, si, progressTracking); logGlobal->info("Gamestate initialized!"); @@ -551,6 +557,12 @@ void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTrack for (auto & elem : gs->players) turnOrder->addPlayer(elem.first); + for (auto & elem : gs->map->allHeroes) + { + if(elem) + heroPool->getHeroSkillsRandomGenerator(elem->getHeroType()); // init RMG seed + } + reinitScripting(); } @@ -659,7 +671,7 @@ void CGameHandler::onNewTurn() { if (obj && obj->ID == Obj::PRISON) //give imprisoned hero 0 exp to level him up. easiest to do at this point { - changePrimSkill (getHero(obj->id), PrimarySkill::EXPERIENCE, 0); + giveExperience(getHero(obj->id), 0); } } } @@ -819,7 +831,7 @@ void CGameHandler::onNewTurn() if (!t->creatures.at(k).second.empty()) // there are creatures at this level { ui32 &availableCount = sac.creatures.at(k).first; - const CCreature *cre = VLC->creh->objects.at(t->creatures.at(k).second.back()); + const CCreature *cre = t->creatures.at(k).second.back().toCreature(); if (n.specialWeek == NewTurn::PLAGUE) availableCount = t->creatures.at(k).first / 2; //halve their number, no growth @@ -860,7 +872,7 @@ void CGameHandler::onNewTurn() fw.mode = ETileVisibility::REVEALED; fw.player = player; // find all hidden tiles - const auto fow = getPlayerTeam(player)->fogOfWarMap; + const auto & fow = getPlayerTeam(player)->fogOfWarMap; auto shape = fow->shape(); for(size_t z = 0; z < shape[0]; z++) @@ -878,7 +890,7 @@ void CGameHandler::onNewTurn() { if (getPlayerStatus(player.first) == EPlayerStatus::INGAME && getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES) - changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN); + changeFogOfWar(t->visitablePos(), t->getFirstBonus(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN); } } } @@ -886,7 +898,7 @@ void CGameHandler::onNewTurn() if (newMonth) { SetAvailableArtifacts saa; - saa.id = -1; + saa.id = ObjectInstanceID::NONE; pickAllowedArtsSet(saa.arts, getRandomGenerator()); sendAndApply(&saa); } @@ -908,24 +920,24 @@ void CGameHandler::onNewTurn() { case NewTurn::DOUBLE_GROWTH: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); + iw.text.replaceNameSingular(n.creatureid); + iw.text.replaceNameSingular(n.creatureid); break; case NewTurn::PLAGUE: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132); break; case NewTurn::BONUS_GROWTH: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); + iw.text.replaceNameSingular(n.creatureid); + iw.text.replaceNameSingular(n.creatureid); break; case NewTurn::DEITYOFFIRE: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 42); //%s imp - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 42); //%s imp - iw.text.replacePositiveNumber(15); //%+d 15 - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 43); //%s familiar - iw.text.replacePositiveNumber(15); //%+d 15 + iw.text.replaceNameSingular(CreatureID::IMP); //%s imp + iw.text.replaceNameSingular(CreatureID::IMP); //%s imp + iw.text.replacePositiveNumber(15);//%+d 15 + iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar + iw.text.replacePositiveNumber(15);//%+d 15 break; default: if (newMonth) @@ -962,11 +974,11 @@ void CGameHandler::onNewTurn() synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that } -void CGameHandler::run(bool resume) +void CGameHandler::start(bool resume) { LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); - for (auto cc : lobby->connections) + for (auto cc : lobby->activeConnections) { auto players = lobby->getAllClientPlayers(cc->connectionID); std::stringstream sbuffer; @@ -974,10 +986,7 @@ void CGameHandler::run(bool resume) for (PlayerColor color : players) { sbuffer << color << " "; - { - boost::unique_lock lock(gsm); - connections[color].insert(cc); - } + connections[color].insert(cc); } logGlobal->info(sbuffer.str()); } @@ -997,18 +1006,11 @@ void CGameHandler::run(bool resume) events::GameResumed::defaultExecute(serverEventBus.get()); turnOrder->onGameStarted(); +} - //wait till game is done - auto clockLast = std::chrono::steady_clock::now(); - while(lobby->getState() == EServerState::GAMEPLAY) - { - const auto clockDuration = std::chrono::steady_clock::now() - clockLast; - const int timePassed = std::chrono::duration_cast(clockDuration).count(); - clockLast += clockDuration; - turnTimerHandler.update(timePassed); - boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); - - } +void CGameHandler::tick(int millisecondsPassed) +{ + turnTimerHandler.update(millisecondsPassed); } void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) @@ -1129,16 +1131,16 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo }; if (guardian && getVisitingHero(guardian) != nullptr) - return complainRet("Cannot move hero, destination monster is busy!"); + return complainRet("You cannot move your hero there. Simultaneous turns are active and another player is interacting with this wandering monster!"); - if (objectToVisit && getVisitingHero(objectToVisit) != nullptr) - return complainRet("Cannot move hero, destination object is busy!"); + if (objectToVisit && getVisitingHero(objectToVisit) != nullptr && getVisitingHero(objectToVisit) != h) + return complainRet("You cannot move your hero there. Simultaneous turns are active and another player is interacting with this map object!"); if (objectToVisit && objectToVisit->getOwner().isValidPlayer() && getPlayerRelations(objectToVisit->getOwner(), h->getOwner()) == PlayerRelations::ENEMIES && !turnOrder->isContactAllowed(objectToVisit->getOwner(), h->getOwner())) - return complainRet("Cannot move hero, destination player is busy!"); + return complainRet("You cannot move your hero there. This object belongs to another player and simultaneous turns are still active!"); //it's a rock or blocked and not visitable tile //OR hero is on land and dest is water and (there is not present only one object - boat) @@ -1187,7 +1189,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if (leavingTile == LEAVING_TILE) leaveTile(); - if (isInTheMap(guardPos)) + if (lookForGuards == CHECK_FOR_GUARDS && isInTheMap(guardPos)) tmh.attackedFrom = std::make_optional(guardPos); tmh.result = result; @@ -1328,8 +1330,8 @@ bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owner) { PlayerColor oldOwner = getOwner(obj->id); - SetObjectProperty sop(obj->id, ObjProperty::OWNER, owner.getNum()); - sendAndApply(&sop); + + setObjPropertyID(obj->id, ObjProperty::OWNER, owner); std::set playerColors = {owner, oldOwner}; checkVictoryLossConditions(playerColors); @@ -1350,7 +1352,7 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne InfoWindow iw; iw.player = oldOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 6); //%s, you have lost your last town. If you do not conquer another town in the next week, you will be eliminated. - iw.text.replaceLocalString(EMetaText::COLOR, oldOwner.getNum()); + iw.text.replaceName(oldOwner); sendAndApply(&iw); } } @@ -1460,6 +1462,9 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta sendAndApply(&vc); visitCastleObjects(obj, hero); giveSpells (obj, hero); + + if (obj->visitingHero && obj->garrisonHero) + useScholarSkill(obj->visitingHero->id, obj->garrisonHero->id); checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact? } @@ -1503,6 +1508,15 @@ void CGameHandler::setMovePoints(SetMovePoints * smp) sendAndApply(smp); } +void CGameHandler::setMovePoints(ObjectInstanceID hid, int val, bool absolute) +{ + SetMovePoints smp; + smp.hid = hid; + smp.val = val; + smp.absolute = absolute; + sendAndApply(&smp); +} + void CGameHandler::setManaPoints(ObjectInstanceID hid, int val) { SetMana sm; @@ -1551,8 +1565,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t if (!ScholarSpellLevel || !h1->hasSpellbook() || !h2->hasSpellbook()) return;//no scholar skill or no spellbook - int h1Lvl = std::min(ScholarSpellLevel, h1->maxSpellLevel()), - h2Lvl = std::min(ScholarSpellLevel, h2->maxSpellLevel());//heroes can receive this levels + int h1Lvl = std::min(ScholarSpellLevel, h1->maxSpellLevel());//heroes can receive these levels + int h2Lvl = std::min(ScholarSpellLevel, h2->maxSpellLevel()); ChangeSpells cs1; cs1.learn = true; @@ -1571,11 +1585,12 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t if (!cs1.spells.empty() || !cs2.spells.empty())//create a message { - int ScholarSkillLevel = std::max(h1->getSecSkillLevel(SecondarySkill::SCHOLAR), - h2->getSecSkillLevel(SecondarySkill::SCHOLAR)); + SecondarySkill scholarSkill = SecondarySkill::SCHOLAR; + + int scholarSkillLevel = std::max(h1->getSecSkillLevel(scholarSkill), h2->getSecSkillLevel(scholarSkill)); InfoWindow iw; iw.player = h1->tempOwner; - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, 18, ScholarSkillLevel, 0); + iw.components.emplace_back(ComponentType::SEC_SKILL, scholarSkill, scholarSkillLevel); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 139);//"%s, who has studied magic extensively, iw.text.replaceRawString(h1->getNameTranslated()); @@ -1586,8 +1601,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t int size = static_cast(cs2.spells.size()); for (auto it : cs2.spells) { - iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0); - iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); + iw.components.emplace_back(ComponentType::SPELL, it); + iw.text.appendName(it); switch (size--) { case 2: @@ -1614,8 +1629,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t int size = static_cast(cs1.spells.size()); for (auto it : cs1.spells) { - iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0); - iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); + iw.components.emplace_back(ComponentType::SPELL, it); + iw.text.appendName(it); switch (size--) { case 2: @@ -1636,7 +1651,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) { - auto h1 = getHero(hero1), h2 = getHero(hero2); + auto h1 = getHero(hero1); + auto h2 = getHero(hero2); if (getPlayerRelations(h1->getOwner(), h2->getOwner()) != PlayerRelations::ENEMIES) { @@ -1656,13 +1672,8 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) void CGameHandler::sendToAllClients(CPackForClient * pack) { logNetwork->trace("\tSending to all clients: %s", typeid(*pack).name()); - for (auto c : lobby->connections) - { - if(!c->isOpen()) - continue; - + for (auto c : lobby->activeConnections) c->sendPack(pack); - } } void CGameHandler::sendAndApply(CPackForClient * pack) @@ -1777,7 +1788,8 @@ bool CGameHandler::load(const std::string & filename) try { { - CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME)), ESerializationVersion::MINIMAL); + lf.serializer.cb = this; loadCommonState(lf); logGlobal->info("Loading server state"); lf >> *this; @@ -1801,12 +1813,23 @@ bool CGameHandler::load(const std::string & filename) lobby->announceMessage(errorMsg); return false; } + catch(const IdentifierResolutionException & e) + { + logGlobal->error("Failed to load game: %s", e.what()); + MetaString errorMsg; + errorMsg.appendTextID("vcmi.server.errors.unknownEntity"); + errorMsg.replaceRawString(e.identifierName); + lobby->announceMessage(errorMsg.toString());//FIXME: should be localized on client side + return false; + } + catch(const std::exception & e) { logGlobal->error("Failed to load game: %s", e.what()); + lobby->announceMessage(std::string("Failed to load game: ") + e.what()); return false; } - gs->preInit(VLC); + gs->preInit(VLC, this); gs->updateOnLoad(lobby->si.get()); return true; } @@ -1963,7 +1986,9 @@ bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destA { const bool needsLastStack = armySrc->needsLastStack(); const auto quantity = setSrc.getStackCount(srcSlot) - (needsLastStack ? 1 : 0); - moves.insert(std::make_pair(srcSlot, std::make_pair(slotToMove, quantity))); + + if(quantity > 0) //0 may happen when we need last creature and we have exactly 1 amount of that creature - amount of "rest we can transfer" becomes 0 + moves.insert(std::make_pair(srcSlot, std::make_pair(slotToMove, quantity))); } } BulkRebalanceStacks bulkRS; @@ -2070,7 +2095,8 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 const CArmedInstance * s2 = static_cast(getObjInstance(id2)); const CCreatureSet & S1 = *s1; const CCreatureSet & S2 = *s2; - StackLocation sl1(s1, p1), sl2(s2, p2); + StackLocation sl1(s1, p1); + StackLocation sl2(s2, p2); if (s1 == nullptr || s2 == nullptr) { @@ -2213,12 +2239,12 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 bool CGameHandler::hasPlayerAt(PlayerColor player, std::shared_ptr c) const { - return connections.at(player).count(c); + return connections.count(player) && connections.at(player).count(c); } bool CGameHandler::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const { - return connections.at(left) == connections.at(right); + return connections.count(left) && connections.count(right) && connections.at(left) == connections.at(right); } bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos) @@ -2270,7 +2296,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, if(!t->visitingHero || !t->visitingHero->hasArt(ArtifactID::GRAIL)) COMPLAIN_RET("Cannot build this without grail!") else - removeArtifact(ArtifactLocation(t->visitingHero, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); + removeArtifact(ArtifactLocation(t->visitingHero->id, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); } break; } @@ -2292,7 +2318,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, return; } - CCreature * crea = VLC->creh->objects.at(t->town->creatures.at(level).at(upgradeNumber)); + const CCreature * crea = t->town->creatures.at(level).at(upgradeNumber).toCreature(); SetAvailableCreatures ssi; ssi.tid = t->id; @@ -2315,7 +2341,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, auto isLibrary = isMageGuild ? false : t->town->buildings.at(buildingID)->subId == BuildingSubID::EBuildingSubID::LIBRARY; - if(isMageGuild || isLibrary || (t->subID == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) + if(isMageGuild || isLibrary || (t->getFaction() == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) { if(t->visitingHero) giveSpells(t,t->visitingHero); @@ -2426,7 +2452,7 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst const CGTownInstance * town = dynamic_cast(getObj(objid)); const CArmedInstance * army = dynamic_cast(getObj(dstid)); const CGHeroInstance * hero = dynamic_cast(getObj(dstid)); - const CCreature * c = VLC->creh->objects.at(crid); + const CCreature * c = crid.toCreature(); const bool warMachine = c->warMachine != ArtifactID::NONE; @@ -2533,7 +2559,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI giveResources(player, -totalCost); //upgrade creature - changeStackType(StackLocation(obj, pos), VLC->creh->objects.at(upgID)); + changeStackType(StackLocation(obj, pos), upgID.toCreature()); return true; } @@ -2664,102 +2690,92 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) // With the amount of changes done to the function, it's more like transferArtifacts. // Function moves artifact from src to dst. If dst is not a backpack and is already occupied, old dst art goes to backpack and is replaced. -bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) +bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) { - ArtifactLocation src = al1, dst = al2; - const PlayerColor srcPlayer = src.owningPlayer(), dstPlayer = dst.owningPlayer(); - const CArmedInstance *srcObj = src.relatedObj(), *dstObj = dst.relatedObj(); + const auto srcArtSet = getArtSet(src); + const auto dstArtSet = getArtSet(dst); + assert(srcArtSet); + assert(dstArtSet); // Make sure exchange is even possible between the two heroes. - if(!isAllowedExchange(srcObj->id, dstObj->id)) + if(!isAllowedExchange(src.artHolder, dst.artHolder)) COMPLAIN_RET("That heroes cannot make any exchange!"); - const CArtifactInstance *srcArtifact = src.getArt(); - const CArtifactInstance *destArtifact = dst.getArt(); - const bool isDstSlotBackpack = ArtifactUtils::isSlotBackpack(dst.slot); + const auto srcArtifact = srcArtSet->getArt(src.slot); + const bool isDstSlotOccupied = dstArtSet->bearerType() == ArtBearer::ALTAR ? false : dstArtSet->getArt(dst.slot) != nullptr; + const bool isDstSlotBackpack = dstArtSet->bearerType() == ArtBearer::HERO ? ArtifactUtils::isSlotBackpack(dst.slot) : false; if(srcArtifact == nullptr) COMPLAIN_RET("No artifact to move!"); - if(destArtifact && srcPlayer != dstPlayer && !isDstSlotBackpack) + if(isDstSlotOccupied && getOwner(src.artHolder) != getOwner(dst.artHolder) && !isDstSlotBackpack) COMPLAIN_RET("Can't touch artifact on hero of another player!"); // Check if src/dest slots are appropriate for the artifacts exchanged. // Moving to the backpack is always allowed. - if((!srcArtifact || !isDstSlotBackpack) - && srcArtifact && !srcArtifact->canBePutAt(dst, true)) + if((!srcArtifact || !isDstSlotBackpack) && !srcArtifact->canBePutAt(dstArtSet, dst.slot, true)) COMPLAIN_RET("Cannot move artifact!"); - auto srcSlot = src.getSlot(); - auto dstSlot = dst.getSlot(); + auto srcSlotInfo = srcArtSet->getSlot(src.slot); + auto dstSlotInfo = dstArtSet->getSlot(dst.slot); - if((srcSlot && srcSlot->locked) || (dstSlot && dstSlot->locked)) + if((srcSlotInfo && srcSlotInfo->locked) || (dstSlotInfo && dstSlotInfo->locked)) COMPLAIN_RET("Cannot move artifact locks."); if(isDstSlotBackpack && srcArtifact->artType->isBig()) COMPLAIN_RET("Cannot put big artifacts in backpack!"); if(src.slot == ArtifactPosition::MACH4 || dst.slot == ArtifactPosition::MACH4) COMPLAIN_RET("Cannot move catapult!"); + if(isDstSlotBackpack && !ArtifactUtils::isBackpackFreeSlots(dstArtSet)) + COMPLAIN_RET("Backpack is full!"); - if(isDstSlotBackpack) + auto dstSlot = std::min(dst.slot, ArtifactPosition(ArtifactPosition::BACKPACK_START + dstArtSet->artifactsInBackpack.size())); + + if(src.slot == dstSlot && src.artHolder == dst.artHolder) + COMPLAIN_RET("Won't move artifact: Dest same as source!"); + + BulkMoveArtifacts ma(src.artHolder, dst.artHolder, false); + ma.srcCreature = src.creature; + ma.dstCreature = dst.creature; + + // Check if dst slot is occupied + if(!isDstSlotBackpack && isDstSlotOccupied) { - if(!ArtifactUtils::isBackpackFreeSlots(dst.getHolderArtSet())) - COMPLAIN_RET("Backpack is full!"); - vstd::amin(dst.slot, ArtifactPosition::BACKPACK_START + dst.getHolderArtSet()->artifactsInBackpack.size()); + // Previous artifact must be removed + ma.artsPack1.push_back(BulkMoveArtifacts::LinkedSlots(dstSlot, src.slot)); + ma.swap = true; } - if(!(src.slot == ArtifactPosition::TRANSITION_POS && dst.slot == ArtifactPosition::TRANSITION_POS)) - { - if(src.slot == dst.slot && src.artHolder == dst.artHolder) - COMPLAIN_RET("Won't move artifact: Dest same as source!"); + auto hero = getHero(dst.artHolder); + if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstSlot)) + giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); - // Check if dst slot is occupied - if(!isDstSlotBackpack && destArtifact) - { - // Previous artifact must be removed first - moveArtifact(dst, ArtifactLocation(dst.artHolder, ArtifactPosition::TRANSITION_POS)); - } - - try - { - auto hero = std::get>(dst.artHolder); - if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dst.slot)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); - } - catch(const std::bad_variant_access &) - { - // object other than hero received an art - ignore - } - - MoveArtifact ma(&src, &dst); - if(src.artHolder == dst.artHolder) - ma.askAssemble = false; - sendAndApply(&ma); - } + ma.artsPack0.push_back(BulkMoveArtifacts::LinkedSlots(src.slot, dstSlot)); + if(src.artHolder != dst.artHolder) + ma.askAssemble = true; + sendAndApply(&ma); return true; } -bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) +bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcId, ObjectInstanceID dstId, bool swap, bool equipped, bool backpack) { // Make sure exchange is even possible between the two heroes. - if(!isAllowedExchange(srcHero, dstHero)) + if(!isAllowedExchange(srcId, dstId)) COMPLAIN_RET("That heroes cannot make any exchange!"); - auto psrcHero = getHero(srcHero); - auto pdstHero = getHero(dstHero); - if((!psrcHero) || (!pdstHero)) + auto psrcSet = getArtSet(srcId); + auto pdstSet = getArtSet(dstId); + if((!psrcSet) || (!pdstSet)) COMPLAIN_RET("bulkMoveArtifacts: wrong hero's ID"); - BulkMoveArtifacts ma(static_cast>(psrcHero), - static_cast>(pdstHero), swap); + BulkMoveArtifacts ma(srcId, dstId, swap); auto & slotsSrcDst = ma.artsPack0; auto & slotsDstSrc = ma.artsPack1; // Temporary fitting set for artifacts. Used to select available slots before sending data. - CArtifactFittingSet artFittingSet(pdstHero->bearerType()); + CArtifactFittingSet artFittingSet(pdstSet->bearerType()); - auto moveArtifact = [this, &artFittingSet](const CArtifactInstance * artifact, - ArtifactPosition srcSlot, const CGHeroInstance * dstHero, - std::vector & slots) -> void + auto moveArtifact = [this, &artFittingSet, dstId](const CArtifactInstance * artifact, + ArtifactPosition srcSlot, std::vector & slots) -> void { assert(artifact); auto dstSlot = ArtifactUtils::getArtAnyPosition(&artFittingSet, artifact->getTypeId()); @@ -2768,20 +2784,23 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID artFittingSet.putArtifact(dstSlot, static_cast>(artifact)); slots.push_back(BulkMoveArtifacts::LinkedSlots(srcSlot, dstSlot)); - if(ArtifactUtils::checkSpellbookIsNeeded(dstHero, artifact->getTypeId(), dstSlot)) - giveHeroNewArtifact(dstHero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); + // TODO Shouldn't be here. Possibly in callback after equipping the artifact + if(auto dstHero = getHero(dstId)) + { + if(ArtifactUtils::checkSpellbookIsNeeded(dstHero, artifact->getTypeId(), dstSlot)) + giveHeroNewArtifact(dstHero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); + } } }; if(swap) { - auto moveArtsWorn = [moveArtifact](const CGHeroInstance * srcHero, const CGHeroInstance * dstHero, - std::vector & slots) -> void + auto moveArtsWorn = [moveArtifact](const CArtifactSet * srcArtSet, std::vector & slots) { - for(auto & artifact : srcHero->artifactsWorn) + for(auto & artifact : srcArtSet->artifactsWorn) { if(ArtifactUtils::isArtRemovable(artifact)) - moveArtifact(artifact.second.getArt(), artifact.first, dstHero, slots); + moveArtifact(artifact.second.getArt(), artifact.first, slots); } }; auto moveArtsInBackpack = [](const CArtifactSet * artSet, @@ -2796,41 +2815,41 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID if(equipped) { // Move over artifacts that are worn srcHero -> dstHero - moveArtsWorn(psrcHero, pdstHero, slotsSrcDst); + moveArtsWorn(psrcSet, slotsSrcDst); artFittingSet.artifactsWorn.clear(); // Move over artifacts that are worn dstHero -> srcHero - moveArtsWorn(pdstHero, psrcHero, slotsDstSrc); + moveArtsWorn(pdstSet, slotsDstSrc); } if(backpack) { // Move over artifacts that are in backpack srcHero -> dstHero - moveArtsInBackpack(psrcHero, slotsSrcDst); + moveArtsInBackpack(psrcSet, slotsSrcDst); // Move over artifacts that are in backpack dstHero -> srcHero - moveArtsInBackpack(pdstHero, slotsDstSrc); + moveArtsInBackpack(pdstSet, slotsDstSrc); } } else { - artFittingSet.artifactsInBackpack = pdstHero->artifactsInBackpack; - artFittingSet.artifactsWorn = pdstHero->artifactsWorn; + artFittingSet.artifactsInBackpack = pdstSet->artifactsInBackpack; + artFittingSet.artifactsWorn = pdstSet->artifactsWorn; if(equipped) { // Move over artifacts that are worn - for(auto & artInfo : psrcHero->artifactsWorn) + for(auto & artInfo : psrcSet->artifactsWorn) { if(ArtifactUtils::isArtRemovable(artInfo)) { - moveArtifact(psrcHero->getArt(artInfo.first), artInfo.first, pdstHero, slotsSrcDst); + moveArtifact(psrcSet->getArt(artInfo.first), artInfo.first, slotsSrcDst); } } } if(backpack) { // Move over artifacts that are in backpack - for(auto & slotInfo : psrcHero->artifactsInBackpack) + for(auto & slotInfo : psrcSet->artifactsInBackpack) { - moveArtifact(psrcHero->getArt(psrcHero->getArtPos(slotInfo.artifact)), - psrcHero->getArtPos(slotInfo.artifact), pdstHero, slotsSrcDst); + moveArtifact(psrcSet->getArt(psrcSet->getArtPos(slotInfo.artifact)), + psrcSet->getArtPos(slotInfo.artifact), slotsSrcDst); } } } @@ -2854,24 +2873,24 @@ bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition a if(!destArtifact) COMPLAIN_RET("assembleArtifacts: there is no such artifact instance!"); - const auto dstLoc = ArtifactLocation(hero, artifactSlot); + const auto dstLoc = ArtifactLocation(hero->id, artifactSlot); if(assemble) { - CArtifact * combinedArt = VLC->arth->objects[assembleTo]; + const CArtifact * combinedArt = assembleTo.toArtifact(); if(!combinedArt->isCombined()) COMPLAIN_RET("assembleArtifacts: Artifact being attempted to assemble is not a combined artifacts!"); if(!vstd::contains(ArtifactUtils::assemblyPossibilities(hero, destArtifact->getTypeId()), combinedArt)) { COMPLAIN_RET("assembleArtifacts: It's impossible to assemble requested artifact!"); } - if(!destArtifact->canBePutAt(dstLoc) - && !destArtifact->canBePutAt(ArtifactLocation(hero, ArtifactPosition::BACKPACK_START))) + if(!destArtifact->canBePutAt(hero, artifactSlot, true) + && !destArtifact->canBePutAt(hero, ArtifactPosition::BACKPACK_START, true)) { COMPLAIN_RET("assembleArtifacts: It's impossible to give the artholder requested artifact!"); } if(ArtifactUtils::checkSpellbookIsNeeded(hero, assembleTo, artifactSlot)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); + giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); AssembledArtifact aa; aa.al = dstLoc; @@ -2897,15 +2916,15 @@ bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition a bool CGameHandler::eraseArtifactByClient(const ArtifactLocation & al) { - const auto * hero = getHero(al.relatedObj()->id); + const auto * hero = getHero(al.artHolder); if(hero == nullptr) COMPLAIN_RET("eraseArtifactByClient: wrong hero's ID"); - const auto * art = al.getArt(); + const auto * art = hero->getArt(al.slot); if(art == nullptr) COMPLAIN_RET("Cannot remove artifact!"); - if(al.getArt()->artType->canBePutAt(hero) || al.slot != ArtifactPosition::TRANSITION_POS) + if(art->canBePutAt(hero) || al.slot != ArtifactPosition::TRANSITION_POS) COMPLAIN_RET("Illegal artifact removal request"); removeArtifact(al); @@ -2928,7 +2947,7 @@ bool CGameHandler::buyArtifact(ObjectInstanceID hid, ArtifactID aid) return false; giveResource(hero->getOwner(),EGameResID::GOLD,-GameConstants::SPELLBOOK_GOLD_COST); - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); + giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); assert(hero->getArt(ArtifactPosition::SPELLBOOK)); giveSpells(town,hero); return true; @@ -2961,7 +2980,8 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe if (!vstd::contains(m->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT), aid)) COMPLAIN_RET("That artifact is unavailable!"); - int b1, b2; + int b1; + int b2; m->getOffer(rid, aid, b1, b2, EMarketMode::RESOURCE_ARTIFACT); if (getResource(h->tempOwner, rid) < b1) @@ -2972,12 +2992,12 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe SetAvailableArtifacts saa; if(dynamic_cast(m)) { - saa.id = -1; - saa.arts = CGTownInstance::merchantArtifacts; + saa.id = ObjectInstanceID::NONE; + saa.arts = gs->map->townMerchantArtifacts; } else if(const CGBlackMarket *bm = dynamic_cast(m)) //black market { - saa.id = bm->id.getNum(); + saa.id = bm->id; saa.arts = bm->artifacts; } else @@ -2998,7 +3018,7 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe COMPLAIN_RET("Cannot find selected artifact on the list"); sendAndApply(&saa); - giveHeroNewArtifact(h, VLC->arth->objects[aid], ArtifactPosition::FIRST_AVAILABLE); + giveHeroNewArtifact(h, aid.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); return true; } @@ -3009,10 +3029,11 @@ bool CGameHandler::sellArtifact(const IMarket *m, const CGHeroInstance *h, Artif COMPLAIN_RET_FALSE_IF((!art), "There is no artifact to sell!"); COMPLAIN_RET_FALSE_IF((!art->artType->isTradable()), "Cannot sell a war machine or spellbook!"); - int resVal = 0, dump = 1; + int resVal = 0; + int dump = 1; m->getOffer(art->artType->getId(), rid, dump, resVal, EMarketMode::ARTIFACT_RESOURCE); - removeArtifact(ArtifactLocation(h, h->getArtPos(art))); + removeArtifact(ArtifactLocation(h->id, h->getArtPos(art))); giveResource(h->tempOwner, rid, resVal); return true; } @@ -3043,23 +3064,24 @@ bool CGameHandler::buySecSkill(const IMarket *m, const CGHeroInstance *h, Second return true; } -bool CGameHandler::tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2) +bool CGameHandler::tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy) { - TResourceCap r1 = getPlayerState(player)->resources[id1]; + TResourceCap haveToSell = getPlayerState(player)->resources[toSell]; - vstd::amin(val, r1); //can't trade more resources than have + vstd::amin(amountToSell, haveToSell); //can't trade more resources than have - int b1, b2; //base quantities for trade - market->getOffer(id1, id2, b1, b2, EMarketMode::RESOURCE_RESOURCE); - int units = val / b1; //how many base quantities we trade + int b1; //base quantities for trade + int b2; + market->getOffer(toSell, toBuy, b1, b2, EMarketMode::RESOURCE_RESOURCE); + int amountToBoy = amountToSell / b1; //how many base quantities we trade - if (val%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error + if (amountToSell % b1 != 0) //all offered units of resource should be used, if not -> somewhere in calculations must be an error { COMPLAIN_RET("Invalid deal, not all offered units of resource were used."); } - giveResource(player, GameResID(id1), - b1 * units); - giveResource(player, GameResID(id2), b2 * units); + giveResource(player, toSell, -b1 * amountToBoy); + giveResource(player, toBuy, b2 * amountToBoy); return true; } @@ -3079,7 +3101,8 @@ bool CGameHandler::sellCreatures(ui32 count, const IMarket *market, const CGHero COMPLAIN_RET("Not enough creatures in army!"); } - int b1, b2; //base quantities for trade + int b1; //base quantities for trade + int b2; market->getOffer(s.type->getId(), resourceID, b1, b2, EMarketMode::CREATURE_RESOURCE); int units = count / b1; //how many base quantities we trade @@ -3143,7 +3166,7 @@ bool CGameHandler::sendResources(ui32 val, PlayerColor player, GameResID r1, Pla return true; } -bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation) +bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) { const CGHeroInstance *h = getHero(hid); if (!h) @@ -3162,8 +3185,6 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation) bool CGameHandler::queryReply(QueryID qid, std::optional answer, PlayerColor player) { - boost::unique_lock lock(gsm); - logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); if (answer) logGlobal->trace("%d", *answer); @@ -3216,10 +3237,10 @@ void CGameHandler::handleTimeEvents() iw.player = color; iw.text = ev.message; - for (int i=0; iresources[i] != n.res.at(player)[i]) //if resource had changed, we add it to the dialog - iw.components.emplace_back(Component::EComponentType::RESOURCE,i,n.res.at(player)[i]-was[i],0); - + iw.components.emplace_back(ComponentType::RESOURCE, i, n.res.at(player)[i] - was[i]); } for (auto & i : ev.buildings) { - if (!town->hasBuilt(i)) + // Only perform action if: + // 1. Building exists in town (don't attempt to build Lvl 5 guild in Fortress + // 2. Building was not built yet + // othervice, silently ignore / skip it + if (town->town->buildings.count(i) && !town->hasBuilt(i)) { buildStructure(town->id, i, true); - iw.components.emplace_back(Component::EComponentType::BUILDING, town->subID, i, 0); + iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i)); } } @@ -3300,8 +3324,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) if (!town->creatures.at(i).second.empty() && ev.creatures.at(i) > 0)//there is dwelling { sac.creatures[i].first += ev.creatures.at(i); - iw.components.emplace_back(Component::EComponentType::CREATURE, - town->creatures.at(i).second.back(), ev.creatures.at(i), 0); + iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), ev.creatures.at(i)); } } sendAndApply(&iw); //show dialog @@ -3378,7 +3401,8 @@ bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) if (id1 == id2) return true; - const CGObjectInstance *o1 = getObj(id1), *o2 = getObj(id2); + const CGObjectInstance *o1 = getObj(id1); + const CGObjectInstance *o2 = getObj(id2); if (!o1 || !o2) return true; //arranging stacks within an object should be always allowed @@ -3397,6 +3421,12 @@ bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) return true; } + auto market = dynamic_cast(o1); + if(market == nullptr) + market = dynamic_cast(o2); + if(market) + return market->allowsTrade(EMarketMode::ARTIFACT_EXP); + if (o1->ID == Obj::HERO && o2->ID == Obj::HERO) { const CGHeroInstance *h1 = static_cast(o1); @@ -3430,7 +3460,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta { using events::ObjectVisitStarted; - logGlobal->debug("%s visits %s (%d:%d)", h->nodeName(), obj->getObjectName(), obj->ID, obj->subID); + logGlobal->debug("%s visits %s (%d)", h->nodeName(), obj->getObjectName(), obj->ID); if (getVisitingHero(obj) != nullptr) { @@ -3498,7 +3528,7 @@ void CGameHandler::objectVisitEnded(const CObjectVisitQuery & query) bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) { - const IShipyard *obj = IShipyard::castFrom(getObj(objid)); + const auto *obj = dynamic_cast(getObj(objid)); if (obj->shipyardStatus() != IBoatGenerator::GOOD) { @@ -3590,7 +3620,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) if(p->human) { - lobby->setState(EServerState::GAMEPLAY_ENDED); + lobby->setState(EServerState::SHUTDOWN); } } else @@ -3640,8 +3670,8 @@ void CGameHandler::getVictoryLossMessage(PlayerColor player, const EVictoryLossC { out.player = player; out.text = victoryLossCheckResult.messageToSelf; - out.text.replaceLocalString(EMetaText::COLOR, player.getNum()); - out.components.emplace_back(Component::EComponentType::FLAG, player.getNum(), 0, 0); + out.text.replaceName(player); + out.components.emplace_back(ComponentType::FLAG, player); } bool CGameHandler::dig(const CGHeroInstance *h) @@ -3662,16 +3692,18 @@ bool CGameHandler::dig(const CGHeroInstance *h) iw.player = h->tempOwner; if (gs->map->grailPos == h->visitablePos()) { - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 58); //"Congratulations! After spending many hours digging here, your hero has uncovered the " - iw.text.appendLocalString(EMetaText::ART_NAMES, ArtifactID::GRAIL); + ArtifactID grail = ArtifactID::GRAIL; + + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 58); //"Congratulations! After spending many hours digging here, your hero has uncovered the " ... + iw.text.appendName(grail); // ... " The Grail" iw.soundID = soundBase::ULTIMATEARTIFACT; - giveHeroNewArtifact(h, VLC->arth->objects[ArtifactID::GRAIL], ArtifactPosition::FIRST_AVAILABLE); //give grail + giveHeroNewArtifact(h, grail.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); //give grail sendAndApply(&iw); iw.soundID = soundBase::invalid; - iw.components.emplace_back(Component::EComponentType::ARTIFACT, ArtifactID::GRAIL, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, grail); iw.text.clear(); - iw.text.appendLocalString(EMetaText::ART_DESCR, ArtifactID::GRAIL); + iw.text.appendTextID(grail.toArtifact()->getDescriptionTextID()); sendAndApply(&iw); } else @@ -3704,7 +3736,7 @@ bool CGameHandler::sacrificeCreatures(const IMarket * market, const CGHeroInstan int expSum = 0; auto finish = [this, &hero, &expSum]() { - changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(expSum)); + giveExperience(hero, hero->calculateXp(expSum)); }; for(int i = 0; i < slot.size(); ++i) @@ -3726,7 +3758,8 @@ bool CGameHandler::sacrificeCreatures(const IMarket * market, const CGHeroInstan changeStackCount(StackLocation(hero, slot[i]), -(TQuantity)count[i]); - int dump, exp; + int dump; + int exp; market->getOffer(crid, 0, dump, exp, EMarketMode::CREATURE_EXP); exp *= count[i]; expSum += exp; @@ -3737,44 +3770,44 @@ bool CGameHandler::sacrificeCreatures(const IMarket * market, const CGHeroInstan return true; } -bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot) +bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & arts) { if (!hero) COMPLAIN_RET("You need hero to sacrifice artifact!"); + if(hero->getAlignment() == EAlignment::EVIL) + COMPLAIN_RET("Evil hero can't sacrifice artifact!"); + + assert(m); + auto altarObj = dynamic_cast(m); int expSum = 0; auto finish = [this, &hero, &expSum]() { - changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(expSum)); + giveExperience(hero, hero->calculateXp(expSum)); }; - for(int i = 0; i < slot.size(); ++i) + for(const auto & artInstId : arts) { - ArtifactLocation al(hero, slot[i]); - const CArtifactInstance * a = al.getArt(); - - if(!a) + if(auto art = altarObj->getArtByInstanceId(artInstId)) + { + if(art->artType->isTradable()) + { + int dmp; + int expToGive; + m->getOffer(art->getTypeId(), 0, dmp, expToGive, EMarketMode::ARTIFACT_EXP); + expSum += expToGive; + removeArtifact(ArtifactLocation(altarObj->id, altarObj->getSlotByInstance(art))); + } + else + { + COMPLAIN_RET("Cannot sacrifice not tradable artifact!"); + } + } + else { finish(); COMPLAIN_RET("Cannot find artifact to sacrifice!"); } - - const CArtifactInstance * art = hero->getArt(slot[i]); - - if(!art) - { - finish(); - COMPLAIN_RET("No artifact at position to sacrifice!"); - } - - si32 typId = art->artType->getId(); - int dmp, expToGive; - - m->getOffer(typId, 0, dmp, expToGive, EMarketMode::ARTIFACT_EXP); - - expSum += expToGive; - - removeArtifact(al); } finish(); @@ -3927,14 +3960,14 @@ bool CGameHandler::moveStack(const StackLocation &src, const StackLocation &dst, void CGameHandler::castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) { - const CSpell * s = spellID.toSpell(); - if(!s) + if (!spellID.hasValue()) return; AdventureSpellCastParameters p; p.caster = caster; p.pos = pos; + const CSpell * s = spellID.toSpell(); s->adventureCast(spellEnv, p); } @@ -3960,38 +3993,46 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s } } -bool CGameHandler::giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) +bool CGameHandler::putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) { - assert(a->artType); - ArtifactLocation al(h, ArtifactPosition::PRE_FIRST); + assert(art && art->artType); + ArtifactLocation dst(al.artHolder, ArtifactPosition::PRE_FIRST); + dst.creature = al.creature; + auto putTo = getArtSet(al); + assert(putTo); - if(pos == ArtifactPosition::FIRST_AVAILABLE) + if(al.slot == ArtifactPosition::FIRST_AVAILABLE) { - al.slot = ArtifactUtils::getArtAnyPosition(h, a->getTypeId()); + dst.slot = ArtifactUtils::getArtAnyPosition(putTo, art->getTypeId()); } - else if(ArtifactUtils::isSlotBackpack(pos)) + else if(ArtifactUtils::isSlotBackpack(al.slot) && !al.creature.has_value()) { - al.slot = ArtifactUtils::getArtBackpackPosition(h, a->getTypeId()); + dst.slot = ArtifactUtils::getArtBackpackPosition(putTo, art->getTypeId()); } else { - al.slot = pos; + dst.slot = al.slot; } - if(a->canBePutAt(al)) - putArtifact(al, a); + if(!askAssemble.has_value()) + { + if(!dst.creature.has_value() && ArtifactUtils::isSlotEquipment(dst.slot)) + askAssemble = true; + else + askAssemble = false; + } + + if(art->canBePutAt(putTo, dst.slot)) + { + PutArtifact pa(dst, askAssemble.value()); + pa.art = art; + sendAndApply(&pa); + return true; + } else + { return false; - - return true; -} - -void CGameHandler::putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) -{ - PutArtifact pa; - pa.art = a; - pa.al = al; - sendAndApply(&pa); + } } bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) @@ -4020,7 +4061,7 @@ bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact na.art = newArtInst; sendAndApply(&na); // -> updates newArtInst!!! - if(giveHeroArtifact(h, newArtInst, pos)) + if(putArtifact(ArtifactLocation(h->id, pos), newArtInst, false)) return true; else return false; @@ -4035,7 +4076,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) RandomGeneratorUtil::randomShuffle(tiles, getRandomGenerator()); logGlobal->trace("Spawning wandering monsters. Found %d free tiles. Creature type: %d", tiles.size(), creatureID.num); - const CCreature *cre = VLC->creh->objects.at(creatureID); + const CCreature *cre = creatureID.toCreature(); for (int i = 0; i < (int)amount; ++i) { tile = tiles.begin(); @@ -4046,8 +4087,8 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) createObject(*tile, PlayerColor::NEUTRAL, Obj::MONSTER, creatureID); auto monsterId = getTopObj(*tile)->id; - setObjProperty(monsterId, ObjProperty::MONSTER_COUNT, count); - setObjProperty(monsterId, ObjProperty::MONSTER_POWER, (si64)1000*count); + setObjPropertyValue(monsterId, ObjProperty::MONSTER_COUNT, count); + setObjPropertyValue(monsterId, ObjProperty::MONSTER_POWER, (si64)1000*count); } tiles.erase(tile); //not use it again } @@ -4056,7 +4097,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; - uahl.allocatedArtifacts = VLC->arth->allocatedArtifacts; + uahl.allocatedArtifacts = gs->allocatedArtifacts; sendAndApply(&uahl); } @@ -4144,7 +4185,7 @@ const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj { assert(obj); - for (auto const & query : queries->allQueries()) + for(const auto & query : queries->allQueries()) { auto visit = std::dynamic_pointer_cast(query); if (visit && visit->visitedObject == obj) @@ -4153,6 +4194,19 @@ const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj return nullptr; } +const CGObjectInstance * CGameHandler::getVisitingObject(const CGHeroInstance *hero) +{ + assert(hero); + + for(const auto & query : queries->allQueries()) + { + auto visit = std::dynamic_pointer_cast(query); + if (visit && visit->visitingHero == hero) + return visit->visitedObject; + } + return nullptr; +} + bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) { assert(obj); @@ -4169,12 +4223,21 @@ bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, con return true; } -void CGameHandler::setObjProperty(ObjectInstanceID objid, int prop, si64 val) +void CGameHandler::setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) { SetObjectProperty sob; sob.id = objid; sob.what = prop; - sob.val = static_cast(val); + sob.identifier = NumericID(value); + sendAndApply(&sob); +} + +void CGameHandler::setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) +{ + SetObjectProperty sob; + sob.id = objid; + sob.what = prop; + sob.identifier = identifier; sendAndApply(&sob); } @@ -4208,7 +4271,7 @@ scripting::Pool * CGameHandler::getGlobalContextPool() const //} #endif -void CGameHandler::createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype) +void CGameHandler::createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) { NewObject no; no.ID = type; diff --git a/server/CGameHandler.h b/server/CGameHandler.h index b6996aa85..dc6bdd3e1 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -73,7 +73,6 @@ public: std::map>> connections; //player color -> connection to client with interface of that player //queries stuff - boost::recursive_mutex gsm; ui32 QID; SpellCastEnvironment * spellEnv; @@ -92,8 +91,7 @@ public: bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); - CGameHandler(); - CGameHandler(CVCMIServer * lobby); + explicit CGameHandler(CVCMIServer * lobby); ~CGameHandler(); ////////////////////////////////////////////////////////////////////////// @@ -101,8 +99,9 @@ public: //do sth void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override; bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; + void giveExperience(const CGHeroInstance * hero, TExpType val) override; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; @@ -127,11 +126,10 @@ public: void removeAfterVisit(const CGObjectInstance *object) override; bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos = ArtifactPosition::FIRST_AVAILABLE) override; - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override; - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override; + bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override; void removeArtifact(const ArtifactLocation &al) override; - bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override; - bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack); + bool moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) override; + bool bulkMoveArtifacts(ObjectInstanceID srcId, ObjectInstanceID dstId, bool swap, bool equipped, bool backpack); bool eraseArtifactByClient(const ArtifactLocation & al); void synchronizeArtifactHandlerLists(); @@ -143,6 +141,7 @@ public: bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override; void giveHeroBonus(GiveBonus * bonus) override; void setMovePoints(SetMovePoints * smp) override; + void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override; void setManaPoints(ObjectInstanceID hid, int val) override; void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override; void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override; @@ -155,8 +154,10 @@ public: /// Returns hero that is currently visiting this object, or nullptr if no visit is active const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj); + const CGObjectInstance * getVisitingObject(const CGHeroInstance *hero); bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; + void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override; + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override; void showInfoDialog(InfoWindow * iw) override; void showInfoDialog(const std::string & msg, PlayerColor player) override; @@ -182,8 +183,8 @@ public: bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); - bool setFormation( ObjectInstanceID hid, ui8 formation ); - bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2); + bool setFormation( ObjectInstanceID hid, EArmyFormation formation ); + bool tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy); bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); bool sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, GameResID resourceID); @@ -221,7 +222,7 @@ public: bool dig(const CGHeroInstance *h); void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & QID; h & getRandomGenerator(); @@ -248,7 +249,7 @@ public: void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer); /// Unconditionally throws with "Action not allowed" message - void throwNotAllowedAction(CPackForServer * pack); + [[noreturn]] void throwNotAllowedAction(CPackForServer * pack); /// Throws if player stated in pack is not making turn right now void throwIfPlayerNotActive(CPackForServer * pack); /// Throws if object is not owned by pack sender @@ -256,12 +257,13 @@ public: /// Throws if player is not present on connection of this pack void throwIfWrongPlayer(CPackForServer * pack, PlayerColor player); void throwIfWrongPlayer(CPackForServer * pack); - void throwAndComplain(CPackForServer * pack, std::string txt); + [[noreturn]] void throwAndComplain(CPackForServer * pack, std::string txt); bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); - void run(bool resume); - bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); + void start(bool resume); + void tick(int millisecondsPassed); + bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & arts); void spawnWanderingMonsters(CreatureID creatureID); // Check for victory and loss conditions diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 0263fef96..6f7093ea4 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,4 +1,4 @@ -set(server_SRCS +set(vcmiservercommon_SRCS StdInc.cpp battles/BattleActionProcessor.cpp @@ -16,6 +16,7 @@ set(server_SRCS processors/TurnOrderProcessor.cpp CGameHandler.cpp + GlobalLobbyProcessor.cpp ServerSpellCastEnvironment.cpp CVCMIServer.cpp NetPacksServer.cpp @@ -23,7 +24,7 @@ set(server_SRCS TurnTimerHandler.cpp ) -set(server_HEADERS +set(vcmiservercommon_HEADERS StdInc.h battles/BattleActionProcessor.h @@ -41,6 +42,7 @@ set(server_HEADERS processors/TurnOrderProcessor.h CGameHandler.h + GlobalLobbyProcessor.h ServerSpellCastEnvironment.h CVCMIServer.h LobbyNetPackVisitors.h @@ -48,45 +50,28 @@ set(server_HEADERS TurnTimerHandler.h ) -assign_source_group(${server_SRCS} ${server_HEADERS}) +assign_source_group(${vcmiservercommon_SRCS} ${vcmiservercommon_HEADERS}) -if(ENABLE_SINGLE_APP_BUILD) - add_library(vcmiserver STATIC ${server_SRCS} ${server_HEADERS}) - target_compile_definitions(vcmiserver PUBLIC VCMI_DLL_STATIC=1) - set(server_LIBS vcmi_lib_server) -else() - if(ANDROID) - add_library(vcmiserver SHARED ${server_SRCS} ${server_HEADERS}) - else() - add_executable(vcmiserver ${server_SRCS} ${server_HEADERS}) - endif() - set(server_LIBS vcmi) -endif() +add_library(vcmiservercommon STATIC ${vcmiservercommon_SRCS} ${vcmiservercommon_HEADERS}) +set(vcmiservercommon_LIBS vcmi) if(CMAKE_SYSTEM_NAME MATCHES FreeBSD OR HAIKU) - set(server_LIBS execinfo ${server_LIBS}) + set(vcmiservercommon_LIBS execinfo ${vcmiservercommon_LIBS}) endif() -target_link_libraries(vcmiserver PRIVATE ${server_LIBS} minizip::minizip) -target_include_directories(vcmiserver +target_link_libraries(vcmiservercommon PRIVATE ${vcmiservercommon_LIBS} minizip::minizip) + +target_include_directories(vcmiservercommon PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) if(WIN32) - set_target_properties(vcmiserver + set_target_properties(vcmiservercommon PROPERTIES - OUTPUT_NAME "VCMI_server" - PROJECT_LABEL "VCMI_server" + OUTPUT_NAME "VCMI_vcmiservercommon" + PROJECT_LABEL "VCMI_vcmiservercommon" ) endif() -vcmi_set_output_dir(vcmiserver "") -enable_pch(vcmiserver) - -if(NOT ENABLE_SINGLE_APP_BUILD) - if(ANDROID) - install(TARGETS vcmiserver DESTINATION ${LIB_DIR}) - else() - install(TARGETS vcmiserver DESTINATION ${BIN_DIR}) - endif() -endif() +vcmi_set_output_dir(vcmiservercommon "") +enable_pch(vcmiservercommon) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 49e303be1..588325a7f 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -8,61 +8,31 @@ * */ #include "StdInc.h" -#include - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/campaign/CampaignState.h" -#include "../lib/CThreadHelper.h" -#include "../lib/serializer/Connection.h" -#include "../lib/CArtHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CTownHandler.h" -#include "../lib/CBuildingHandler.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CCreatureHandler.h" -#include "zlib.h" #include "CVCMIServer.h" -#include "../lib/StartInfo.h" -#include "../lib/mapping/CMapHeader.h" -#include "../lib/rmg/CMapGenOptions.h" -#include "LobbyNetPackVisitors.h" -#ifdef VCMI_ANDROID -#include -#include -#include "lib/CAndroidVMHelper.h" -#endif -#include "../lib/VCMI_Lib.h" -#include "../lib/VCMIDirs.h" + #include "CGameHandler.h" +#include "GlobalLobbyProcessor.h" +#include "LobbyNetPackVisitors.h" #include "processors/PlayerMessageProcessor.h" -#include "../lib/mapping/CMapInfo.h" -#include "../lib/GameConstants.h" -#include "../lib/logging/CBasicLogConfigurator.h" -#include "../lib/CConfigHandler.h" -#include "../lib/ScopeGuard.h" + +#include "../lib/CHeroHandler.h" +#include "../lib/registerTypes/RegisterTypesLobbyPacks.h" #include "../lib/serializer/CMemorySerializer.h" -#include "../lib/serializer/Cast.h" - -#include "../lib/UnlockGuard.h" - -// for applier -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/serializer/Connection.h" // UUID generation #include #include #include - -#include "../lib/gameState/CGameState.h" +#include template class CApplyOnServer; class CBaseForServerApply { public: - virtual bool applyOnServerBefore(CVCMIServer * srv, void * pack) const =0; - virtual void applyOnServerAfter(CVCMIServer * srv, void * pack) const =0; + virtual bool applyOnServerBefore(CVCMIServer * srv, CPack * pack) const =0; + virtual void applyOnServerAfter(CVCMIServer * srv, CPack * pack) const =0; virtual ~CBaseForServerApply() {} template static CBaseForServerApply * getApplier(const U * t = nullptr) { @@ -73,7 +43,7 @@ public: template class CApplyOnServer : public CBaseForServerApply { public: - bool applyOnServerBefore(CVCMIServer * srv, void * pack) const override + bool applyOnServerBefore(CVCMIServer * srv, CPack * pack) const override { T * ptr = static_cast(pack); ClientPermissionsCheckerNetPackVisitor checker(*srv); @@ -81,18 +51,15 @@ public: if(checker.getResult()) { - boost::unique_lock stateLock(srv->stateMutex); ApplyOnServerNetPackVisitor applier(*srv); - ptr->visit(applier); - return applier.getResult(); } else return false; } - void applyOnServerAfter(CVCMIServer * srv, void * pack) const override + void applyOnServerAfter(CVCMIServer * srv, CPack * pack) const override { T * ptr = static_cast(pack); ApplyOnServerAfterAnnounceNetPackVisitor applier(*srv); @@ -104,188 +71,183 @@ template <> class CApplyOnServer : public CBaseForServerApply { public: - bool applyOnServerBefore(CVCMIServer * srv, void * pack) const override + bool applyOnServerBefore(CVCMIServer * srv, CPack * pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); return false; } - void applyOnServerAfter(CVCMIServer * srv, void * pack) const override + void applyOnServerAfter(CVCMIServer * srv, CPack * pack) const override { logGlobal->error("Cannot apply plain CPack!"); assert(0); } }; -std::string SERVER_NAME_AFFIX = "server"; -std::string SERVER_NAME = GameConstants::VCMI_VERSION + std::string(" (") + SERVER_NAME_AFFIX + ')'; +class CVCMIServerPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) +{ +private: + CVCMIServer & handler; + std::shared_ptr gh; -CVCMIServer::CVCMIServer(boost::program_options::variables_map & opts) - : port(3030), io(std::make_shared()), state(EServerState::LOBBY), cmdLineOptions(opts), currentClientId(1), currentPlayerId(1), restartGameplay(false) +public: + CVCMIServerPackVisitor(CVCMIServer & handler, std::shared_ptr gh) + :handler(handler), gh(gh) + { + } + + bool callTyped() override { return false; } + + void visitForLobby(CPackForLobby & packForLobby) override + { + handler.handleReceivedPack(std::unique_ptr(&packForLobby)); + } + + void visitForServer(CPackForServer & serverPack) override + { + if (gh) + gh->handleReceivedPack(&serverPack); + else + logNetwork->error("Received pack for game server while in lobby!"); + } + + void visitForClient(CPackForClient & clientPack) override + { + } +}; + +CVCMIServer::CVCMIServer(uint16_t port, bool connectToLobby, bool runByClient) + : currentClientId(1) + , currentPlayerId(1) + , port(port) + , runByClient(runByClient) { uuid = boost::uuids::to_string(boost::uuids::random_generator()()); logNetwork->trace("CVCMIServer created! UUID: %s", uuid); applier = std::make_shared>(); registerTypesLobbyPacks(*applier); - if(cmdLineOptions.count("port")) - port = cmdLineOptions["port"].as(); + networkHandler = INetworkHandler::createHandler(); + + if(connectToLobby) + lobbyProcessor = std::make_unique(*this); + else + startAcceptingIncomingConnections(); +} + +CVCMIServer::~CVCMIServer() = default; + +void CVCMIServer::startAcceptingIncomingConnections() +{ logNetwork->info("Port %d will be used", port); - try - { - acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)); - } - catch(...) - { - logNetwork->info("Port %d is busy, trying to use random port instead", port); - if(cmdLineOptions.count("run-by-client")) - { - logNetwork->error("Port must be specified when run-by-client is used!!"); - exit(0); - } - acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); - port = acceptor->local_endpoint().port(); - } + + networkServer = networkHandler->createServerTCP(*this); + networkServer->start(port); logNetwork->info("Listening for connections at port %d", port); } -CVCMIServer::~CVCMIServer() +void CVCMIServer::onNewConnection(const std::shared_ptr & connection) { - announceQueue.clear(); + if(getState() == EServerState::LOBBY) + { + activeConnections.push_back(std::make_shared(connection)); + activeConnections.back()->enterLobbyConnectionMode(); + } + else + { + // TODO: reconnection support + connection->close(); + } +} - if(announceLobbyThread) - announceLobbyThread->join(); +void CVCMIServer::onPacketReceived(const std::shared_ptr & connection, const std::vector & message) +{ + std::shared_ptr c = findConnection(connection); + auto pack = c->retrievePack(message); + pack->c = c; + CVCMIServerPackVisitor visitor(*this, this->gh); + pack->visit(visitor); } void CVCMIServer::setState(EServerState value) { - state.store(value); + assert(state != EServerState::SHUTDOWN); // do not attempt to restart dying server + state = value; + + if (state == EServerState::SHUTDOWN) + networkHandler->stop(); } EServerState CVCMIServer::getState() const { - return state.load(); + return state; +} + +std::shared_ptr CVCMIServer::findConnection(const std::shared_ptr & netConnection) +{ + for(const auto & gameConnection : activeConnections) + { + if (gameConnection->isMyConnection(netConnection)) + return gameConnection; + } + + throw std::runtime_error("Unknown connection received in CVCMIServer::findConnection"); +} + +bool CVCMIServer::wasStartedByClient() const +{ + return runByClient; } void CVCMIServer::run() { - if(!restartGameplay) - { - this->announceLobbyThread = std::make_unique(&CVCMIServer::threadAnnounceLobby, this); - - startAsyncAccept(); - if(!remoteConnectionsThread && cmdLineOptions.count("lobby")) - { - remoteConnectionsThread = std::make_unique(&CVCMIServer::establishRemoteConnections, this); - } - -#if defined(VCMI_ANDROID) -#ifndef SINGLE_PROCESS_APP - CAndroidVMHelper vmHelper; - vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "onServerReady"); -#endif -#endif - } - - while(state == EServerState::LOBBY || state == EServerState::GAMEPLAY_STARTING) - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); - - logNetwork->info("Thread handling connections ended"); - - if(state == EServerState::GAMEPLAY) - { - gh->run(si->mode == StartInfo::LOAD_GAME); - } - while(state == EServerState::GAMEPLAY_ENDED) - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); + networkHandler->run(); } -void CVCMIServer::establishRemoteConnections() +void CVCMIServer::onTimer() { - setThreadName("establishConnection"); + // we might receive onTimer call after transitioning from GAMEPLAY to LOBBY state, e.g. on game restart + if (getState() != EServerState::GAMEPLAY) + return; - //wait for host connection - while(connections.empty()) - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); - - uuid = cmdLineOptions["lobby-uuid"].as(); - int numOfConnections = cmdLineOptions["connections"].as(); - for(int i = 0; i < numOfConnections; ++i) - connectToRemote(); -} + static const auto serverUpdateInterval = std::chrono::milliseconds(100); -void CVCMIServer::connectToRemote() -{ - std::shared_ptr c; - try - { - auto address = cmdLineOptions["lobby"].as(); - int port = cmdLineOptions["lobby-port"].as(); - - logNetwork->info("Establishing connection to remote at %s:%d with uuid %s", address, port, uuid); - c = std::make_shared(address, port, SERVER_NAME, uuid); - } - catch(...) - { - logNetwork->error("\nCannot establish remote connection!"); - } - - if(c) - { - connections.insert(c); - remoteConnections.insert(c); - c->handler = std::make_shared(&CVCMIServer::threadHandleClient, this, c); - } -} + auto timeNow = std::chrono::steady_clock::now(); + auto timePassedBefore = lastTimerUpdateTime - gameplayStartTime; + auto timePassedNow = timeNow - gameplayStartTime; -void CVCMIServer::threadAnnounceLobby() -{ - setThreadName("announceLobby"); - while(state != EServerState::SHUTDOWN) - { - { - boost::unique_lock myLock(mx); - while(!announceQueue.empty()) - { - announcePack(std::move(announceQueue.front())); - announceQueue.pop_front(); - } + lastTimerUpdateTime = timeNow; - if(acceptor) - { - io->reset(); - io->poll(); - } - } + auto msPassedBefore = std::chrono::duration_cast(timePassedBefore); + auto msPassedNow = std::chrono::duration_cast(timePassedNow); + auto msDelta = msPassedNow - msPassedBefore; - boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); - } + if (msDelta.count()) + gh->tick(msDelta.count()); + networkHandler->createTimer(*this, serverUpdateInterval); } void CVCMIServer::prepareToRestart() { - if(state == EServerState::GAMEPLAY) + if(getState() != EServerState::GAMEPLAY) { - restartGameplay = true; - * si = * gh->gs->initialOpts; - si->seedToBeUsed = si->seedPostInit = 0; - state = EServerState::LOBBY; - if (si->campState) - { - assert(si->campState->currentScenario().has_value()); - campaignMap = si->campState->currentScenario().value_or(CampaignScenarioID(0)); - campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1); - } - // FIXME: dirry hack to make sure old CGameHandler::run is finished - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + assert(0); + return; + } + + * si = * gh->gs->initialOpts; + si->seedToBeUsed = si->seedPostInit = 0; + setState(EServerState::LOBBY); + if (si->campState) + { + assert(si->campState->currentScenario().has_value()); + campaignMap = si->campState->currentScenario().value_or(CampaignScenarioID(0)); + campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1); } - for(auto c : connections) - { - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); - } - boost::unique_lock queueLock(mx); + for(auto activeConnection : activeConnections) + activeConnection->enterLobbyConnectionMode(); + gh = nullptr; } @@ -294,18 +256,20 @@ bool CVCMIServer::prepareToStartGame() Load::ProgressAccumulator progressTracking; Load::Progress current(1); progressTracking.include(current); - Load::Type currentProgress = std::numeric_limits::max(); - - auto progressTrackingThread = boost::thread([this, &progressTracking, ¤tProgress]() + + auto progressTrackingThread = boost::thread([this, &progressTracking]() { + auto currentProgress = std::numeric_limits::max(); + while(!progressTracking.finished()) { if(progressTracking.get() != currentProgress) { + //FIXME: UNGUARDED MULTITHREADED ACCESS!!! currentProgress = progressTracking.get(); std::unique_ptr loadProgress(new LobbyLoadProgress); loadProgress->progress = currentProgress; - addToAnnounceQueue(std::move(loadProgress)); + announcePack(std::move(loadProgress)); } boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } @@ -314,7 +278,7 @@ bool CVCMIServer::prepareToStartGame() gh = std::make_shared(this); switch(si->mode) { - case StartInfo::CAMPAIGN: + case EStartMode::CAMPAIGN: logNetwork->info("Preparing to start new campaign"); si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); si->fileURI = mi->fileURI; @@ -323,14 +287,14 @@ bool CVCMIServer::prepareToStartGame() gh->init(si.get(), progressTracking); break; - case StartInfo::NEW_GAME: + case EStartMode::NEW_GAME: logNetwork->info("Preparing to start new game"); si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); si->fileURI = mi->fileURI; gh->init(si.get(), progressTracking); break; - case StartInfo::LOAD_GAME: + case EStartMode::LOAD_GAME: logNetwork->info("Preparing to start loaded game"); if(!gh->load(si->mapname)) { @@ -351,158 +315,60 @@ bool CVCMIServer::prepareToStartGame() return true; } -void CVCMIServer::startGameImmidiately() +void CVCMIServer::startGameImmediately() { - for(auto c : connections) - c->enterGameplayConnectionMode(gh->gs); + for(auto activeConnection : activeConnections) + activeConnection->enterGameplayConnectionMode(gh->gs); - state = EServerState::GAMEPLAY; + gh->start(si->mode == EStartMode::LOAD_GAME); + setState(EServerState::GAMEPLAY); + lastTimerUpdateTime = gameplayStartTime = std::chrono::steady_clock::now(); + onTimer(); } -void CVCMIServer::startAsyncAccept() +void CVCMIServer::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) { - assert(!upcomingConnection); - assert(acceptor); + logNetwork->error("Network error receiving a pack. Connection has been closed"); -#if BOOST_VERSION >= 107000 // Boost version >= 1.70 - upcomingConnection = std::make_shared(acceptor->get_executor()); -#else - upcomingConnection = std::make_shared(acceptor->get_io_service()); -#endif - acceptor->async_accept(*upcomingConnection, std::bind(&CVCMIServer::connectionAccepted, this, _1)); -} + std::shared_ptr c = findConnection(connection); + vstd::erase(activeConnections, c); -void CVCMIServer::connectionAccepted(const boost::system::error_code & ec) -{ - if(ec) + if(activeConnections.empty() || hostClientId == c->connectionID) { - if(state != EServerState::SHUTDOWN) - logNetwork->info("Something wrong during accepting: %s", ec.message()); + setState(EServerState::SHUTDOWN); return; } - try + if(gh && getState() == EServerState::GAMEPLAY) { - if(state == EServerState::LOBBY || !hangingConnections.empty()) - { - logNetwork->info("We got a new connection! :)"); - auto c = std::make_shared(upcomingConnection, SERVER_NAME, uuid); - upcomingConnection.reset(); - connections.insert(c); - c->handler = std::make_shared(&CVCMIServer::threadHandleClient, this, c); - } - } - catch(std::exception & e) - { - logNetwork->error("Failure processing new connection! %s", e.what()); - upcomingConnection.reset(); - } + gh->handleClientDisconnection(c); - startAsyncAccept(); -} - -class CVCMIServerPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) -{ -private: - CVCMIServer & handler; - std::shared_ptr gh; - -public: - CVCMIServerPackVisitor(CVCMIServer & handler, std::shared_ptr gh) - :handler(handler), gh(gh) - { - } - - virtual bool callTyped() override { return false; } - - virtual void visitForLobby(CPackForLobby & packForLobby) override - { - handler.handleReceivedPack(std::unique_ptr(&packForLobby)); - } - - virtual void visitForServer(CPackForServer & serverPack) override - { - if (gh) - gh->handleReceivedPack(&serverPack); - else - logNetwork->error("Received pack for game server while in lobby!"); - } - - virtual void visitForClient(CPackForClient & clientPack) override - { - } -}; - -void CVCMIServer::threadHandleClient(std::shared_ptr c) -{ - setThreadName("handleClient"); - c->enterLobbyConnectionMode(); - - while(c->connected) - { - CPack * pack; - - try - { - pack = c->retrievePack(); - } - catch(boost::system::system_error & e) - { - if (e.code() == boost::asio::error::eof) - logNetwork->error("Network error receiving a pack. Connection has been closed"); - else - logNetwork->error("Network error receiving a pack. Connection %s dies. What happened: %s", c->toString(), e.what()); - - hangingConnections.insert(c); - connections.erase(c); - if(connections.empty() || hostClient == c) - state = EServerState::SHUTDOWN; - - if(gh && state == EServerState::GAMEPLAY) - { - gh->handleClientDisconnection(c); - } - break; - } - - CVCMIServerPackVisitor visitor(*this, this->gh); - pack->visit(visitor); - } - - boost::unique_lock queueLock(mx); - - if(c->connected) - { auto lcd = std::make_unique(); lcd->c = c; lcd->clientId = c->connectionID; handleReceivedPack(std::move(lcd)); } - - logNetwork->info("Thread listening for %s ended", c->toString()); - c->handler.reset(); } void CVCMIServer::handleReceivedPack(std::unique_ptr pack) { - CBaseForServerApply * apply = applier->getApplier(typeList.getTypeID(pack.get())); + CBaseForServerApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack.get())); if(apply->applyOnServerBefore(this, pack.get())) - addToAnnounceQueue(std::move(pack)); + announcePack(std::move(pack)); } void CVCMIServer::announcePack(std::unique_ptr pack) { - for(auto c : connections) + for(auto activeConnection : activeConnections) { // FIXME: we need to avoid sending something to client that not yet get answer for LobbyClientConnected // Until UUID set we only pass LobbyClientConnected to this client - if(c->uuid == uuid && !dynamic_cast(pack.get())) - continue; - - c->sendPack(pack.get()); + //if(c->uuid == uuid && !dynamic_cast(pack.get())) + // continue; + activeConnection->sendPack(pack.get()); } - applier->getApplier(typeList.getTypeID(pack.get()))->applyOnServerAfter(this, pack.get()); + applier->getApplier(CTypeList::getInstance().getTypeID(pack.get()))->applyOnServerAfter(this, pack.get()); } void CVCMIServer::announceMessage(const std::string & txt) @@ -510,7 +376,7 @@ void CVCMIServer::announceMessage(const std::string & txt) logNetwork->info("Show message: %s", txt); auto cm = std::make_unique(); cm->message = txt; - addToAnnounceQueue(std::move(cm)); + announcePack(std::move(cm)); } void CVCMIServer::announceTxt(const std::string & txt, const std::string & playerName) @@ -519,115 +385,101 @@ void CVCMIServer::announceTxt(const std::string & txt, const std::string & playe auto cm = std::make_unique(); cm->playerName = playerName; cm->message = txt; - addToAnnounceQueue(std::move(cm)); -} - -void CVCMIServer::addToAnnounceQueue(std::unique_ptr pack) -{ - boost::unique_lock queueLock(mx); - announceQueue.push_back(std::move(pack)); + announcePack(std::move(cm)); } bool CVCMIServer::passHost(int toConnectionId) { - for(auto c : connections) + for(auto activeConnection : activeConnections) { - if(isClientHost(c->connectionID)) + if(isClientHost(activeConnection->connectionID)) continue; - if(c->connectionID != toConnectionId) + if(activeConnection->connectionID != toConnectionId) continue; - hostClient = c; - hostClientId = c->connectionID; + hostClientId = activeConnection->connectionID; announceTxt(boost::str(boost::format("Pass host to connection %d") % toConnectionId)); return true; } return false; } -void CVCMIServer::clientConnected(std::shared_ptr c, std::vector & names, std::string uuid, StartInfo::EMode mode) +void CVCMIServer::clientConnected(std::shared_ptr c, std::vector & names, const std::string & uuid, EStartMode mode) { - if(state == EServerState::LOBBY) - c->connectionID = currentClientId++; + assert(getState() == EServerState::LOBBY); - if(!hostClient) + c->connectionID = currentClientId++; + + if(hostClientId == -1) { - hostClient = c; hostClientId = c->connectionID; si->mode = mode; } logNetwork->info("Connection with client %d established. UUID: %s", c->connectionID, c->uuid); - if(state == EServerState::LOBBY) + for(auto & name : names) { - for(auto & name : names) + logNetwork->info("Client %d player: %s", c->connectionID, name); + ui8 id = currentPlayerId++; + + ClientPlayer cp; + cp.connection = c->connectionID; + cp.name = name; + playerNames.insert(std::make_pair(id, cp)); + announceTxt(boost::str(boost::format("%s (pid %d cid %d) joins the game") % name % id % c->connectionID)); + + //put new player in first slot with AI + for(auto & elem : si->playerInfos) { - logNetwork->info("Client %d player: %s", c->connectionID, name); - ui8 id = currentPlayerId++; - - ClientPlayer cp; - cp.connection = c->connectionID; - cp.name = name; - playerNames.insert(std::make_pair(id, cp)); - announceTxt(boost::str(boost::format("%s (pid %d cid %d) joins the game") % name % id % c->connectionID)); - - //put new player in first slot with AI - for(auto & elem : si->playerInfos) + if(elem.second.isControlledByAI() && !elem.second.compOnly) { - if(elem.second.isControlledByAI() && !elem.second.compOnly) - { - setPlayerConnectedId(elem.second, id); - break; - } + setPlayerConnectedId(elem.second, id); + break; } } } } -void CVCMIServer::clientDisconnected(std::shared_ptr c) +void CVCMIServer::clientDisconnected(std::shared_ptr connection) { - connections -= c; - if(connections.empty() || hostClient == c) - { - state = EServerState::SHUTDOWN; - return; - } - - PlayerReinitInterface startAiPack; - startAiPack.playerConnectionId = PlayerSettings::PLAYER_AI; - - for(auto it = playerNames.begin(); it != playerNames.end();) - { - if(it->second.connection != c->connectionID) - { - ++it; - continue; - } + connection->getConnection()->close(); + vstd::erase(activeConnections, connection); - int id = it->first; - std::string playerLeftMsgText = boost::str(boost::format("%s (pid %d cid %d) left the game") % id % playerNames[id].name % c->connectionID); - announceTxt(playerLeftMsgText); //send lobby text, it will be ignored for non-lobby clients - auto * playerSettings = si->getPlayersSettings(id); - if(!playerSettings) - { - ++it; - continue; - } - - it = playerNames.erase(it); - setPlayerConnectedId(*playerSettings, PlayerSettings::PLAYER_AI); - - if(gh && si && state == EServerState::GAMEPLAY) - { - gh->playerMessages->broadcastMessage(playerSettings->color, playerLeftMsgText); - gh->connections[playerSettings->color].insert(hostClient); - startAiPack.players.push_back(playerSettings->color); - } - } - - if(!startAiPack.players.empty()) - gh->sendAndApply(&startAiPack); +// PlayerReinitInterface startAiPack; +// startAiPack.playerConnectionId = PlayerSettings::PLAYER_AI; +// +// for(auto it = playerNames.begin(); it != playerNames.end();) +// { +// if(it->second.connection != c->connectionID) +// { +// ++it; +// continue; +// } +// +// int id = it->first; +// std::string playerLeftMsgText = boost::str(boost::format("%s (pid %d cid %d) left the game") % id % playerNames[id].name % c->connectionID); +// announceTxt(playerLeftMsgText); //send lobby text, it will be ignored for non-lobby clients +// auto * playerSettings = si->getPlayersSettings(id); +// if(!playerSettings) +// { +// ++it; +// continue; +// } +// +// it = playerNames.erase(it); +// setPlayerConnectedId(*playerSettings, PlayerSettings::PLAYER_AI); +// +// if(gh && si && state == EServerState::GAMEPLAY) +// { +// gh->playerMessages->broadcastMessage(playerSettings->color, playerLeftMsgText); +// // gh->connections[playerSettings->color].insert(hostClient); +// startAiPack.players.push_back(playerSettings->color); +// } +// } +// +// if(!startAiPack.players.empty()) +// gh->sendAndApply(&startAiPack); } void CVCMIServer::reconnectPlayer(int connId) @@ -635,7 +487,7 @@ void CVCMIServer::reconnectPlayer(int connId) PlayerReinitInterface startAiPack; startAiPack.playerConnectionId = connId; - if(gh && si && state == EServerState::GAMEPLAY) + if(gh && si && getState() == EServerState::GAMEPLAY) { for(auto it = playerNames.begin(); it != playerNames.end(); ++it) { @@ -681,7 +533,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, if(mi->scenarioOptionsOfSave) { si = CMemorySerializer::deepCopy(*mi->scenarioOptionsOfSave); - si->mode = StartInfo::LOAD_GAME; + si->mode = EStartMode::LOAD_GAME; if(si->campState) campaignMap = si->campState->currentScenario().value(); @@ -697,7 +549,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, } } } - else if(si->mode == StartInfo::NEW_GAME || si->mode == StartInfo::CAMPAIGN) + else if(si->mode == EStartMode::NEW_GAME || si->mode == EStartMode::CAMPAIGN) { if(mi->campaign) return; @@ -748,15 +600,15 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, void CVCMIServer::updateAndPropagateLobbyState() { - boost::unique_lock stateLock(stateMutex); // Update player settings for RMG // TODO: find appropriate location for this code - if(si->mapGenOptions && si->mode == StartInfo::NEW_GAME) + if(si->mapGenOptions && si->mode == EStartMode::NEW_GAME) { for(const auto & psetPair : si->playerInfos) { const auto & pset = psetPair.second; si->mapGenOptions->setStartingTownForPlayer(pset.color, pset.castle); + si->mapGenOptions->setStartingHeroForPlayer(pset.color, pset.hero); if(pset.isControlledByHuman()) { si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN); @@ -766,7 +618,7 @@ void CVCMIServer::updateAndPropagateLobbyState() auto lus = std::make_unique(); lus->state = *this; - addToAnnounceQueue(std::move(lus)); + announcePack(std::move(lus)); } void CVCMIServer::setPlayer(PlayerColor clickedColor) @@ -1050,7 +902,7 @@ void CVCMIServer::optionSetBonus(PlayerColor player, PlayerStartingBonus id) if(s.castle == FactionID::RANDOM && id == PlayerStartingBonus::RESOURCE) //random castle - can't be resource return; - s.bonus = id;; + s.bonus = id; } bool CVCMIServer::canUseThisHero(PlayerColor player, HeroTypeID ID) @@ -1058,7 +910,7 @@ bool CVCMIServer::canUseThisHero(PlayerColor player, HeroTypeID ID) return VLC->heroh->size() > ID && si->playerInfos[player].castle == VLC->heroh->objects[ID]->heroClass->faction && !vstd::contains(getUsedHeroes(), ID) - && mi->mapHeader->allowedHeroes[ID]; + && mi->mapHeader->allowedHeroes.count(ID); } std::vector CVCMIServer::getUsedHeroes() @@ -1084,169 +936,10 @@ ui8 CVCMIServer::getIdOfFirstUnallocatedPlayer() const if(!si->getPlayersSettings(i->first)) return i->first; } - return 0; } -static void handleCommandOptions(int argc, const char * argv[], boost::program_options::variables_map & options) +INetworkHandler & CVCMIServer::getNetworkHandler() { - namespace po = boost::program_options; - po::options_description opts("Allowed options"); - opts.add_options() - ("help,h", "display help and exit") - ("version,v", "display version information and exit") - ("run-by-client", "indicate that server launched by client on same machine") - ("uuid", po::value(), "") - ("port", po::value(), "port at which server will listen to connections from client") - ("lobby", po::value(), "address to remote lobby") - ("lobby-port", po::value(), "port at which server connect to remote lobby") - ("lobby-uuid", po::value(), "") - ("connections", po::value(), "amount of connections to remote lobby"); - - if(argc > 1) - { - try - { - po::store(po::parse_command_line(argc, argv, opts), options); - } - catch(boost::program_options::error & e) - { - std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; - } - } - -#ifdef SINGLE_PROCESS_APP - options.emplace("run-by-client", po::variable_value{true, true}); -#endif - - po::notify(options); - -#ifndef SINGLE_PROCESS_APP - if(options.count("help")) - { - auto time = std::time(nullptr); - printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); - printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); - printf("This is free software; see the source for copying conditions. There is NO\n"); - printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); - printf("\n"); - std::cout << opts; - exit(0); - } - - if(options.count("version")) - { - printf("%s\n", GameConstants::VCMI_VERSION.c_str()); - std::cout << VCMIDirs::get().genHelpString(); - exit(0); - } -#endif + return *networkHandler; } - -#ifdef SINGLE_PROCESS_APP -#define main server_main -#endif - -#if VCMI_ANDROID_DUAL_PROCESS -void CVCMIServer::create() -{ - const int argc = 1; - const char * argv[argc] = { "android-server" }; -#else -int main(int argc, const char * argv[]) -{ -#endif - -#if !defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) - // Correct working dir executable folder (not bundle folder) so we can use executable relative paths - boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); -#endif - -#ifndef VCMI_IOS - console = new CConsoleHandler(); -#endif - CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Server_log.txt", console); - logConfig.configureDefault(); - logGlobal->info(SERVER_NAME); - - boost::program_options::variables_map opts; - handleCommandOptions(argc, argv, opts); - preinitDLL(console); - logConfig.configure(); - - loadDLLClasses(); - srand((ui32)time(nullptr)); - -#ifdef SINGLE_PROCESS_APP - boost::condition_variable * cond = reinterpret_cast(const_cast(argv[0])); - cond->notify_one(); -#endif - - try - { - boost::asio::io_service io_service; - CVCMIServer server(opts); - - try - { - while(server.getState() != EServerState::SHUTDOWN) - { - server.run(); - } - io_service.run(); - } - catch(boost::system::system_error & e) //for boost errors just log, not crash - probably client shut down connection - { - logNetwork->error(e.what()); - server.setState(EServerState::SHUTDOWN); - } - } - catch(boost::system::system_error & e) - { - logNetwork->error(e.what()); - //catch any startup errors (e.g. can't access port) errors - //and return non-zero status so client can detect error - throw; - } -#if VCMI_ANDROID_DUAL_PROCESS - CAndroidVMHelper envHelper; - envHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "killServer"); -#endif - logConfig.deconfigure(); - vstd::clear_pointer(VLC); - -#if !VCMI_ANDROID_DUAL_PROCESS - return 0; -#endif -} - -#if VCMI_ANDROID_DUAL_PROCESS -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_createServer(JNIEnv * env, jclass cls) -{ - __android_log_write(ANDROID_LOG_INFO, "VCMI", "Got jni call to init server"); - CAndroidVMHelper::cacheVM(env); - - CVCMIServer::create(); -} - -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_initClassloader(JNIEnv * baseEnv, jclass cls) -{ - CAndroidVMHelper::initClassloader(baseEnv); -} -#elif defined(SINGLE_PROCESS_APP) -void CVCMIServer::create(boost::condition_variable * cond, const std::vector & args) -{ - std::vector argv = {cond}; - for(auto & a : args) - argv.push_back(a.c_str()); - main(argv.size(), reinterpret_cast(&*argv.begin())); -} - -#ifdef VCMI_ANDROID -void CVCMIServer::reuseClientJNIEnv(void * jniEnv) -{ - CAndroidVMHelper::initClassloader(jniEnv); - CAndroidVMHelper::alwaysUseLoadedClass = true; -} -#endif // VCMI_ANDROID -#endif // VCMI_ANDROID_DUAL_PROCESS diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 22db4b93c..bc19a097b 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -9,21 +9,16 @@ */ #pragma once -#include "../lib/serializer/Connection.h" +#include "../lib/network/NetworkInterface.h" #include "../lib/StartInfo.h" -#include - -#if defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) -#define VCMI_ANDROID_DUAL_PROCESS 1 -#endif - VCMI_LIB_NAMESPACE_BEGIN class CMapInfo; struct CPackForLobby; +class CConnection; struct StartInfo; struct LobbyInfo; struct PlayerSettings; @@ -36,72 +31,81 @@ VCMI_LIB_NAMESPACE_END class CGameHandler; class CBaseForServerApply; class CBaseForGHApply; +class GlobalLobbyProcessor; enum class EServerState : ui8 { LOBBY, - GAMEPLAY_STARTING, GAMEPLAY, - GAMEPLAY_ENDED, SHUTDOWN }; -class CVCMIServer : public LobbyInfo +class CVCMIServer : public LobbyInfo, public INetworkServerListener, public INetworkTimerListener { - std::atomic restartGameplay; // FIXME: this is just a hack - std::shared_ptr io; - std::shared_ptr acceptor; - std::shared_ptr upcomingConnection; - std::list> announceQueue; - boost::recursive_mutex mx; + /// Network server instance that receives and processes incoming connections on active socket + std::unique_ptr networkServer; + std::unique_ptr lobbyProcessor; + + std::chrono::steady_clock::time_point gameplayStartTime; + std::chrono::steady_clock::time_point lastTimerUpdateTime; + + std::unique_ptr networkHandler; + std::shared_ptr> applier; - std::unique_ptr announceLobbyThread, remoteConnectionsThread; - std::atomic state; + EServerState state = EServerState::LOBBY; + + std::shared_ptr findConnection(const std::shared_ptr &); + + int currentClientId; + ui8 currentPlayerId; + uint16_t port; + bool runByClient; public: + /// List of all active connections + std::vector> activeConnections; + + // INetworkListener impl + void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; + void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; + void onNewConnection(const std::shared_ptr &) override; + void onTimer() override; + std::shared_ptr gh; - ui16 port; - boost::program_options::variables_map cmdLineOptions; - std::set> connections; - std::set> remoteConnections; - std::set> hangingConnections; //keep connections of players disconnected during the game - - std::atomic currentClientId; - std::atomic currentPlayerId; - std::shared_ptr hostClient; - - CVCMIServer(boost::program_options::variables_map & opts); + CVCMIServer(uint16_t port, bool connectToLobby, bool runByClient); ~CVCMIServer(); + void run(); + + bool wasStartedByClient() const; bool prepareToStartGame(); void prepareToRestart(); - void startGameImmidiately(); + void startGameImmediately(); + void startAcceptingIncomingConnections(); - void establishRemoteConnections(); - void connectToRemote(); - void startAsyncAccept(); - void connectionAccepted(const boost::system::error_code & ec); void threadHandleClient(std::shared_ptr c); - void threadAnnounceLobby(); - void handleReceivedPack(std::unique_ptr pack); void announcePack(std::unique_ptr pack); bool passHost(int toConnectionId); void announceTxt(const std::string & txt, const std::string & playerName = "system"); - void announceMessage(const std::string & txt); - void addToAnnounceQueue(std::unique_ptr pack); void setPlayerConnectedId(PlayerSettings & pset, ui8 player) const; void updateStartInfoOnMapChange(std::shared_ptr mapInfo, std::shared_ptr mapGenOpt = {}); - void clientConnected(std::shared_ptr c, std::vector & names, std::string uuid, StartInfo::EMode mode); + void clientConnected(std::shared_ptr c, std::vector & names, const std::string & uuid, EStartMode mode); void clientDisconnected(std::shared_ptr c); void reconnectPlayer(int connId); + void announceMessage(const std::string & txt); + + void handleReceivedPack(std::unique_ptr pack); + void updateAndPropagateLobbyState(); + INetworkHandler & getNetworkHandler(); + void setState(EServerState value); EServerState getState() const; @@ -123,13 +127,4 @@ public: void setCampaignBonus(int bonusId); ui8 getIdOfFirstUnallocatedPlayer() const; - -#if VCMI_ANDROID_DUAL_PROCESS - static void create(); -#elif defined(SINGLE_PROCESS_APP) - static void create(boost::condition_variable * cond, const std::vector & args); -# ifdef VCMI_ANDROID - static void reuseClientJNIEnv(void * jniEnv); -# endif // VCMI_ANDROID -#endif // VCMI_ANDROID_DUAL_PROCESS }; diff --git a/server/GlobalLobbyProcessor.cpp b/server/GlobalLobbyProcessor.cpp new file mode 100644 index 000000000..f06d7c19c --- /dev/null +++ b/server/GlobalLobbyProcessor.cpp @@ -0,0 +1,145 @@ +/* + * GlobalLobbyProcessor.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" +#include "GlobalLobbyProcessor.h" + +#include "CVCMIServer.h" +#include "../lib/CConfigHandler.h" + +GlobalLobbyProcessor::GlobalLobbyProcessor(CVCMIServer & owner) + : owner(owner) +{ + logGlobal->info("Connecting to lobby server"); + establishNewConnection(); +} + +void GlobalLobbyProcessor::establishNewConnection() +{ + std::string hostname = settings["lobby"]["hostname"].String(); + int16_t port = settings["lobby"]["port"].Integer(); + owner.getNetworkHandler().connectToRemote(*this, hostname, port); +} + +void GlobalLobbyProcessor::onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) +{ + if (connection == controlConnection) + { + owner.setState(EServerState::SHUTDOWN); + return; + } + else + { + if (owner.getState() == EServerState::LOBBY) + { + for (auto const & proxy : proxyConnections) + { + if (proxy.second == connection) + { + JsonNode message; + message["type"].String() = "leaveGameRoom"; + message["accountID"].String() = proxy.first; + controlConnection->sendPacket(message.toBytes()); + break; + } + } + } + + // player disconnected + owner.onDisconnected(connection, errorMessage); + } +} + +void GlobalLobbyProcessor::onPacketReceived(const std::shared_ptr & connection, const std::vector & message) +{ + if (connection == controlConnection) + { + JsonNode json(message.data(), message.size()); + + if(json["type"].String() == "operationFailed") + return receiveOperationFailed(json); + + if(json["type"].String() == "loginSuccess") + return receiveLoginSuccess(json); + + if(json["type"].String() == "accountJoinsRoom") + return receiveAccountJoinsRoom(json); + + logGlobal->error("Received unexpected message from lobby server of type '%s' ", json["type"].String()); + } + else + { + // received game message via proxy connection + owner.onPacketReceived(connection, message); + } +} + +void GlobalLobbyProcessor::receiveOperationFailed(const JsonNode & json) +{ + logGlobal->info("Lobby: Failed to login into a lobby server!"); + + owner.setState(EServerState::SHUTDOWN); +} + +void GlobalLobbyProcessor::receiveLoginSuccess(const JsonNode & json) +{ + // no-op, wait just for any new commands from lobby + logGlobal->info("Lobby: Succesfully connected to lobby server"); + owner.startAcceptingIncomingConnections(); +} + +void GlobalLobbyProcessor::receiveAccountJoinsRoom(const JsonNode & json) +{ + std::string accountID = json["accountID"].String(); + + logGlobal->info("Lobby: Account %s will join our room!", accountID); + assert(proxyConnections.count(accountID) == 0); + + proxyConnections[accountID] = nullptr; + establishNewConnection(); +} + +void GlobalLobbyProcessor::onConnectionFailed(const std::string & errorMessage) +{ + owner.setState(EServerState::SHUTDOWN); +} + +void GlobalLobbyProcessor::onConnectionEstablished(const std::shared_ptr & connection) +{ + if (controlConnection == nullptr) + { + controlConnection = connection; + logGlobal->info("Connection to lobby server established"); + + JsonNode toSend; + toSend["type"].String() = "serverLogin"; + toSend["gameRoomID"].String() = owner.uuid; + toSend["accountID"] = settings["lobby"]["accountID"]; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + connection->sendPacket(toSend.toBytes()); + } + else + { + // Proxy connection for a player + std::string guestAccountID; + for (auto const & proxies : proxyConnections) + if (proxies.second == nullptr) + guestAccountID = proxies.first; + + JsonNode toSend; + toSend["type"].String() = "serverProxyLogin"; + toSend["gameRoomID"].String() = owner.uuid; + toSend["guestAccountID"].String() = guestAccountID; + toSend["accountCookie"] = settings["lobby"]["accountCookie"]; + connection->sendPacket(toSend.toBytes()); + + proxyConnections[guestAccountID] = connection; + owner.onNewConnection(connection); + } +} diff --git a/server/GlobalLobbyProcessor.h b/server/GlobalLobbyProcessor.h new file mode 100644 index 000000000..4c81cc70c --- /dev/null +++ b/server/GlobalLobbyProcessor.h @@ -0,0 +1,39 @@ +/* + * GlobalLobbyProcessor.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../lib/network/NetworkInterface.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class CVCMIServer; + +class GlobalLobbyProcessor : public INetworkClientListener +{ + CVCMIServer & owner; + + NetworkConnectionPtr controlConnection; + std::map proxyConnections; + + void onDisconnected(const std::shared_ptr & connection, const std::string & errorMessage) override; + void onPacketReceived(const std::shared_ptr & connection, const std::vector & message) override; + void onConnectionFailed(const std::string & errorMessage) override; + void onConnectionEstablished(const std::shared_ptr &) override; + + void receiveOperationFailed(const JsonNode & json); + void receiveLoginSuccess(const JsonNode & json); + void receiveAccountJoinsRoom(const JsonNode & json); + + void establishNewConnection(); +public: + explicit GlobalLobbyProcessor(CVCMIServer & owner); +}; diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index 5ee02504c..ed0a566bd 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -28,15 +28,16 @@ public: return result; } - virtual void visitForLobby(CPackForLobby & pack) override; - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; - virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; - virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override; - virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override; + void visitForLobby(CPackForLobby & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyChangeHost(LobbyChangeHost & pack) override; + void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; + void visitLobbyChatMessage(LobbyChatMessage & pack) override; + void visitLobbyGuiAction(LobbyGuiAction & pack) override; }; class ApplyOnServerAfterAnnounceNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) @@ -50,12 +51,12 @@ public: { } - virtual void visitForLobby(CPackForLobby & pack) override; - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; + void visitForLobby(CPackForLobby & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyChangeHost(LobbyChangeHost & pack) override; }; class ApplyOnServerNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) @@ -75,20 +76,21 @@ public: return result; } - virtual void visitLobbyClientConnected(LobbyClientConnected & pack) override; - virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; - virtual void visitLobbySetMap(LobbySetMap & pack) override; - virtual void visitLobbySetCampaign(LobbySetCampaign & pack) override; - virtual void visitLobbySetCampaignMap(LobbySetCampaignMap & pack) override; - virtual void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) override; - virtual void visitLobbyEndGame(LobbyEndGame & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; - virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; - virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; - virtual void visitLobbySetPlayer(LobbySetPlayer & pack) override; - virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) override; - virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; - virtual void visitLobbySetSimturns(LobbySetSimturns & pack) override; - virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; - virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override; + void visitLobbyClientConnected(LobbyClientConnected & pack) override; + void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; + void visitLobbySetMap(LobbySetMap & pack) override; + void visitLobbySetCampaign(LobbySetCampaign & pack) override; + void visitLobbySetCampaignMap(LobbySetCampaignMap & pack) override; + void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) override; + void visitLobbyRestartGame(LobbyRestartGame & pack) override; + void visitLobbyStartGame(LobbyStartGame & pack) override; + void visitLobbyChangeHost(LobbyChangeHost & pack) override; + void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; + void visitLobbySetPlayer(LobbySetPlayer & pack) override; + void visitLobbySetPlayerName(LobbySetPlayerName & pack) override; + void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; + void visitLobbySetExtraOptions(LobbySetExtraOptions & pack) override; + void visitLobbySetSimturns(LobbySetSimturns & pack) override; + void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; + void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override; }; diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index a079309ef..1234dfdca 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -13,11 +13,11 @@ #include "CVCMIServer.h" #include "CGameHandler.h" -#include "../lib/serializer/Connection.h" #include "../lib/StartInfo.h" +#include "../lib/CRandomGenerator.h" -// Campaigns #include "../lib/campaign/CampaignState.h" +#include "../lib/serializer/Connection.h" void ClientPermissionsCheckerNetPackVisitor::visitForLobby(CPackForLobby & pack) { @@ -38,67 +38,11 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitForLobby(CPackForLobby & pac void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) { - if(srv.gh) - { - for(auto & connection : srv.hangingConnections) - { - if(connection->uuid == pack.uuid) - { - { - result = true; - return; - } - } - } - } - - if(srv.getState() == EServerState::LOBBY) - { - result = true; - return; - } - - //disconnect immediately and ignore this client - srv.connections.erase(pack.c); - if(pack.c && pack.c->isOpen()) - { - pack.c->close(); - pack.c->connected = false; - } - { - result = false; - return; - } + result = srv.getState() == EServerState::LOBBY; } void ApplyOnServerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) { - if(srv.gh) - { - for(auto & connection : srv.hangingConnections) - { - if(connection->uuid == pack.uuid) - { - logNetwork->info("Reconnection player"); - pack.c->connectionID = connection->connectionID; - for(auto & playerConnection : srv.gh->connections) - { - for(auto & existingConnection : playerConnection.second) - { - if(existingConnection == connection) - { - playerConnection.second.erase(existingConnection); - playerConnection.second.insert(pack.c); - break; - } - } - } - srv.hangingConnections.erase(connection); - break; - } - } - } - srv.clientConnected(pack.c, pack.names, pack.uuid, pack.mode); // Server need to pass some data to newly connected client pack.clientId = pack.c->connectionID; @@ -114,15 +58,17 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientConnected(LobbyCl // Until UUID set we only pass LobbyClientConnected to this client pack.c->uuid = pack.uuid; srv.updateAndPropagateLobbyState(); - if(srv.getState() == EServerState::GAMEPLAY) - { - //immediately start game - std::unique_ptr startGameForReconnectedPlayer(new LobbyStartGame); - startGameForReconnectedPlayer->initializedStartInfo = srv.si; - startGameForReconnectedPlayer->initializedGameState = srv.gh->gameState(); - startGameForReconnectedPlayer->clientId = pack.c->connectionID; - srv.addToAnnounceQueue(std::move(startGameForReconnectedPlayer)); - } + +// FIXME: what is this??? We do NOT support reconnection into ongoing game - at the very least queries and battles are NOT serialized +// if(srv.getState() == EServerState::GAMEPLAY) +// { +// //immediately start game +// std::unique_ptr startGameForReconnectedPlayer(new LobbyStartGame); +// startGameForReconnectedPlayer->initializedStartInfo = srv.si; +// startGameForReconnectedPlayer->initializedGameState = srv.gh->gameState(); +// startGameForReconnectedPlayer->clientId = pack.c->connectionID; +// srv.announcePack(std::move(startGameForReconnectedPlayer)); +// } } void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) @@ -135,13 +81,13 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyC if(pack.shutdownServer) { - if(!srv.cmdLineOptions.count("run-by-client")) + if(!srv.wasStartedByClient()) { result = false; return; } - if(pack.c->uuid != srv.cmdLineOptions["uuid"].as()) + if(pack.c->connectionID != srv.hostClientId) { result = false; return; @@ -154,46 +100,36 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientDisconnected(LobbyC void ApplyOnServerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { srv.clientDisconnected(pack.c); - pack.c->close(); - pack.c->connected = false; - result = true; } void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { - if(pack.c && pack.c->isOpen()) - { - boost::unique_lock lock(*pack.c->mutexWrite); - pack.c->close(); - pack.c->connected = false; - } - if(pack.shutdownServer) { logNetwork->info("Client requested shutdown, server will close itself..."); srv.setState(EServerState::SHUTDOWN); return; } - else if(srv.connections.empty()) + else if(srv.activeConnections.empty()) { logNetwork->error("Last connection lost, server will close itself..."); srv.setState(EServerState::SHUTDOWN); } - else if(pack.c == srv.hostClient) + else if(pack.c->connectionID == srv.hostClientId) { auto ph = std::make_unique(); - auto newHost = *RandomGeneratorUtil::nextItem(srv.connections, CRandomGenerator::getDefault()); + auto newHost = srv.activeConnections.front(); ph->newHostConnectionId = newHost->connectionID; - srv.addToAnnounceQueue(std::move(ph)); + srv.announcePack(std::move(ph)); } srv.updateAndPropagateLobbyState(); - if(srv.getState() != EServerState::SHUTDOWN && srv.remoteConnections.count(pack.c)) - { - srv.remoteConnections -= pack.c; - srv.connectToRemote(); - } +// if(srv.getState() != EServerState::SHUTDOWN && srv.remoteConnections.count(pack.c)) +// { +// srv.remoteConnections -= pack.c; +// srv.connectToRemote(); +// } } void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack) @@ -217,7 +153,7 @@ void ApplyOnServerNetPackVisitor::visitLobbySetMap(LobbySetMap & pack) void ApplyOnServerNetPackVisitor::visitLobbySetCampaign(LobbySetCampaign & pack) { srv.si->mapname = pack.ourCampaign->getFilename(); - srv.si->mode = StartInfo::CAMPAIGN; + srv.si->mode = EStartMode::CAMPAIGN; srv.si->campState = pack.ourCampaign; srv.si->turnTimerInfo = TurnTimerInfo{}; @@ -256,26 +192,27 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction result = srv.isClientHost(pack.c->connectionID); } -void ClientPermissionsCheckerNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ClientPermissionsCheckerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { result = srv.isClientHost(pack.c->connectionID); } -void ApplyOnServerNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ApplyOnServerNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { srv.prepareToRestart(); result = true; } -void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyEndGame(LobbyEndGame & pack) +void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyRestartGame(LobbyRestartGame & pack) { - boost::unique_lock stateLock(srv.stateMutex); - for(auto & c : srv.connections) - { - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); - } + for(const auto & connection : srv.activeConnections) + connection->enterLobbyConnectionMode(); +} + +void ClientPermissionsCheckerNetPackVisitor::visitLobbyPrepareStartGame(LobbyPrepareStartGame & pack) +{ + result = srv.isClientHost(pack.c->connectionID); } void ClientPermissionsCheckerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) @@ -304,22 +241,20 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) pack.initializedStartInfo = std::make_shared(*srv.gh->getStartInfo(true)); pack.initializedGameState = srv.gh->gameState(); - - srv.setState(EServerState::GAMEPLAY_STARTING); result = true; } void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) { if(pack.clientId == -1) //do not restart game for single client only - srv.startGameImmidiately(); + srv.startGameImmediately(); else { - for(auto & c : srv.connections) + for(const auto & connection : srv.activeConnections) { - if(c->connectionID == pack.clientId) + if(connection->connectionID == pack.clientId) { - c->enterGameplayConnectionMode(srv.gh->gameState()); + connection->enterGameplayConnectionMode(srv.gh->gameState()); srv.reconnectPlayer(pack.clientId); } } @@ -414,6 +349,12 @@ void ApplyOnServerNetPackVisitor::visitLobbySetTurnTime(LobbySetTurnTime & pack) result = true; } +void ApplyOnServerNetPackVisitor::visitLobbySetExtraOptions(LobbySetExtraOptions & pack) +{ + srv.si->extraOptionsInfo = pack.extraOptionsInfo; + result = true; +} + void ApplyOnServerNetPackVisitor::visitLobbySetDifficulty(LobbySetDifficulty & pack) { srv.si->difficulty = std::clamp(pack.difficulty, 0, 4); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 540751603..6ad658784 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -20,11 +20,11 @@ #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/gameState/CGameState.h" #include "../lib/battle/IBattleState.h" #include "../lib/battle/BattleAction.h" #include "../lib/battle/Unit.h" -#include "../lib/serializer/Connection.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/spells/ISpellMechanics.h" #include "../lib/serializer/Cast.h" @@ -134,13 +134,15 @@ void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack) void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) { - gh.throwIfWrongPlayer(&pack, pack.src.owningPlayer()); //second hero can be ally + if(gh.getHero(pack.src.artHolder)) + gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.src.artHolder)); //second hero can be ally result = gh.moveArtifact(pack.src, pack.dst); } void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) { - gh.throwIfWrongOwner(&pack, pack.srcHero); + if(dynamic_cast(gh.getObj(pack.srcHero)) == nullptr) + gh.throwIfWrongOwner(&pack, pack.srcHero); if(pack.swap) gh.throwIfWrongOwner(&pack, pack.dstHero); result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap, pack.equipped, pack.backpack); @@ -154,7 +156,7 @@ void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack) { - gh.throwIfWrongPlayer(&pack, pack.al.owningPlayer()); + gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.al.artHolder)); result = gh.eraseArtifactByClient(pack.al); } @@ -168,7 +170,7 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) { const CGObjectInstance * object = gh.getObj(pack.marketId); const CGHeroInstance * hero = gh.getHero(pack.heroId); - const IMarket * market = IMarket::castFrom(object); + const auto * market = dynamic_cast(object); gh.throwIfWrongPlayer(&pack); @@ -221,42 +223,49 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) { case EMarketMode::RESOURCE_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i], pack.r2[i]); + result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::RESOURCE_PLAYER: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sendResources(pack.val[i], pack.player, GameResID(pack.r1[i]), PlayerColor(pack.r2[i])); + result &= gh.sendResources(pack.val[i], pack.player, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::CREATURE_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellCreatures(pack.val[i], market, hero, SlotID(pack.r1[i]), GameResID(pack.r2[i])); + result &= gh.sellCreatures(pack.val[i], market, hero, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::RESOURCE_ARTIFACT: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.buyArtifact(market, hero, GameResID(pack.r1[i]), ArtifactID(pack.r2[i])); + result &= gh.buyArtifact(market, hero, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::ARTIFACT_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellArtifact(market, hero, ArtifactInstanceID(pack.r1[i]), GameResID(pack.r2[i])); + result &= gh.sellArtifact(market, hero, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::CREATURE_UNDEAD: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.transformInUndead(market, hero, SlotID(pack.r1[i])); + result &= gh.transformInUndead(market, hero, pack.r1[i].as()); break; case EMarketMode::RESOURCE_SKILL: for(int i = 0; i < pack.r2.size(); ++i) - result &= gh.buySecSkill(market, hero, SecondarySkill(pack.r2[i])); + result &= gh.buySecSkill(market, hero, pack.r2[i].as()); break; case EMarketMode::CREATURE_EXP: { - std::vector slotIDs(pack.r1.begin(), pack.r1.end()); + std::vector slotIDs; std::vector count(pack.val.begin(), pack.val.end()); + + for(auto const & slot : pack.r1) + slotIDs.push_back(slot.as()); + result = gh.sacrificeCreatures(market, hero, slotIDs, count); return; } case EMarketMode::ARTIFACT_EXP: { - std::vector positions(pack.r1.begin(), pack.r1.end()); + std::vector positions; + for(auto const & artInstId : pack.r1) + positions.push_back(artInstId.as()); + result = gh.sacrificeArtifact(market, hero, positions); return; } @@ -275,7 +284,7 @@ void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) { gh.throwIfWrongPlayer(&pack); - result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player); + result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player, pack.nhid); } void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack) @@ -320,9 +329,9 @@ void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) { gh.throwIfWrongOwner(&pack, pack.hid); - const CSpell * s = pack.sid.toSpell(); - if(!s) + if (!pack.sid.hasValue()) gh.throwNotAllowedAction(&pack); + const CGHeroInstance * h = gh.getHero(pack.hid); if(!h) gh.throwNotAllowedAction(&pack); @@ -331,6 +340,7 @@ void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) p.caster = h; p.pos = pack.pos; + const CSpell * s = pack.sid.toSpell(); result = s->adventureCast(gh.spellEnv, p); } diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index 487af47de..c2cc0de42 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -28,34 +28,34 @@ public: return result; } - virtual void visitSaveGame(SaveGame & pack) override; - virtual void visitGamePause(GamePause & pack) override; - virtual void visitEndTurn(EndTurn & pack) override; - virtual void visitDismissHero(DismissHero & pack) override; - virtual void visitMoveHero(MoveHero & pack) override; - virtual void visitCastleTeleportHero(CastleTeleportHero & pack) override; - virtual void visitArrangeStacks(ArrangeStacks & pack) override; - virtual void visitBulkMoveArmy(BulkMoveArmy & pack) override; - virtual void visitBulkSplitStack(BulkSplitStack & pack) override; - virtual void visitBulkMergeStacks(BulkMergeStacks & pack) override; - virtual void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) override; - virtual void visitDisbandCreature(DisbandCreature & pack) override; - virtual void visitBuildStructure(BuildStructure & pack) override; - virtual void visitRecruitCreatures(RecruitCreatures & pack) override; - virtual void visitUpgradeCreature(UpgradeCreature & pack) override; - virtual void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) override; - virtual void visitExchangeArtifacts(ExchangeArtifacts & pack) override; - virtual void visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) override; - virtual void visitAssembleArtifacts(AssembleArtifacts & pack) override; - virtual void visitEraseArtifactByClient(EraseArtifactByClient & pack) override; - virtual void visitBuyArtifact(BuyArtifact & pack) override; - virtual void visitTradeOnMarketplace(TradeOnMarketplace & pack) override; - virtual void visitSetFormation(SetFormation & pack) override; - virtual void visitHireHero(HireHero & pack) override; - virtual void visitBuildBoat(BuildBoat & pack) override; - virtual void visitQueryReply(QueryReply & pack) override; - virtual void visitMakeAction(MakeAction & pack) override; - virtual void visitDigWithHero(DigWithHero & pack) override; - virtual void visitCastAdvSpell(CastAdvSpell & pack) override; - virtual void visitPlayerMessage(PlayerMessage & pack) override; + void visitSaveGame(SaveGame & pack) override; + void visitGamePause(GamePause & pack) override; + void visitEndTurn(EndTurn & pack) override; + void visitDismissHero(DismissHero & pack) override; + void visitMoveHero(MoveHero & pack) override; + void visitCastleTeleportHero(CastleTeleportHero & pack) override; + void visitArrangeStacks(ArrangeStacks & pack) override; + void visitBulkMoveArmy(BulkMoveArmy & pack) override; + void visitBulkSplitStack(BulkSplitStack & pack) override; + void visitBulkMergeStacks(BulkMergeStacks & pack) override; + void visitBulkSmartSplitStack(BulkSmartSplitStack & pack) override; + void visitDisbandCreature(DisbandCreature & pack) override; + void visitBuildStructure(BuildStructure & pack) override; + void visitRecruitCreatures(RecruitCreatures & pack) override; + void visitUpgradeCreature(UpgradeCreature & pack) override; + void visitGarrisonHeroSwap(GarrisonHeroSwap & pack) override; + void visitExchangeArtifacts(ExchangeArtifacts & pack) override; + void visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) override; + void visitAssembleArtifacts(AssembleArtifacts & pack) override; + void visitEraseArtifactByClient(EraseArtifactByClient & pack) override; + void visitBuyArtifact(BuyArtifact & pack) override; + void visitTradeOnMarketplace(TradeOnMarketplace & pack) override; + void visitSetFormation(SetFormation & pack) override; + void visitHireHero(HireHero & pack) override; + void visitBuildBoat(BuildBoat & pack) override; + void visitQueryReply(QueryReply & pack) override; + void visitMakeAction(MakeAction & pack) override; + void visitDigWithHero(DigWithHero & pack) override; + void visitCastAdvSpell(CastAdvSpell & pack) override; + void visitPlayerMessage(PlayerMessage & pack) override; }; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index ebd51e826..247ee4804 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -29,11 +29,12 @@ TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): void TurnTimerHandler::onGameplayStart(PlayerColor player) { - std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { timers[player] = si->turnTimerInfo; timers[player].turnTimer = 0; + timers[player].battleTimer = 0; + timers[player].unitTimer = 0; timers[player].isActive = true; timers[player].isBattle = false; lastUpdate[player] = std::numeric_limits::max(); @@ -43,7 +44,6 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) { - std::lock_guard guard(mx); assert(player.isValidPlayer()); timers[player].isActive = enabled; sendTimerUpdate(player); @@ -51,7 +51,6 @@ void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) void TurnTimerHandler::setEndTurnAllowed(PlayerColor player, bool enabled) { - std::lock_guard guard(mx); assert(player.isValidPlayer()); endTurnAllowed[player] = enabled; } @@ -67,14 +66,13 @@ void TurnTimerHandler::sendTimerUpdate(PlayerColor player) void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { - std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { if(si->turnTimerInfo.isEnabled()) { endTurnAllowed[player] = true; auto & timer = timers[player]; - if(si->turnTimerInfo.baseTimer > 0) + if(si->turnTimerInfo.accumulatingTurnTimer) timer.baseTimer += timer.turnTimer; timer.turnTimer = si->turnTimerInfo.turnTimer; @@ -85,16 +83,21 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) void TurnTimerHandler::update(int waitTime) { - std::lock_guard guard(mx); - if(const auto * gs = gameHandler.gameState()) - { - for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) - if(gs->isPlayerMakingTurn(player)) - onPlayerMakingTurn(player, waitTime); - - for (auto & battle : gs->currentBattles) - onBattleLoop(battle->battleID, waitTime); - } + if(!gameHandler.getStartInfo()->turnTimerInfo.isEnabled()) + return; + + for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + if(gameHandler.gameState()->isPlayerMakingTurn(player)) + onPlayerMakingTurn(player, waitTime); + + // create copy for iterations - battle might end during onBattleLoop call + std::vector ongoingBattles; + + for (auto & battle : gameHandler.gameState()->currentBattles) + ongoingBattles.push_back(battle->battleID); + + for (auto & battleID : ongoingBattles) + onBattleLoop(battleID, waitTime); } bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor player, int waitTime) @@ -103,11 +106,8 @@ bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor { timer -= waitTime; lastUpdate[player] += waitTime; - int frequency = (timer > turnTimePropagateThreshold - && initialTimer - timer > turnTimePropagateThreshold) - ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; - if(lastUpdate[player] >= frequency) + if(lastUpdate[player] >= turnTimePropagateFrequency) sendTimerUpdate(player); return true; @@ -117,7 +117,6 @@ bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !si->turnTimerInfo.isEnabled()) @@ -127,17 +126,18 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) const auto * state = gameHandler.getPlayerState(player); if(state && state->human && timer.isActive && !timer.isBattle && state->status == EPlayerStatus::INGAME) { - if(!timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) - { - if(timer.baseTimer > 0) - { - timer.turnTimer = timer.baseTimer; - timer.baseTimer = 0; - onPlayerMakingTurn(player, 0); - } - else if(endTurnAllowed[state->color] && !gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries - gameHandler.turnOrder->onPlayerEndsTurn(state->color); - } + // turn timers are only used if turn timer is non-zero + if (si->turnTimerInfo.turnTimer == 0) + return; + + if(timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) + return; + + if(timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime)) + return; + + if(endTurnAllowed[state->color] && !gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries + gameHandler.turnOrder->onPlayerEndsTurn(state->color); } } @@ -158,7 +158,6 @@ bool TurnTimerHandler::isPvpBattle(const BattleID & battleID) const void TurnTimerHandler::onBattleStart(const BattleID & battleID) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs) @@ -176,8 +175,8 @@ void TurnTimerHandler::onBattleStart(const BattleID & battleID) auto & timer = timers[i]; timer.isBattle = true; timer.isActive = si->turnTimerInfo.isBattleEnabled(); - timer.battleTimer = (pvpBattle ? si->turnTimerInfo.battleTimer : 0); - timer.creatureTimer = (pvpBattle ? si->turnTimerInfo.creatureTimer : si->turnTimerInfo.battleTimer); + timer.battleTimer = si->turnTimerInfo.battleTimer; + timer.unitTimer = (pvpBattle ? si->turnTimerInfo.unitTimer : 0); sendTimerUpdate(i); } @@ -186,7 +185,6 @@ void TurnTimerHandler::onBattleStart(const BattleID & battleID) void TurnTimerHandler::onBattleEnd(const BattleID & battleID) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs) @@ -201,8 +199,6 @@ void TurnTimerHandler::onBattleEnd(const BattleID & battleID) auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); - bool pvpBattle = isPvpBattle(battleID); - for(auto i : {attacker, defender}) { if(i.isValidPlayer()) @@ -210,14 +206,6 @@ void TurnTimerHandler::onBattleEnd(const BattleID & battleID) auto & timer = timers[i]; timer.isBattle = false; timer.isActive = true; - if(!pvpBattle) - { - if(si->turnTimerInfo.baseTimer && timer.baseTimer == 0) - timer.baseTimer = timer.creatureTimer; - else if(si->turnTimerInfo.turnTimer && timer.turnTimer == 0) - timer.turnTimer = timer.creatureTimer; - } - sendTimerUpdate(i); } } @@ -225,7 +213,6 @@ void TurnTimerHandler::onBattleEnd(const BattleID & battleID) void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack & stack) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !gs->getBattle(battleID)) @@ -242,9 +229,9 @@ void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack auto player = stack.getOwner(); auto & timer = timers[player]; - if(timer.battleTimer == 0) - timer.battleTimer = timer.creatureTimer; - timer.creatureTimer = si->turnTimerInfo.creatureTimer; + if(timer.accumulatingUnitTimer) + timer.battleTimer += timer.unitTimer; + timer.unitTimer = si->turnTimerInfo.unitTimer; sendTimerUpdate(player); } @@ -252,7 +239,6 @@ void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) { - std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs) @@ -283,56 +269,48 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) return; const auto * state = gameHandler.getPlayerState(player); - assert(state && state->status != EPlayerStatus::INGAME); + assert(state && state->status == EPlayerStatus::INGAME); if(!state || state->status != EPlayerStatus::INGAME || !state->human) return; auto & timer = timers[player]; - if(timer.isActive && timer.isBattle && !timerCountDown(timer.creatureTimer, si->turnTimerInfo.creatureTimer, player, waitTime)) + if(timer.isActive && timer.isBattle) { + // in pvp battles, timers are only used if unit timer is non-zero + if(isPvpBattle(battleID) && si->turnTimerInfo.unitTimer == 0) + return; + + if (timerCountDown(timer.unitTimer, si->turnTimerInfo.unitTimer, player, waitTime)) + return; + + if (timerCountDown(timer.battleTimer, si->turnTimerInfo.battleTimer, player, waitTime)) + return; + + if (timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) + return; + + if (timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime)) + return; + if(isPvpBattle(battleID)) { - if(timer.battleTimer > 0) - { - timer.creatureTimer = timer.battleTimer; - timerCountDown(timer.creatureTimer, timer.battleTimer, player, 0); - timer.battleTimer = 0; - } + BattleAction doNothing; + doNothing.side = side; + if(isTactisPhase) + doNothing.actionType = EActionType::END_TACTIC_PHASE; else { - BattleAction doNothing; - doNothing.side = side; - if(isTactisPhase) - doNothing.actionType = EActionType::END_TACTIC_PHASE; - else - { - doNothing.actionType = EActionType::DEFEND; - doNothing.stackNumber = stack->unitId(); - } - gameHandler.battles->makePlayerBattleAction(battleID, player, doNothing); + doNothing.actionType = EActionType::DEFEND; + doNothing.stackNumber = stack->unitId(); } + gameHandler.battles->makePlayerBattleAction(battleID, player, doNothing); } else { - if(timer.turnTimer > 0) - { - timer.creatureTimer = timer.turnTimer; - timerCountDown(timer.creatureTimer, timer.turnTimer, player, 0); - timer.turnTimer = 0; - } - else if(timer.baseTimer > 0) - { - timer.creatureTimer = timer.baseTimer; - timerCountDown(timer.creatureTimer, timer.baseTimer, player, 0); - timer.baseTimer = 0; - } - else - { - BattleAction retreat; - retreat.side = side; - retreat.actionType = EActionType::RETREAT; //harsh punishment - gameHandler.battles->makePlayerBattleAction(battleID, player, retreat); - } + BattleAction retreat; + retreat.side = side; + retreat.actionType = EActionType::RETREAT; //harsh punishment + gameHandler.battles->makePlayerBattleAction(battleID, player, retreat); } } } diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index a780b466c..8f6fae5a6 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -25,13 +25,10 @@ class CGameHandler; class TurnTimerHandler { CGameHandler & gameHandler; - const int turnTimePropagateFrequency = 5000; - const int turnTimePropagateFrequencyCrit = 1000; - const int turnTimePropagateThreshold = 3000; + const int turnTimePropagateFrequency = 1000; std::map timers; std::map lastUpdate; std::map endTurnAllowed; - std::recursive_mutex mx; void onPlayerMakingTurn(PlayerColor player, int waitTime); void onBattleLoop(const BattleID & battleID, int waitTime); diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 6fcf831b0..653cbc9db 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -102,13 +102,13 @@ bool BattleActionProcessor::doHeroSpellAction(const CBattleInfoCallback & battle return false; } - const CSpell * s = ba.spell.toSpell(); - if (!s) + if (!ba.spell.hasValue()) { logGlobal->error("Wrong spell id (%d)!", ba.spell.getNum()); return false; } + const CSpell * s = ba.spell.toSpell(); spells::BattleCast parameters(&battle, h, spells::Mode::HERO, s); spells::detail::ProblemImpl problem; @@ -268,12 +268,17 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); } - const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); + static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeMelee)); + const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector); + const bool retaliation = destinationStack->ableToRetaliate(); + bool ferocityApplied = false; + int32_t defenderInitialQuantity = destinationStack->getCount(); + for (int i = 0; i < totalAttacks; ++i) { //first strike - if(i == 0 && firstStrike && retaliation) + if(i == 0 && firstStrike && retaliation && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION)) { makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true); } @@ -282,6 +287,18 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) { makeAttack(battle, stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack + + if(!ferocityApplied && stack->hasBonusOfType(BonusType::FEROCITY)) + { + auto ferocityBonus = stack->getBonus(Selector::type()(BonusType::FEROCITY)); + int32_t requiredCreaturesToKill = ferocityBonus->additionalInfo != CAddInfo::NONE ? ferocityBonus->additionalInfo[0] : 1; + if(defenderInitialQuantity - destinationStack->getCount() >= requiredCreaturesToKill) + { + ferocityApplied = true; + int additionalAttacksCount = stack->valOfBonuses(BonusType::FEROCITY); + totalAttacks += additionalAttacksCount; + } + } } //counterattack @@ -338,7 +355,11 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co return false; } - makeAttack(battle, stack, destinationStack, 0, destination, true, true, false); + static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeRanged)); + const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector); + + if (!firstStrike) + makeAttack(battle, stack, destinationStack, 0, destination, true, true, false); //ranged counterattack if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) @@ -360,7 +381,7 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); } - for(int i = 1; i < totalRangedAttacks; ++i) + for(int i = firstStrike ? 0:1; i < totalRangedAttacks; ++i) { if( stack->alive() @@ -383,7 +404,7 @@ bool BattleActionProcessor::doCatapultAction(const CBattleInfoCallback & battle, if (!canStackAct(battle, stack)) return false; - std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); + std::shared_ptr catapultAbility = stack->getFirstBonus(Selector::type()(BonusType::CATAPULT)); if(!catapultAbility || catapultAbility->subtype == BonusSubtypeID()) { gameHandler->complain("We do not know how to shoot :P"); @@ -411,24 +432,48 @@ bool BattleActionProcessor::doUnitSpellAction(const CBattleInfoCallback & battle std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spellID))); - //TODO special bonus for genies ability - if (randSpellcaster && battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE) - spellID = battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); - - if (spellID == SpellID::NONE) - gameHandler->complain("That stack can't cast spells!"); - else + if (!spellcaster && !randSpellcaster) { - const CSpell * spell = SpellID(spellID).toSpell(); - spells::BattleCast parameters(&battle, stack, spells::Mode::CREATURE_ACTIVE, spell); - int32_t spellLvl = 0; - if(spellcaster) - vstd::amax(spellLvl, spellcaster->val); - if(randSpellcaster) - vstd::amax(spellLvl, randSpellcaster->val); - parameters.setSpellLevel(spellLvl); - parameters.cast(gameHandler->spellEnv, target); + gameHandler->complain("That stack can't cast spells!"); + return false; } + + if (randSpellcaster) + { + if (target.size() != 1) + { + gameHandler->complain("Invalid target for random spellcaster!"); + return false; + } + + const battle::Unit * subject = target[0].unitValue; + if (target[0].unitValue == nullptr) + subject = battle.battleGetStackByPos(target[0].hexValue, true); + + if (subject == nullptr) + { + gameHandler->complain("Invalid target for random spellcaster!"); + return false; + } + + spellID = battle.getRandomBeneficialSpell(gameHandler->getRandomGenerator(), stack, subject); + + if (spellID == SpellID::NONE) + { + gameHandler->complain("That stack can't cast spells!"); + return false; + } + } + + const CSpell * spell = SpellID(spellID).toSpell(); + spells::BattleCast parameters(&battle, stack, spells::Mode::CREATURE_ACTIVE, spell); + int32_t spellLvl = 0; + if(spellcaster) + vstd::amax(spellLvl, spellcaster->val); + if(randSpellcaster) + vstd::amax(spellLvl, randSpellcaster->val); + parameters.setSpellLevel(spellLvl); + parameters.cast(gameHandler->spellEnv, target); return true; } @@ -447,7 +492,7 @@ bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, con } const battle::Unit * destStack = nullptr; - std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); + std::shared_ptr healerAbility = stack->getFirstBonus(Selector::type()(BonusType::HEALER)); if(target.at(0).unitValue) destStack = target.at(0).unitValue; @@ -620,7 +665,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta ret = path.second; - int creSpeed = curStack->speed(0, true); + int creSpeed = curStack->getMovementRange(0); if (battle.battleGetTacticDist() > 0 && creSpeed > 0) creSpeed = GameConstants::BFIELD_SIZE; @@ -930,7 +975,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, stack, distance, true); } - std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + std::shared_ptr bonus = attacker->getFirstBonus(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); if(bonus && ranged) //TODO: make it work in melee? { //this is need for displaying hit animation @@ -1032,7 +1077,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const const CStack * actor = item.first; int64_t rawDamage = item.second; - const CGHeroInstance * actorOwner = battle.battleGetFightingHero(actor->unitOwner()); + const CGHeroInstance * actorOwner = battle.battleGetFightingHero(actor->unitSide()); if(actorOwner) { @@ -1067,7 +1112,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const { MetaString text; text.appendLocalString(EMetaText::GENERAL_TXT, 376); - text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); + text.replaceName(SpellID(SpellID::FIRE_SHIELD)); text.replaceNumber(totalDamage); blm.lines.push_back(std::move(text)); } @@ -1080,16 +1125,13 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const handleAfterAttackCasting(battle, ranged, attacker, defender); } -void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender) { if(attacker->hasBonusOfType(attackMode)) { - std::set spellsToCast; TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - for(const auto & sf : *spells) - { - spellsToCast.insert(sf->subtype.as()); - } + std::set spellsToCast = getSpellsForAttackCasting(spells, defender); + for(SpellID spellID : spellsToCast) { bool castMe = false; @@ -1103,18 +1145,10 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo for(const auto & sf : *spellsByType) { int meleeRanged; - if(sf->additionalInfo.size() < 2) - { - // legacy format - vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); - meleeRanged = sf->additionalInfo[0] / 1000; - } - else - { - vstd::amax(spellLevel, sf->additionalInfo[0]); - meleeRanged = sf->additionalInfo[1]; - } - if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) + vstd::amax(spellLevel, sf->additionalInfo[0]); + meleeRanged = sf->additionalInfo[1]; + + if (meleeRanged == CAddInfo::NONE || meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) castMe = true; } int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, BonusSubtypeID(spellID)))); @@ -1148,11 +1182,130 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo } } +std::set BattleActionProcessor::getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender) +{ + std::set spellsToCast; + constexpr int unlayeredItemsInternalLayer = -1; + + std::map>> spellsWithBackupLayers; + + for(int i = 0; i < spells->size(); i++) + { + std::shared_ptr bonus = spells->operator[](i); + int layer = bonus->additionalInfo[2]; + vstd::amax(layer, -1); + spellsWithBackupLayers[layer].push_back(bonus); + } + + auto addSpellsFromLayer = [&](int layer) -> void + { + assert(spellsWithBackupLayers.find(layer) != spellsWithBackupLayers.end()); + + for(const auto & spell : spellsWithBackupLayers[layer]) + { + if (spell->subtype.as() != SpellID()) + spellsToCast.insert(spell->subtype.as()); + else + logGlobal->error("Invalid spell to cast during attack!"); + } + }; + + if(spellsWithBackupLayers.find(unlayeredItemsInternalLayer) != spellsWithBackupLayers.end()) + { + addSpellsFromLayer(unlayeredItemsInternalLayer); + spellsWithBackupLayers.erase(unlayeredItemsInternalLayer); + } + + for(auto item : spellsWithBackupLayers) + { + bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(), + [&](const std::shared_ptr spell) + { + std::vector activeSpells = defender->activeSpells(); + return vstd::find(activeSpells, spell->subtype.as()) != activeSpells.end(); + }); + + if(!areCurrentLayerSpellsApplied || item.first == spellsWithBackupLayers.rbegin()->first) + { + addSpellsFromLayer(item.first); + break; + } + } + + return spellsToCast; +} + void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) { attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? } +void BattleActionProcessor::handleDeathStare(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) +{ + // mechanics of Death Stare as in H3: + // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution + //original formula x = min(x, (gorgons_count + 9)/10); + + /* mechanics of Accurate Shot as in HotA: + * each creature in an attacking stack has a X% chance of killing a creature in the attacked squad, + * but the total number of killed creatures cannot be more than (number of creatures in an attacking squad) * X/100 (rounded up). + * X = 3 multiplier for shooting without penalty and X = 2 if shooting with penalty. Ability doesn't work if shooting at creatures behind walls. + */ + + auto subtype = BonusCustomSubtype::deathStareGorgon; + + if (ranged) + { + bool rangePenalty = battle.battleHasDistancePenalty(attacker, attacker->getPosition(), defender->getPosition()); + bool obstaclePenalty = battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()); + + if(rangePenalty) + { + if(obstaclePenalty) + subtype = BonusCustomSubtype::deathStareRangeObstaclePenalty; + else + subtype = BonusCustomSubtype::deathStareRangePenalty; + } + else + { + if(obstaclePenalty) + subtype = BonusCustomSubtype::deathStareObstaclePenalty; + else + subtype = BonusCustomSubtype::deathStareNoRangePenalty; + } + } + + int singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::DEATH_STARE, subtype); + double chanceToKill = singleCreatureKillChancePercent / 100.0; + vstd::amin(chanceToKill, 1); //cap at 100% + std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); + int killedCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); + + int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100; + vstd::amin(killedCreatures, maxToKill); + + killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); + + if(killedCreatures) + { + //TODO: death stare or accurate shot was not originally available for multiple-hex attacks, but... + + SpellID spellID = SpellID(SpellID::DEATH_STARE); //also used as fallback spell for ACCURATE_SHOT + auto bonus = attacker->getBonus(Selector::typeSubtype(BonusType::DEATH_STARE, subtype)); + if(bonus && bonus->additionalInfo[0] != SpellID::NONE) + spellID = SpellID(bonus->additionalInfo[0]); + + const CSpell * spell = spellID.toSpell(); + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + parameters.setEffectValue(killedCreatures); + parameters.cast(gameHandler->spellEnv, target); + } +} + void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) { if(!attacker->alive() || !defender->alive()) // can be already dead @@ -1167,37 +1320,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & } if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) - { - // mechanics of Death Stare as in H3: - // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution - //original formula x = min(x, (gorgons_count + 9)/10); - - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon) / 100.0f; - vstd::amin(chanceToKill, 1); //cap at 100% - - std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); - - int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); - - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count - vstd::amin(staredCreatures, maxToKill); - - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); - if(staredCreatures) - { - //TODO: death stare was not originally available for multiple-hex attacks, but... - const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - parameters.setEffectValue(staredCreatures); - parameters.cast(gameHandler->spellEnv, target); - } - } + handleDeathStare(battle, ranged, attacker, defender); if(!defender->alive()) return; @@ -1274,6 +1397,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & // send empty event to client // temporary(?) workaround to force animations to trigger StacksInjured fakeEvent; + fakeEvent.battleID = battle.getBattle()->getBattleID(); gameHandler->sendAndApply(&fakeEvent); } @@ -1449,8 +1573,11 @@ bool BattleActionProcessor::makePlayerBattleAction(const CBattleInfoCallback & b else { auto active = battle.battleActiveUnit(); - if(!active && gameHandler->complain("No active unit in battle!")) + if(!active) + { + gameHandler->complain("No active unit in battle!"); return false; + } if (ba.isUnitAction() && ba.stackNumber != active->unitId()) { @@ -1460,8 +1587,11 @@ bool BattleActionProcessor::makePlayerBattleAction(const CBattleInfoCallback & b auto unitOwner = battle.battleGetOwner(active); - if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) + if(player != unitOwner) + { + gameHandler->complain("Can not make actions in battles you are not part of!"); return false; + } } return makeBattleActionImpl(battle, ba); diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 34e40aa29..6c28b5950 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -8,6 +8,7 @@ * */ #pragma once +#include "bonuses/BonusList.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,8 +44,13 @@ class BattleActionProcessor : boost::noncopyable void makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); void handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender); + + void handleDeathStare(const CBattleInfoCallback &battle, bool ranged, const CStack *attacker, const CStack *defender); + void handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender); - void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); + void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender); + + std::set getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender); // damage, drain life & fire shield; returns amount of drained life int64_t applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 0fba8f63a..94d585803 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -181,6 +181,7 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, // send empty event to client // temporary(?) workaround to force animations to trigger StacksInjured fakeEvent; + fakeEvent.battleID = battle.getBattle()->getBattleID(); gameHandler->sendAndApply(&fakeEvent); } @@ -583,10 +584,10 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CBattleInfoCallback & batt auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); for(auto b : bl) { - const CSpell * sp = b->subtype.as().toSpell(); - if(!sp) + if (!b->subtype.as().hasValue()) continue; + const CSpell * sp = b->subtype.as().toSpell(); const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); const int32_t level = ((val > 3) ? (val - 3) : val); @@ -663,7 +664,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c if (st->hasBonusOfType(BonusType::POISON)) { - std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::POISON))).And(Selector::type()(BonusType::STACK_HEALTH))); + std::shared_ptr b = st->getFirstBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::POISON))).And(Selector::type()(BonusType::STACK_HEALTH))); if (b) //TODO: what if not?... { bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); @@ -676,8 +677,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c } if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) { - const PlayerColor opponent = battle.otherPlayer(battle.battleGetOwner(st)); - const CGHeroInstance * opponentHero = battle.battleGetFightingHero(opponent); + const CGHeroInstance * opponentHero = battle.battleGetFightingHero(battle.otherSide(st->unitSide())); if(opponentHero) { ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); @@ -712,6 +712,11 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c } } BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); + bl.remove_if([](const Bonus * b) + { + return b->subtype.as() == SpellID::NONE; + }); + int side = *battle.playerToSide(st->unitOwner()); if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0) { diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index e32811505..c2c3e4be3 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -18,6 +18,7 @@ #include "../queries/QueriesProcessor.h" #include "../queries/BattleQueries.h" +#include "../../lib/CPlayerState.h" #include "../../lib/TerrainHandler.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/CObstacleInstance.h" @@ -63,6 +64,8 @@ void BattleProcessor::restartBattlePrimary(const BattleID & battleID, const CArm auto battle = gameHandler->gameState()->getBattle(battleID); auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + if(!lastBattleQuery) + lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[1].color)); assert(lastBattleQuery); @@ -122,14 +125,16 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm { for(auto bonus : attackerInfo->battleBonuses) { - GiveBonus giveBonus(GiveBonus::ETarget::HERO); - giveBonus.id = hero1->id.getNum(); + GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); + giveBonus.id = hero1->id; giveBonus.bonus = bonus; gameHandler->sendAndApply(&giveBonus); } } auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + if(!lastBattleQuery) + lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[1].color)); if (lastBattleQuery) { @@ -183,7 +188,13 @@ BattleID BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2] engageIntoBattle(bs.info->sides[1].color); auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->sides[0].color)); - bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); + if(!lastBattleQuery) + lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->sides[1].color)); + bool isDefenderHuman = bs.info->sides[1].color.isValidPlayer() && gameHandler->getPlayerState(bs.info->sides[1].color)->isHuman(); + bool isAttackerHuman = gameHandler->getPlayerState(bs.info->sides[0].color)->isHuman(); + + bool onlyOnePlayerHuman = isDefenderHuman != isAttackerHuman; + bs.info->replayAllowed = lastBattleQuery == nullptr && onlyOnePlayerHuman; gameHandler->sendAndApply(&bs); diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index c7423eeb2..952759fde 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -74,7 +74,7 @@ public: /// Applies results of a battle after potential levelup void battleAfterLevelUp(const BattleID & battleID, const BattleResult & result); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { } diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 230d2cf1b..e4b851331 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -17,6 +17,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CStack.h" +#include "../../lib/CPlayerState.h" #include "../../lib/GameSettings.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/IBattleState.h" @@ -46,26 +47,31 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, PlayerColor color = battle.sideToPlayer(sideInBattle); - for(const CStack * stConst : battle.battleGetAllStacks(true)) + auto allStacks = battle.battleGetStacksIf([color](const CStack * stack){ + + if (stack->summoned)//don't take into account temporary summoned stacks + return false; + + if(stack->unitOwner() != color) //remove only our stacks + return false; + + if (stack->isTurret()) + return false; + + return true; + }); + + for(const CStack * stConst : allStacks) { // Use const cast - in order to call non-const "takeResurrected" for proper calculation of casualties // TODO: better solution CStack * st = const_cast(stConst); - if(st->summoned) //don't take into account temporary summoned stacks - continue; - if(st->unitOwner() != color) //remove only our stacks - continue; - logGlobal->debug("Calculating casualties for %s", st->nodeName()); st->health.takeResurrected(); - if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) - { - logGlobal->debug("Ignored arrow towers stack."); - } - else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) + if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) { auto warMachine = st->unitType()->warMachine; @@ -79,7 +85,7 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, logGlobal->debug("War machine has been destroyed"); auto hero = dynamic_ptr_cast (army); if (hero) - removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); + removedWarMachines.push_back (ArtifactLocation(hero->id, hero->getArtPos(warMachine, true))); else logGlobal->error("War machine in army without hero"); } @@ -124,15 +130,9 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, StackLocation sl(army, st->unitSlot()); newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); } - else if(st->getCount() < army->getStackCount(st->unitSlot())) + else if(st->getCount() != army->getStackCount(st->unitSlot())) { - logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - else if(st->getCount() > army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); + logGlobal->debug("Stack size changed: %d -> %d units.", army->getStackCount(st->unitSlot()), st->getCount()); StackLocation sl(army, st->unitSlot()); newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); } @@ -256,6 +256,8 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]); auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); + if(!battleQuery) + battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(1))); if (!battleQuery) { logGlobal->error("Cannot find battle query!"); @@ -272,9 +274,15 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) finishingBattles[battle.getBattle()->getBattleID()] = std::make_unique(battle, *battleResult, queriedPlayers); // in battles against neutrals, 1st player can ask to replay battle manually - if (!battle.sideToPlayer(1).isValidPlayer()) + const auto * attackerPlayer = gameHandler->getPlayerState(battle.getBattle()->getSidePlayer(BattleSide::ATTACKER)); + const auto * defenderPlayer = gameHandler->getPlayerState(battle.getBattle()->getSidePlayer(BattleSide::DEFENDER)); + bool isAttackerHuman = attackerPlayer && attackerPlayer->isHuman(); + bool isDefenderHuman = defenderPlayer && defenderPlayer->isHuman(); + bool onlyOnePlayerHuman = isAttackerHuman != isDefenderHuman; + // in battles against neutrals attacker can ask to replay battle manually, additionally in battles against AI player human side can also ask for replay + if(onlyOnePlayerHuman) { - auto battleDialogQuery = std::make_shared(gameHandler, battle.getBattle()); + auto battleDialogQuery = std::make_shared(gameHandler, battle.getBattle(), battleQuery->result); battleResult->queryID = battleDialogQuery->queryID; gameHandler->queries->addQuery(battleDialogQuery); } @@ -299,6 +307,8 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) { auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); + if(!battleQuery) + battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(1))); if(!battleQuery) { logGlobal->trace("No battle query, battle end was confirmed by another player"); @@ -323,7 +333,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE); for(auto & spellId : battle.getBattle()->getUsedSpells(battle.otherSide(battleResult->winner))) { - auto spell = spellId.toSpell(VLC->spells()); + auto spell = spellId.toEntity(VLC->spells()); if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) cs.spells.insert(spell->getId()); } @@ -339,7 +349,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) if(slot != ArtifactPosition::PRE_FIRST) { arts.push_back(art); - ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); + ma->dst = ArtifactLocation(finishingBattle->winnerHero->id, slot); if(ArtifactUtils::isSlotBackpack(slot)) ma->askAssemble = false; gameHandler->sendAndApply(ma); @@ -353,8 +363,8 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto artSlot : artifactsWorn) { MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); + ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); + const CArtifactInstance * art = finishingBattle->loserHero->getArt(artSlot.first); if (art && !art->artType->isBig() && art->artType->getId() != ArtifactID::SPELLBOOK) // don't move war machines or locked arts (spellbook) @@ -366,9 +376,9 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) { //we assume that no big artifacts can be found MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, + ma.src = ArtifactLocation(finishingBattle->loserHero->id, ArtifactPosition(ArtifactPosition::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = ma.src.getArt(); + const CArtifactInstance * art = finishingBattle->loserHero->getArt(ArtifactPosition::BACKPACK_START + slotNumber); if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won { sendMoveArtifact(art, &ma); @@ -380,8 +390,9 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto artSlot : artifactsWorn) { MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); + ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); + ma.src.creature = finishingBattle->loserHero->findStack(finishingBattle->loserHero->commander); + const auto art = finishingBattle->loserHero->commander->getArt(artSlot.first); if (art && !art->artType->isBig()) { sendMoveArtifact(art, &ma); @@ -395,11 +406,12 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto armySlot : battle.battleGetArmyObject(loser)->stacks) { auto artifactsWorn = armySlot.second->artifactsWorn; - for (auto artSlot : artifactsWorn) + for(const auto & artSlot : artifactsWorn) { MoveArtifact ma; - ma.src = ArtifactLocation(armySlot.second, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); + ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); + ma.src.creature = finishingBattle->loserHero->findStack(finishingBattle->loserHero->commander); + const auto art = finishingBattle->loserHero->commander->getArt(artSlot.first); if (art && !art->artType->isBig()) { sendMoveArtifact(art, &ma); @@ -417,9 +429,11 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto art : arts) //TODO; separate function to display loot for various ojects? { - iw.components.emplace_back( - Component::EComponentType::ARTIFACT, art->artType->getId(), - art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : SpellID(0), 0); + if (art->artType->getId() == ArtifactID::SPELL_SCROLL) + iw.components.emplace_back(ComponentType::SPELL_SCROLL, art->getScrollSpellID()); + else + iw.components.emplace_back(ComponentType::ARTIFACT, art->artType->getId()); + if (iw.components.size() >= 14) { gameHandler->sendAndApply(&iw); @@ -458,10 +472,10 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) auto it = cs.spells.begin(); for (int i = 0; i < cs.spells.size(); i++, it++) { - iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); + iw.text.replaceName(*it); if (i == cs.spells.size() - 2) //we just added pre-last name iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); + iw.components.emplace_back(ComponentType::SPELL, *it); } gameHandler->sendAndApply(&iw); gameHandler->sendAndApply(&cs); @@ -490,14 +504,14 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) } //give exp if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) - gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); + gameHandler->giveExperience(finishingBattle->winnerHero, battleResult->exp[finishingBattle->winnerSide]); BattleResultAccepted raccepted; raccepted.battleID = battle.getBattle()->getBattleID(); - raccepted.heroResult[0].army = const_cast(battle.battleGetArmyObject(0)); - raccepted.heroResult[1].army = const_cast(battle.battleGetArmyObject(1)); - raccepted.heroResult[0].hero = const_cast(battle.battleGetFightingHero(0)); - raccepted.heroResult[1].hero = const_cast(battle.battleGetFightingHero(1)); + raccepted.heroResult[0].army = const_cast(battle.battleGetArmyObject(BattleSide::ATTACKER)); + raccepted.heroResult[1].army = const_cast(battle.battleGetArmyObject(BattleSide::DEFENDER)); + raccepted.heroResult[0].hero = const_cast(battle.battleGetFightingHero(BattleSide::ATTACKER)); + raccepted.heroResult[1].hero = const_cast(battle.battleGetFightingHero(BattleSide::DEFENDER)); raccepted.heroResult[0].exp = battleResult->exp[0]; raccepted.heroResult[1].exp = battleResult->exp[1]; raccepted.winnerSide = finishingBattle->winnerSide; @@ -581,7 +595,18 @@ void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, battleResult->result = resultType; battleResult->winner = victoriusSide; //surrendering side loses - for(const auto & st : battle.battleGetAllStacks(true)) //setting casualties + auto allStacks = battle.battleGetStacksIf([](const CStack * stack){ + + if (stack->summoned)//don't take into account temporary summoned stacks + return false; + + if (stack->isTurret()) + return false; + + return true; + }); + + for(const auto & st : allStacks) //setting casualties { si32 killed = st->getKilled(); if(killed > 0) diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index 7616c1775..5160c55e4 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -50,7 +50,7 @@ struct FinishingBattleHelper int remainingBattleQueriesCount; - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { h & winnerHero; h & loserHero; diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index f37f09f8a..053a5cd70 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -23,6 +23,7 @@ #include "../../lib/gameState/CGameState.h" #include "../../lib/gameState/TavernHeroesPool.h" #include "../../lib/gameState/TavernSlot.h" +#include "../../lib/GameSettings.h" HeroPoolProcessor::HeroPoolProcessor() : gameHandler(nullptr) @@ -50,8 +51,8 @@ TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, // try to find "better" slot to overwrite // we want to avoid overwriting retreated heroes when tavern still has slot with random hero // as well as avoid overwriting surrendered heroes if we can overwrite retreated hero - auto roleLeft = heroesPool->getSlotRole(HeroTypeID(heroes[0]->subID)); - auto roleRight = heroesPool->getSlotRole(HeroTypeID(heroes[1]->subID)); + auto roleLeft = heroesPool->getSlotRole(heroes[0]->getHeroType()); + auto roleRight = heroesPool->getSlotRole(heroes[1]->getHeroType()); if (roleLeft > roleRight) return TavernHeroSlot::RANDOM; @@ -73,7 +74,8 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid.setNum(hero->subID); + sah.hid = hero->getHeroType(); + sah.replenishPoints = false; gameHandler->sendAndApply(&sah); } @@ -84,9 +86,10 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid.setNum(hero->subID); + sah.hid = hero->getHeroType(); sah.army.clearSlots(); sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); + sah.replenishPoints = false; gameHandler->sendAndApply(&sah); } @@ -98,20 +101,22 @@ void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroS sah.roleID = TavernSlotRole::NONE; sah.slotID = slot; sah.hid = HeroTypeID::NONE; + sah.replenishPoints = false; gameHandler->sendAndApply(&sah); } -void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveArmy) +void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveArmy, const HeroTypeID & nextHero) { SetAvailableHero sah; sah.player = color; sah.slotID = slot; + sah.replenishPoints = true; - CGHeroInstance *newHero = pickHeroFor(needNativeHero, color); + CGHeroInstance *newHero = (nextHero == HeroTypeID::NONE) ? pickHeroFor(needNativeHero, color) : gameHandler->gameState()->heroesPool->unusedHeroesFromPool()[nextHero]; if (newHero) { - sah.hid.setNum(newHero->subID); + sah.hid = newHero->getHeroType(); if (giveArmy) { @@ -129,6 +134,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe { sah.hid = HeroTypeID::NONE; } + gameHandler->sendAndApply(&sah); } @@ -140,11 +146,12 @@ void HeroPoolProcessor::onNewWeek(const PlayerColor & color) selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true); } -bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTypeID & heroToRecruit, const PlayerColor & player) +bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTypeID & heroToRecruit, const PlayerColor & player, const HeroTypeID & nextHero) { const PlayerState * playerState = gameHandler->getPlayerState(player); const CGObjectInstance * mapObject = gameHandler->getObj(objectID); const CGTownInstance * town = gameHandler->getTown(objectID); + const auto & heroesPool = gameHandler->gameState()->heroesPool; if (!mapObject && gameHandler->complain("Invalid map object!")) return false; @@ -161,6 +168,18 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy if (gameHandler->getHeroCount(player, true) >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP) && gameHandler->complain("Cannot hire hero, too many heroes garrizoned and wandering already!")) return false; + if (nextHero != HeroTypeID::NONE) // player attempts to invite next hero + { + if(!VLC->settings()->getBoolean(EGameSettings::HEROES_TAVERN_INVITE) && gameHandler->complain("Inviting heroes not allowed!")) + return false; + + if(!heroesPool->unusedHeroesFromPool().count(nextHero) && gameHandler->complain("Cannot invite specified hero!")) + return false; + + if(!heroesPool->isHeroAvailableFor(nextHero, player) && gameHandler->complain("Cannot invite specified hero!")) + return false; + } + if(town) //tavern in town { if(gameHandler->getPlayerRelations(mapObject->tempOwner, player) == PlayerRelations::ENEMIES && gameHandler->complain("Can't buy hero in enemy town!")) @@ -187,13 +206,13 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy return false; } - auto recruitableHeroes = gameHandler->gameState()->heroesPool->getHeroesFor(player); + auto recruitableHeroes = heroesPool->getHeroesFor(player); const CGHeroInstance * recruitedHero = nullptr; for(const auto & hero : recruitableHeroes) { - if(hero->subID == heroToRecruit) + if(hero->getHeroType() == heroToRecruit) recruitedHero = hero; } @@ -206,7 +225,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy HeroRecruited hr; hr.tid = mapObject->id; - hr.hid.setNum(recruitedHero->subID); + hr.hid = recruitedHero->getHeroType(); hr.player = player; hr.tile = recruitedHero->convertFromVisitablePos(targetPos ); if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->boat) @@ -221,9 +240,9 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy gameHandler->sendAndApply(&hr); if(recruitableHeroes[0] == recruitedHero) - selectNewHeroForSlot(player, TavernHeroSlot::NATIVE, false, false); + selectNewHeroForSlot(player, TavernHeroSlot::NATIVE, false, false, nextHero); else - selectNewHeroForSlot(player, TavernHeroSlot::RANDOM, false, false); + selectNewHeroForSlot(player, TavernHeroSlot::RANDOM, false, false, nextHero); gameHandler->giveResource(player, EGameResID::GOLD, -GameConstants::HERO_GOLD_COST); @@ -242,13 +261,13 @@ std::vector HeroPoolProcessor::findAvailableClassesFor(const const auto & heroesPool = gameHandler->gameState()->heroesPool; FactionID factionID = gameHandler->getPlayerSettings(player)->castle; - for(auto & elem : heroesPool->unusedHeroesFromPool()) + for(const auto & elem : heroesPool->unusedHeroesFromPool()) { if (vstd::contains(result, elem.second->type->heroClass)) continue; bool heroAvailable = heroesPool->isHeroAvailableFor(elem.first, player); - bool heroClassBanned = elem.second->type->heroClass->selectionProbability[factionID] == 0; + bool heroClassBanned = elem.second->type->heroClass->tavernProbability(factionID) == 0; if(heroAvailable && !heroClassBanned) result.push_back(elem.second->type->heroClass); @@ -263,7 +282,7 @@ std::vector HeroPoolProcessor::findAvailableHeroesFor(const Pl const auto & heroesPool = gameHandler->gameState()->heroesPool; - for(auto & elem : heroesPool->unusedHeroesFromPool()) + for(const auto & elem : heroesPool->unusedHeroesFromPool()) { assert(!vstd::contains(result, elem.second)); @@ -321,13 +340,13 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo int totalWeight = 0; for(const auto & heroClass : possibleClasses) - totalWeight += heroClass->selectionProbability.at(factionID); + totalWeight += heroClass->tavernProbability(factionID); int roll = getRandomGenerator(player).nextInt(totalWeight - 1); for(const auto & heroClass : possibleClasses) { - roll -= heroClass->selectionProbability.at(factionID); + roll -= heroClass->tavernProbability(factionID); if(roll < 0) return heroClass; } @@ -351,6 +370,17 @@ CGHeroInstance * HeroPoolProcessor::pickHeroFor(bool isNative, const PlayerColor return *RandomGeneratorUtil::nextItem(possibleHeroes, getRandomGenerator(player)); } +CRandomGenerator & HeroPoolProcessor::getHeroSkillsRandomGenerator(const HeroTypeID & hero) +{ + if (heroSeed.count(hero) == 0) + { + int seed = gameHandler->getRandomGenerator().nextInt(); + heroSeed.emplace(hero, std::make_unique(seed)); + } + + return *heroSeed.at(hero); +} + CRandomGenerator & HeroPoolProcessor::getRandomGenerator(const PlayerColor & player) { if (playerSeed.count(player) == 0) diff --git a/server/processors/HeroPoolProcessor.h b/server/processors/HeroPoolProcessor.h index 3cf48ca5f..941801dd0 100644 --- a/server/processors/HeroPoolProcessor.h +++ b/server/processors/HeroPoolProcessor.h @@ -9,6 +9,8 @@ */ #pragma once +#include "../../lib/constants/EntityIdentifiers.h" + VCMI_LIB_NAMESPACE_BEGIN enum class TavernHeroSlot : int8_t; @@ -29,8 +31,11 @@ class HeroPoolProcessor : boost::noncopyable /// per-player random generators std::map> playerSeed; + /// per-hero random generators used to randomize skills + std::map> heroSeed; + void clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot); - void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveStartingArmy); + void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveStartingArmy, const HeroTypeID & nextHero = HeroTypeID::NONE); std::vector findAvailableClassesFor(const PlayerColor & player) const; std::vector findAvailableHeroesFor(const PlayerColor & player, const CHeroClass * heroClass) const; @@ -54,12 +59,15 @@ public: void onNewWeek(const PlayerColor & color); - /// Incoming net pack handling - bool hireHero(const ObjectInstanceID & objectID, const HeroTypeID & hid, const PlayerColor & player); + CRandomGenerator & getHeroSkillsRandomGenerator(const HeroTypeID & hero); - template void serialize(Handler &h, const int version) + /// Incoming net pack handling + bool hireHero(const ObjectInstanceID & objectID, const HeroTypeID & hid, const PlayerColor & player, const HeroTypeID & nextHero); + + template void serialize(Handler &h) { // h & gameHandler; // FIXME: make this work instead of using deserializationFix in gameHandler h & playerSeed; + h & heroSeed; } }; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index c2d319380..65c18f9e9 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -13,7 +13,6 @@ #include "../CGameHandler.h" #include "../CVCMIServer.h" -#include "../../lib/serializer/Connection.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/modding/IdentifierStorage.h" @@ -22,10 +21,13 @@ #include "../../lib/StartInfo.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/modding/IdentifierStorage.h" #include "../../lib/modding/ModScope.h" +#include "../../lib/mapping/CMap.h" #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/StackLocation.h" +#include "../../lib/serializer/Connection.h" PlayerMessageProcessor::PlayerMessageProcessor() :gameHandler(nullptr) @@ -61,10 +63,7 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st std::vector words; boost::split(words, message, boost::is_any_of(" ")); - bool isHost = false; - for(auto & c : gameHandler->connections[player]) - if(gameHandler->gameLobby()->isClientHost(c->connectionID)) - isHost = true; + bool isHost = gameHandler->gameLobby()->isPlayerHost(player); if(!isHost || words.size() < 2 || words[0] != "game") return false; @@ -136,11 +135,11 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns ///Give hero spellbook if (!hero->hasSpellbook()) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::SPELLBOOK).toArtifact(), ArtifactPosition::SPELLBOOK); ///Give all spells with bonus (to allow banned spells) - GiveBonus giveBonus(GiveBonus::ETarget::HERO); - giveBonus.id = hero->id.getNum(); + GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); + giveBonus.id = hero->id; giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, BonusSourceID()); //start with level 0 to skip abilities for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) @@ -214,11 +213,11 @@ void PlayerMessageProcessor::cheatGiveMachines(PlayerColor player, const CGHeroI return; if (!hero->getArt(ArtifactPosition::MACH1)) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::BALLISTA], ArtifactPosition::MACH1); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::BALLISTA).toArtifact(), ArtifactPosition::MACH1); if (!hero->getArt(ArtifactPosition::MACH2)) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::AMMO_CART], ArtifactPosition::MACH2); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::AMMO_CART).toArtifact(), ArtifactPosition::MACH2); if (!hero->getArt(ArtifactPosition::MACH3)) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(ArtifactID::FIRST_AID_TENT).toArtifact(), ArtifactPosition::MACH3); } void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector words) @@ -232,7 +231,7 @@ void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHero { auto artID = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", word, false); if(artID && VLC->arth->objects[*artID]) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[*artID], ArtifactPosition::FIRST_AVAILABLE); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(*artID).toArtifact(), ArtifactPosition::FIRST_AVAILABLE); } } else @@ -240,7 +239,7 @@ void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHero for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods { if(VLC->arth->objects[g]->canBePutAt(hero)) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE); + gameHandler->giveHeroNewArtifact(hero, ArtifactID(g).toArtifact(), ArtifactPosition::FIRST_AVAILABLE); } } } @@ -260,7 +259,7 @@ void PlayerMessageProcessor::cheatLevelup(PlayerColor player, const CGHeroInstan levelsToGain = 1; } - gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level)); + gameHandler->giveExperience(hero, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level)); } void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector words) @@ -279,7 +278,7 @@ void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroIns expAmountProcessed = 10000; } - gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expAmountProcessed); + gameHandler->giveExperience(hero, expAmountProcessed); } void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector words) @@ -289,23 +288,35 @@ void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInsta SetMovePoints smp; smp.hid = hero->id; + bool unlimited = false; try { - smp.val = std::stol(words.at(0));; + smp.val = std::stol(words.at(0)); } catch(std::logic_error&) { smp.val = 1000000; + unlimited = true; } gameHandler->sendAndApply(&smp); - GiveBonus gb(GiveBonus::ETarget::HERO); + GiveBonus gb(GiveBonus::ETarget::OBJECT); gb.bonus.type = BonusType::FREE_SHIP_BOARDING; - gb.bonus.duration = BonusDuration::ONE_DAY; + gb.bonus.duration = unlimited ? BonusDuration::PERMANENT : BonusDuration::ONE_DAY; gb.bonus.source = BonusSource::OTHER; - gb.id = hero->id.getNum(); + gb.id = hero->id; gameHandler->giveHeroBonus(&gb); + + if(unlimited) + { + GiveBonus gb(GiveBonus::ETarget::OBJECT); + gb.bonus.type = BonusType::UNLIMITED_MOVEMENT; + gb.bonus.duration = BonusDuration::PERMANENT; + gb.bonus.source = BonusSource::OTHER; + gb.id = hero->id; + gameHandler->giveHeroBonus(&gb); + } } void PlayerMessageProcessor::cheatResources(PlayerColor player, std::vector words) @@ -313,7 +324,7 @@ void PlayerMessageProcessor::cheatResources(PlayerColor player, std::vectorsendAndApply(&fc); } +void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player) +{ + TeamState *t = gameHandler->gameState()->getPlayerTeam(player); + + for(auto & obj : gameHandler->gameState()->map->objects) + { + if(obj && obj->ID == Obj::OBELISK && !obj->wasVisited(player)) + { + gameHandler->setObjPropertyID(obj->id, ObjProperty::OBELISK_VISITED, t->id); + for(const auto & color : t->players) + { + gameHandler->setObjPropertyID(obj->id, ObjProperty::VISITED, color); + + PlayerCheated pc; + pc.player = color; + gameHandler->sendAndApply(&pc); + } + } + } +} + +void PlayerMessageProcessor::cheatMaxLuck(PlayerColor player, const CGHeroInstance * hero) +{ + if (!hero) + return; + + GiveBonus gb; + gb.bonus = Bonus(BonusDuration::PERMANENT, BonusType::MAX_LUCK, BonusSource::OTHER, 0, BonusSourceID(Obj(Obj::NO_OBJ))); + gb.id = hero->id; + + gameHandler->giveHeroBonus(&gb); +} + +void PlayerMessageProcessor::cheatFly(PlayerColor player, const CGHeroInstance * hero) +{ + if (!hero) + return; + + GiveBonus gb; + gb.bonus = Bonus(BonusDuration::PERMANENT, BonusType::FLYING_MOVEMENT, BonusSource::OTHER, 0, BonusSourceID(Obj(Obj::NO_OBJ))); + gb.id = hero->id; + + gameHandler->giveHeroBonus(&gb); +} + +void PlayerMessageProcessor::cheatMaxMorale(PlayerColor player, const CGHeroInstance * hero) +{ + if (!hero) + return; + + GiveBonus gb; + gb.bonus = Bonus(BonusDuration::PERMANENT, BonusType::MAX_MORALE, BonusSource::OTHER, 0, BonusSourceID(Obj(Obj::NO_OBJ))); + gb.id = hero->id; + + gameHandler->giveHeroBonus(&gb); +} + bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerColor player, ObjectInstanceID currObj) { std::vector words; boost::split(words, cheat, boost::is_any_of("\t\r\n ")); - if (words.empty()) + if (words.empty() || !gameHandler->getStartInfo()->extraOptionsInfo.cheatsAllowed) return false; //Make cheat name case-insensitive, but keep words/parameters (e.g. creature name) as it @@ -383,19 +451,23 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo "vcmimelkor", "vcmilose", "nwcbluepill", "vcmisilmaril", "vcmiwin", "nwcredpill", "vcmieagles", "vcmimap", "nwcwhatisthematrix", - "vcmiungoliant", "vcmihidemap", "nwcignoranceisbliss" + "vcmiungoliant", "vcmihidemap", "nwcignoranceisbliss", + "vcmiobelisk", "nwcoracle" }; std::vector heroTargetedCheats = { - "vcmiainur", "vcmiarchangel", "nwctrinity", - "vcmiangband", "vcmiblackknight", "nwcagents", - "vcmiglaurung", "vcmicrystal", "vcmiazure", - "vcmifaerie", "vcmiarmy", "vcminissi", - "vcmiistari", "vcmispells", "nwcthereisnospoon", - "vcminoldor", "vcmimachines", "nwclotsofguns", - "vcmiglorfindel", "vcmilevel", "nwcneo", - "vcminahar", "vcmimove", "nwcnebuchadnezzar", - "vcmiforgeofnoldorking", "vcmiartifacts", - "vcmiolorin", "vcmiexp", + "vcmiainur", "vcmiarchangel", "nwctrinity", + "vcmiangband", "vcmiblackknight", "nwcagents", + "vcmiglaurung", "vcmicrystal", "vcmiazure", + "vcmifaerie", "vcmiarmy", "vcminissi", + "vcmiistari", "vcmispells", "nwcthereisnospoon", + "vcminoldor", "vcmimachines", "nwclotsofguns", + "vcmiglorfindel", "vcmilevel", "nwcneo", + "vcminahar", "vcmimove", "nwcnebuchadnezzar", + "vcmiforgeofnoldorking", "vcmiartifacts", + "vcmiolorin", "vcmiexp", + "vcmiluck", "nwcfollowthewhiterabbit", + "vcmimorale", "nwcmorpheus", + "vcmigod", "nwctheone" }; if (!vstd::contains(townTargetedCheats, cheatName) && !vstd::contains(playerTargetedCheats, cheatName) && !vstd::contains(heroTargetedCheats, cheatName)) @@ -469,60 +541,77 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla const auto & doCheatDefeat = [&]() { cheatDefeat(player); }; const auto & doCheatMapReveal = [&]() { cheatMapReveal(player, true); }; const auto & doCheatMapHide = [&]() { cheatMapReveal(player, false); }; + const auto & doCheatRevealPuzzle = [&]() { cheatPuzzleReveal(player); }; + const auto & doCheatMaxLuck = [&]() { cheatMaxLuck(player, hero); }; + const auto & doCheatMaxMorale = [&]() { cheatMaxMorale(player, hero); }; + const auto & doCheatTheOne = [&]() + { + if(!hero) + return; + cheatMapReveal(player, true); + cheatGiveArmy(player, hero, { "archangel", "5" }); + cheatMovement(player, hero, { }); + cheatFly(player, hero); + }; // Unimplemented H3 cheats: - // nwcfollowthewhiterabbit - The currently selected hero permanently gains maximum luck. - // nwcmorpheus - The currently selected hero permanently gains maximum morale. - // nwcoracle - The puzzle map is permanently revealed. // nwcphisherprice - Changes and brightens the game colors. std::map> callbacks = { - {"vcmiainur", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, - {"nwctrinity", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, - {"vcmiangband", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, - {"vcmiglaurung", [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} }, - {"vcmiarchangel", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, - {"nwcagents", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, - {"vcmiblackknight", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, - {"vcmicrystal", [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} }, - {"vcmiazure", [&] () {doCheatGiveArmyFixed({ "azureDragon", "5000" });} }, - {"vcmifaerie", [&] () {doCheatGiveArmyFixed({ "fairieDragon", "5000" });} }, - {"vcmiarmy", doCheatGiveArmyCustom }, - {"vcminissi", doCheatGiveArmyCustom }, - {"vcmiistari", doCheatGiveSpells }, - {"vcmispells", doCheatGiveSpells }, - {"nwcthereisnospoon", doCheatGiveSpells }, - {"vcmiarmenelos", doCheatBuildTown }, - {"vcmibuild", doCheatBuildTown }, - {"nwczion", doCheatBuildTown }, - {"vcminoldor", doCheatGiveMachines }, - {"vcmimachines", doCheatGiveMachines }, - {"nwclotsofguns", doCheatGiveMachines }, - {"vcmiforgeofnoldorking", doCheatGiveArtifacts }, - {"vcmiartifacts", doCheatGiveArtifacts }, - {"vcmiglorfindel", doCheatLevelup }, - {"vcmilevel", doCheatLevelup }, - {"nwcneo", doCheatLevelup }, - {"vcmiolorin", doCheatExperience }, - {"vcmiexp", doCheatExperience }, - {"vcminahar", doCheatMovement }, - {"vcmimove", doCheatMovement }, - {"nwcnebuchadnezzar", doCheatMovement }, - {"vcmiformenos", doCheatResources }, - {"vcmiresources", doCheatResources }, - {"nwctheconstruct", doCheatResources }, - {"nwcbluepill", doCheatDefeat }, - {"vcmimelkor", doCheatDefeat }, - {"vcmilose", doCheatDefeat }, - {"nwcredpill", doCheatVictory }, - {"vcmisilmaril", doCheatVictory }, - {"vcmiwin", doCheatVictory }, - {"nwcwhatisthematrix", doCheatMapReveal }, - {"vcmieagles", doCheatMapReveal }, - {"vcmimap", doCheatMapReveal }, - {"vcmiungoliant", doCheatMapHide }, - {"vcmihidemap", doCheatMapHide }, - {"nwcignoranceisbliss", doCheatMapHide }, + {"vcmiainur", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, + {"nwctrinity", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, + {"vcmiangband", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, + {"vcmiglaurung", [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} }, + {"vcmiarchangel", [&] () {doCheatGiveArmyFixed({ "archangel", "5" });} }, + {"nwcagents", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, + {"vcmiblackknight", [&] () {doCheatGiveArmyFixed({ "blackKnight", "10" });} }, + {"vcmicrystal", [&] () {doCheatGiveArmyFixed({ "crystalDragon", "5000" });} }, + {"vcmiazure", [&] () {doCheatGiveArmyFixed({ "azureDragon", "5000" });} }, + {"vcmifaerie", [&] () {doCheatGiveArmyFixed({ "fairieDragon", "5000" });} }, + {"vcmiarmy", doCheatGiveArmyCustom }, + {"vcminissi", doCheatGiveArmyCustom }, + {"vcmiistari", doCheatGiveSpells }, + {"vcmispells", doCheatGiveSpells }, + {"nwcthereisnospoon", doCheatGiveSpells }, + {"vcmiarmenelos", doCheatBuildTown }, + {"vcmibuild", doCheatBuildTown }, + {"nwczion", doCheatBuildTown }, + {"vcminoldor", doCheatGiveMachines }, + {"vcmimachines", doCheatGiveMachines }, + {"nwclotsofguns", doCheatGiveMachines }, + {"vcmiforgeofnoldorking", doCheatGiveArtifacts }, + {"vcmiartifacts", doCheatGiveArtifacts }, + {"vcmiglorfindel", doCheatLevelup }, + {"vcmilevel", doCheatLevelup }, + {"nwcneo", doCheatLevelup }, + {"vcmiolorin", doCheatExperience }, + {"vcmiexp", doCheatExperience }, + {"vcminahar", doCheatMovement }, + {"vcmimove", doCheatMovement }, + {"nwcnebuchadnezzar", doCheatMovement }, + {"vcmiformenos", doCheatResources }, + {"vcmiresources", doCheatResources }, + {"nwctheconstruct", doCheatResources }, + {"nwcbluepill", doCheatDefeat }, + {"vcmimelkor", doCheatDefeat }, + {"vcmilose", doCheatDefeat }, + {"nwcredpill", doCheatVictory }, + {"vcmisilmaril", doCheatVictory }, + {"vcmiwin", doCheatVictory }, + {"nwcwhatisthematrix", doCheatMapReveal }, + {"vcmieagles", doCheatMapReveal }, + {"vcmimap", doCheatMapReveal }, + {"vcmiungoliant", doCheatMapHide }, + {"vcmihidemap", doCheatMapHide }, + {"nwcignoranceisbliss", doCheatMapHide }, + {"vcmiobelisk", doCheatRevealPuzzle }, + {"nwcoracle", doCheatRevealPuzzle }, + {"vcmiluck", doCheatMaxLuck }, + {"nwcfollowthewhiterabbit", doCheatMaxLuck }, + {"vcmimorale", doCheatMaxMorale }, + {"nwcmorpheus", doCheatMaxMorale }, + {"vcmigod", doCheatTheOne }, + {"nwctheone", doCheatTheOne }, }; assert(callbacks.count(cheatName)); diff --git a/server/processors/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h index 47d2a8a4a..9d4fd36dc 100644 --- a/server/processors/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -37,6 +37,10 @@ class PlayerMessageProcessor void cheatVictory(PlayerColor player); void cheatDefeat(PlayerColor player); void cheatMapReveal(PlayerColor player, bool reveal); + void cheatPuzzleReveal(PlayerColor player); + void cheatMaxLuck(PlayerColor player, const CGHeroInstance * hero); + void cheatMaxMorale(PlayerColor player, const CGHeroInstance * hero); + void cheatFly(PlayerColor player, const CGHeroInstance * hero); public: CGameHandler * gameHandler; @@ -56,7 +60,7 @@ public: /// Send message from specific player to all other players void broadcastMessage(PlayerColor playerSender, const std::string & message); - template void serialize(Handler &h, const int version) + template void serialize(Handler &h) { } }; diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 242055dc0..2266036a5 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" #include "TurnOrderProcessor.h" +#include "PlayerMessageProcessor.h" #include "../queries/QueriesProcessor.h" #include "../queries/MapQueries.h" @@ -35,9 +36,9 @@ int TurnOrderProcessor::simturnsTurnsMinLimit() const return gameHandler->getStartInfo()->simturnsInfo.requiredTurns; } -void TurnOrderProcessor::updateContactStatus() +std::vector TurnOrderProcessor::computeContactStatus() const { - blockedContacts.clear(); + std::vector result; assert(actedPlayers.empty()); assert(actingPlayers.empty()); @@ -50,9 +51,40 @@ void TurnOrderProcessor::updateContactStatus() continue; if (computeCanActSimultaneously(left, right)) - blockedContacts.push_back({left, right}); + result.push_back({left, right}); } } + return result; +} + +void TurnOrderProcessor::updateAndNotifyContactStatus() +{ + auto newBlockedContacts = computeContactStatus(); + + if (newBlockedContacts.empty()) + { + // Simturns between all players have ended - send single global notification + if (!blockedContacts.empty()) + gameHandler->playerMessages->broadcastSystemMessage("Simultaneous turns have ended"); + } + else + { + // Simturns between some players have ended - notify each pair + for (auto const & contact : blockedContacts) + { + if (vstd::contains(newBlockedContacts, contact)) + continue; + + MetaString message; + message.appendRawString("Simultaneous turns between players %s and %s have ended"); // FIXME: we should send MetaString itself and localize it on client side + message.replaceName(contact.a); + message.replaceName(contact.b); + + gameHandler->playerMessages->broadcastSystemMessage(message.toString()); + } + } + + blockedContacts = newBlockedContacts; } bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const @@ -74,6 +106,8 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c { CPathsInfo out(mapSize, hero); auto config = std::make_shared(out, gameHandler->gameState(), hero); + config->options.ignoreGuards = true; + config->options.turnLimit = 1; CPathfinder pathfinder(gameHandler->gameState(), config); pathfinder.calculatePaths(); @@ -88,6 +122,8 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c { CPathsInfo out(mapSize, hero); auto config = std::make_shared(out, gameHandler->gameState(), hero); + config->options.ignoreGuards = true; + config->options.turnLimit = 1; CPathfinder pathfinder(gameHandler->gameState(), config); pathfinder.calculatePaths(); @@ -163,7 +199,7 @@ bool TurnOrderProcessor::mustActBefore(PlayerColor left, PlayerColor right) cons if (!leftInfo->isHuman() && rightInfo->isHuman()) return false; - return left < right; + return false; } bool TurnOrderProcessor::canStartTurn(PlayerColor which) const @@ -196,12 +232,15 @@ void TurnOrderProcessor::doStartNewDay() } if(!activePlayer) - gameHandler->gameLobby()->setState(EServerState::GAMEPLAY_ENDED); + { + gameHandler->gameLobby()->setState(EServerState::SHUTDOWN); + return; + } std::swap(actedPlayers, awaitingPlayers); gameHandler->onNewTurn(); - updateContactStatus(); + updateAndNotifyContactStatus(); tryStartTurnsForPlayers(); } @@ -263,8 +302,6 @@ void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which) if (actingPlayers.empty()) doStartNewDay(); - - assert(!actingPlayers.empty()); } bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) @@ -300,7 +337,7 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) void TurnOrderProcessor::onGameStarted() { if (actingPlayers.empty()) - updateContactStatus(); + blockedContacts = computeContactStatus(); // this may be game load - send notification to players that they can act auto actingPlayersCopy = actingPlayers; diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index 378ed007f..8be868599 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -28,7 +28,7 @@ class TurnOrderProcessor : boost::noncopyable } template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & a; h & b; @@ -62,7 +62,9 @@ class TurnOrderProcessor : boost::noncopyable /// Starts turn for all players that can start turn void tryStartTurnsForPlayers(); - void updateContactStatus(); + void updateAndNotifyContactStatus(); + + std::vector computeContactStatus() const; void doStartNewDay(); void doStartPlayerTurn(PlayerColor which); @@ -90,7 +92,7 @@ public: void onGameStarted(); template - void serialize(Handler & h, const int version) + void serialize(Handler & h) { h & blockedContacts; h & awaitingPlayers; diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index a49e55be3..f97b0663f 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -16,14 +16,18 @@ #include "../battles/BattleProcessor.h" #include "../../lib/battle/IBattleState.h" +#include "../../lib/battle/SideInBattle.h" +#include "../../lib/CPlayerState.h" #include "../../lib/mapObjects/CGObjectInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/networkPacks/PacksForServer.h" +#include "../../lib/serializer/Cast.h" void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { assert(result); - if(result) + if(result && !isAiVsHuman) objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); } @@ -34,6 +38,8 @@ CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): belligerents[0] = bi->getSideArmy(0); belligerents[1] = bi->getSideArmy(1); + isAiVsHuman = bi->getSidePlayer(1).isValidPlayer() && gh->getPlayerState(bi->getSidePlayer(0))->isHuman() != gh->getPlayerState(bi->getSidePlayer(1))->isHuman(); + addPlayer(bi->getSidePlayer(0)); addPlayer(bi->getSidePlayer(1)); } @@ -46,8 +52,13 @@ CBattleQuery::CBattleQuery(CGameHandler * owner): bool CBattleQuery::blocksPack(const CPack * pack) const { - const char * name = typeid(*pack).name(); - return strcmp(name, typeid(MakeAction).name()) != 0; + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + return true; } void CBattleQuery::onRemoval(PlayerColor color) @@ -67,9 +78,10 @@ void CBattleQuery::onExposure(QueryPtr topQuery) owner->popQuery(*this); } -CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi): +CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi, std::optional Br): CDialogQuery(owner), - bi(bi) + bi(bi), + result(Br) { addPlayer(bi->getSidePlayer(0)); addPlayer(bi->getSidePlayer(1)); @@ -77,6 +89,9 @@ CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * void CBattleDialogQuery::onRemoval(PlayerColor color) { + if (!gh->getPlayerState(color)->isHuman()) + return; + assert(answer); if(*answer == 1) { @@ -93,6 +108,13 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) } else { + auto hero = bi->getSideHero(BattleSide::ATTACKER); + auto visitingObj = bi->getDefendedTown() ? bi->getDefendedTown() : gh->getVisitingObject(hero); + bool isAiVsHuman = bi->getSidePlayer(1).isValidPlayer() && gh->getPlayerState(bi->getSidePlayer(0))->isHuman() != gh->getPlayerState(bi->getSidePlayer(1))->isHuman(); + gh->battles->endBattleConfirm(bi->getBattleID()); + + if(visitingObj && result && isAiVsHuman) + visitingObj->battleFinished(hero, *result); } } diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index 4d2fb10fe..84064a81d 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class IBattleInfo; +struct SideInBattle; VCMI_LIB_NAMESPACE_END class CBattleQuery : public CQuery @@ -22,23 +23,24 @@ public: std::array belligerents; std::array initialHeroMana; + bool isAiVsHuman; BattleID battleID; std::optional result; CBattleQuery(CGameHandler * owner); CBattleQuery(CGameHandler * owner, const IBattleInfo * Bi); //TODO - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; - virtual void onExposure(QueryPtr topQuery) override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + bool blocksPack(const CPack *pack) const override; + void onRemoval(PlayerColor color) override; + void onExposure(QueryPtr topQuery) override; }; class CBattleDialogQuery : public CDialogQuery { public: - CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * Bi); + CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * Bi, std::optional Br); const IBattleInfo * bi; - - virtual void onRemoval(PlayerColor color) override; + std::optional result; + void onRemoval(PlayerColor color) override; }; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index ad23dcc25..0580b75ed 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -49,8 +49,6 @@ CQuery::CQuery(CGameHandler * gameHandler) : owner(gameHandler->queries.get()) , gh(gameHandler) { - boost::unique_lock l(QueriesProcessor::mx); - static QueryID QID = QueryID(0); queryID = ++QID; diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h index cf6e18254..5773ecadd 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -68,8 +68,8 @@ class CDialogQuery : public CQuery { public: CDialogQuery(CGameHandler * owner); - virtual bool endsByPlayerAnswer() const override; - virtual bool blocksPack(const CPack *pack) const override; + bool endsByPlayerAnswer() const override; + bool blocksPack(const CPack *pack) const override; void setReply(std::optional reply) override; protected: std::optional answer; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index e2561adcf..424404fe8 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -17,20 +17,6 @@ #include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/serializer/Cast.h" -struct GetEngagedHeroIds -{ - std::optional operator()(const ConstTransitivePtr & h) const - { - return h->id; - } - std::optional operator()(const ConstTransitivePtr & s) const - { - if(s->armyObj && s->armyObj->ID == Obj::HERO) - return s->armyObj->id; - return std::optional(); - } -}; - TimerPauseQuery::TimerPauseQuery(CGameHandler * owner, PlayerColor player): CQuery(owner) { @@ -127,12 +113,12 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const if(auto arts = dynamic_ptr_cast(pack)) { - if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder)) - if(!vstd::contains(ourIds, *id1)) + if(auto id1 = arts->src.artHolder) + if(!vstd::contains(ourIds, id1)) return true; - if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder)) - if(!vstd::contains(ourIds, *id2)) + if(auto id2 = arts->dst.artHolder) + if(!vstd::contains(ourIds, id2)) return true; return false; } @@ -144,8 +130,8 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const if(auto art = dynamic_ptr_cast(pack)) { - if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder)) - return !vstd::contains(ourIds, *id); + if(auto id = art->al.artHolder) + return !vstd::contains(ourIds, id); } if(auto dismiss = dynamic_ptr_cast(pack)) @@ -214,6 +200,9 @@ bool OpenWindowQuery::blocksPack(const CPack *pack) const if(dynamic_ptr_cast(pack) != nullptr) return false; + if(dynamic_ptr_cast(pack) != nullptr) + return false; + if(dynamic_ptr_cast(pack)) return false; diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index 5668dad02..7a6cabf3c 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -46,9 +46,9 @@ public: CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile); - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; - virtual void onExposure(QueryPtr topQuery) override; + bool blocksPack(const CPack *pack) const override; + void onRemoval(PlayerColor color) override; + void onExposure(QueryPtr topQuery) override; }; //Created when hero attempts move and something happens @@ -60,11 +60,11 @@ public: bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated const CGHeroInstance *hero; - virtual void onExposure(QueryPtr topQuery) override; + void onExposure(QueryPtr topQuery) override; CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); - virtual void onAdding(PlayerColor color) override; - virtual void onRemoval(PlayerColor color) override; + void onAdding(PlayerColor color) override; + void onRemoval(PlayerColor color) override; }; class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs @@ -73,8 +73,8 @@ public: std::array exchangingArmies; CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + bool blocksPack(const CPack *pack) const override; }; //yes/no and component selection dialogs @@ -85,7 +85,7 @@ public: CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; }; class OpenWindowQuery : public CDialogQuery @@ -105,7 +105,7 @@ public: CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; }; class CHeroLevelUpDialogQuery : public CDialogQuery @@ -113,8 +113,8 @@ class CHeroLevelUpDialogQuery : public CDialogQuery public: CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void onRemoval(PlayerColor color) override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; HeroLevelUp hlu; const CGHeroInstance * hero; @@ -125,8 +125,8 @@ class CCommanderLevelUpDialogQuery : public CDialogQuery public: CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + void onRemoval(PlayerColor color) override; + void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; CommanderLevelUp clu; const CGHeroInstance * hero; diff --git a/server/queries/QueriesProcessor.cpp b/server/queries/QueriesProcessor.cpp index 259e33a3e..73918cbf4 100644 --- a/server/queries/QueriesProcessor.cpp +++ b/server/queries/QueriesProcessor.cpp @@ -12,8 +12,6 @@ #include "CQuery.h" -boost::mutex QueriesProcessor::mx; - void QueriesProcessor::popQuery(PlayerColor player, QueryPtr query) { LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); diff --git a/server/queries/QueriesProcessor.h b/server/queries/QueriesProcessor.h index d0fd6df35..b46cd24fd 100644 --- a/server/queries/QueriesProcessor.h +++ b/server/queries/QueriesProcessor.h @@ -23,8 +23,6 @@ private: std::map> queries; //player => stack of queries public: - static boost::mutex mx; - void addQuery(QueryPtr query); void popQuery(const CQuery &query); void popQuery(QueryPtr query); diff --git a/serverapp/CMakeLists.txt b/serverapp/CMakeLists.txt new file mode 100644 index 000000000..059e6132d --- /dev/null +++ b/serverapp/CMakeLists.txt @@ -0,0 +1,34 @@ +set(serverapp_SRCS + StdInc.cpp + EntryPoint.cpp +) + +set(serverapp_HEADERS + StdInc.h +) + +assign_source_group(${serverapp_SRCS} ${serverapp_HEADERS}) +add_executable(vcmiserver ${serverapp_SRCS} ${serverapp_HEADERS}) +set(serverapp_LIBS vcmi) + +if(CMAKE_SYSTEM_NAME MATCHES FreeBSD OR HAIKU) + set(serverapp_LIBS execinfo ${serverapp_LIBS}) +endif() +target_link_libraries(vcmiserver PRIVATE ${serverapp_LIBS} minizip::minizip vcmiservercommon) + +target_include_directories(vcmiserver + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} +) + +if(WIN32) + set_target_properties(vcmiserver + PROPERTIES + OUTPUT_NAME "VCMI_server" + PROJECT_LABEL "VCMI_server" + ) +endif() + +vcmi_set_output_dir(vcmiserver "") +enable_pch(vcmiserver) + +install(TARGETS vcmiserver DESTINATION ${BIN_DIR}) diff --git a/serverapp/EntryPoint.cpp b/serverapp/EntryPoint.cpp new file mode 100644 index 000000000..f473ff05d --- /dev/null +++ b/serverapp/EntryPoint.cpp @@ -0,0 +1,104 @@ +/* + * EntryPoint.cpp, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#include "StdInc.h" + +#include "../server/CVCMIServer.h" + +#include "../lib/CConsoleHandler.h" +#include "../lib/logging/CBasicLogConfigurator.h" +#include "../lib/VCMIDirs.h" +#include "../lib/VCMI_Lib.h" + +#include + +static const std::string SERVER_NAME_AFFIX = "server"; +static const std::string SERVER_NAME = GameConstants::VCMI_VERSION + std::string(" (") + SERVER_NAME_AFFIX + ')'; + +static void handleCommandOptions(int argc, const char * argv[], boost::program_options::variables_map & options) +{ + boost::program_options::options_description opts("Allowed options"); + opts.add_options() + ("help,h", "display help and exit") + ("version,v", "display version information and exit") + ("run-by-client", "indicate that server launched by client on same machine") + ("port", boost::program_options::value(), "port at which server will listen to connections from client") + ("lobby", "start server in lobby mode in which server connects to a global lobby"); + + if(argc > 1) + { + try + { + boost::program_options::store(boost::program_options::parse_command_line(argc, argv, opts), options); + } + catch(boost::program_options::error & e) + { + std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; + } + } + + boost::program_options::notify(options); + + if(options.count("help")) + { + auto time = std::time(nullptr); + printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); + printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); + printf("This is free software; see the source for copying conditions. There is NO\n"); + printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); + printf("\n"); + std::cout << opts; + exit(0); + } + + if(options.count("version")) + { + printf("%s\n", GameConstants::VCMI_VERSION.c_str()); + std::cout << VCMIDirs::get().genHelpString(); + exit(0); + } +} + +int main(int argc, const char * argv[]) +{ + // Correct working dir executable folder (not bundle folder) so we can use executable relative paths + boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); + + console = new CConsoleHandler(); + CBasicLogConfigurator logConfig(VCMIDirs::get().userLogsPath() / "VCMI_Server_log.txt", console); + logConfig.configureDefault(); + logGlobal->info(SERVER_NAME); + + boost::program_options::variables_map opts; + handleCommandOptions(argc, argv, opts); + preinitDLL(console, false); + logConfig.configure(); + + loadDLLClasses(); + std::srand(static_cast(time(nullptr))); + + { + bool connectToLobby = opts.count("lobby"); + bool runByClient = opts.count("runByClient"); + uint16_t port = 3030; + if(opts.count("port")) + port = opts["port"].as(); + + CVCMIServer server(port, connectToLobby, runByClient); + + server.run(); + + // CVCMIServer destructor must be called here - before VLC cleanup + } + + logConfig.deconfigure(); + vstd::clear_pointer(VLC); + + return 0; +} diff --git a/serverapp/StdInc.cpp b/serverapp/StdInc.cpp new file mode 100644 index 000000000..dd7f66cb8 --- /dev/null +++ b/serverapp/StdInc.cpp @@ -0,0 +1,2 @@ +// Creates the precompiled header +#include "StdInc.h" diff --git a/serverapp/StdInc.h b/serverapp/StdInc.h new file mode 100644 index 000000000..d03216bdf --- /dev/null +++ b/serverapp/StdInc.h @@ -0,0 +1,14 @@ +/* + * StdInc.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +#include "../Global.h" + +VCMI_LIB_USING_NAMESPACE diff --git a/test/CVcmiTestConfig.h b/test/CVcmiTestConfig.h index 5a1fb007d..9052fa80b 100644 --- a/test/CVcmiTestConfig.h +++ b/test/CVcmiTestConfig.h @@ -15,6 +15,6 @@ class CVcmiTestConfig : public ::testing::Environment { public: - virtual void SetUp() override; - virtual void TearDown() override; + void SetUp() override; + void TearDown() override; }; diff --git a/test/JsonComparer.cpp b/test/JsonComparer.cpp index 99c3c0c99..1a28071cd 100644 --- a/test/JsonComparer.cpp +++ b/test/JsonComparer.cpp @@ -45,7 +45,7 @@ bool JsonComparer::isEmpty(const JsonNode & value) case JsonNode::JsonType::DATA_BOOL: return !value.Bool(); case JsonNode::JsonType::DATA_FLOAT: - return value.Float() == 0; + return vstd::isAlmostEqual(value.Float(), 0.0); case JsonNode::JsonType::DATA_INTEGER: return value.Integer() == 0; case JsonNode::JsonType::DATA_STRING: @@ -165,7 +165,7 @@ void JsonComparer::checkEqualJson(const JsonNode & actual, const JsonNode & expe } else { - check(false, "type mismatch. \n expected:\n"+expected.toJson(true)+"\n actual:\n" +actual.toJson(true)); + check(false, "type mismatch. \n expected:\n"+expected.toCompactString()+"\n actual:\n" +actual.toCompactString()); } } diff --git a/test/JsonComparer.h b/test/JsonComparer.h index dd10b6950..e98ed039c 100644 --- a/test/JsonComparer.h +++ b/test/JsonComparer.h @@ -10,7 +10,7 @@ #pragma once -#include "../lib/JsonNode.h" +#include "../lib/json/JsonNode.h" namespace vstd { diff --git a/test/battle/BattleHexTest.cpp b/test/battle/BattleHexTest.cpp index d420a99d5..d2179587d 100644 --- a/test/battle/BattleHexTest.cpp +++ b/test/battle/BattleHexTest.cpp @@ -45,7 +45,8 @@ TEST(BattleHexTest, getNeighbouringTiles) TEST(BattleHexTest, getDistance) { - BattleHex firstHex(0,0), secondHex(16,0); + BattleHex firstHex(0,0); + BattleHex secondHex(16,0); EXPECT_EQ((int)firstHex.getDistance(firstHex,secondHex), 16); firstHex=0, secondHex=170; EXPECT_EQ((int)firstHex.getDistance(firstHex,secondHex), 10); @@ -63,7 +64,8 @@ TEST(BattleHexTest, getDistance) TEST(BattleHexTest, mutualPositions) { - BattleHex firstHex(0,0), secondHex(16,0); + BattleHex firstHex(0,0); + BattleHex secondHex(16,0); firstHex=86, secondHex=68; EXPECT_EQ((int)firstHex.mutualPosition(firstHex,secondHex), BattleHex::EDir::TOP_LEFT); secondHex=69; diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index 5cef1f907..d7b3ae386 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -85,7 +85,7 @@ public: UnitFake & add(ui8 side) { - UnitFake * unit = new UnitFake(); + auto * unit = new UnitFake(); EXPECT_CALL(*unit, unitSide()).WillRepeatedly(Return(side)); unit->setDefaultExpectations(); @@ -224,7 +224,7 @@ public: } }; -TEST_F(BattleFinishedTest, NoBattleIsDraw) +TEST_F(BattleFinishedTest, DISABLED_NoBattleIsDraw) { expectBattleDraw(); } diff --git a/test/battle/CUnitStateMagicTest.cpp b/test/battle/CUnitStateMagicTest.cpp index 9926548ae..d888178fa 100644 --- a/test/battle/CUnitStateMagicTest.cpp +++ b/test/battle/CUnitStateMagicTest.cpp @@ -74,7 +74,7 @@ TEST_F(UnitStateMagicTest, initialNormal) EXPECT_EQ(subject.casts.available(), 567); } -TEST_F(UnitStateMagicTest, schoolLevelByDefault) +TEST_F(UnitStateMagicTest, DISABLED_schoolLevelByDefault) { setDefaultExpectations(); initUnit(); @@ -82,7 +82,7 @@ TEST_F(UnitStateMagicTest, schoolLevelByDefault) EXPECT_EQ(subject.getSpellSchoolLevel(&spellMock, nullptr), 0); } -TEST_F(UnitStateMagicTest, schoolLevelForNormalCaster) +TEST_F(UnitStateMagicTest, DISABLED_schoolLevelForNormalCaster) { setDefaultExpectations(); initUnit(); @@ -91,7 +91,7 @@ TEST_F(UnitStateMagicTest, schoolLevelForNormalCaster) EXPECT_EQ(subject.getSpellSchoolLevel(&spellMock, nullptr), DEFAULT_SCHOOL_LEVEL); } -TEST_F(UnitStateMagicTest, effectLevelForNormalCaster) +TEST_F(UnitStateMagicTest, DISABLED_effectLevelForNormalCaster) { setDefaultExpectations(); initUnit(); @@ -155,7 +155,7 @@ TEST_F(UnitStateMagicTest, enchantPower) EXPECT_EQ(subject.getEnchantPower(&spellMock), ENCHANT_POWER); } -TEST_F(UnitStateMagicTest, effectValueByDefault) +TEST_F(UnitStateMagicTest, DISABLED_effectValueByDefault) { setDefaultExpectations(); initUnit(); @@ -164,7 +164,7 @@ TEST_F(UnitStateMagicTest, effectValueByDefault) EXPECT_EQ(subject.getEffectValue(&spellMock), 0); } -TEST_F(UnitStateMagicTest, effectValue) +TEST_F(UnitStateMagicTest, DISABLED_effectValue) { setDefaultExpectations(); initUnit(); diff --git a/test/entity/CCreatureTest.cpp b/test/entity/CCreatureTest.cpp index 81befa660..590754e73 100644 --- a/test/entity/CCreatureTest.cpp +++ b/test/entity/CCreatureTest.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "../../lib/CCreatureHandler.h" +#include "../../lib/json/JsonNode.h" namespace test { @@ -46,9 +47,9 @@ TEST_F(CCreatureTest, RegistersIcons) subject->registerIcons(cb); } -TEST_F(CCreatureTest, JsonUpdate) +TEST_F(CCreatureTest, DISABLED_JsonUpdate) { - JsonNode data(JsonNode::JsonType::DATA_STRUCT); + JsonNode data; JsonNode & config = data["config"]; config["cost"]["gold"].Integer() = 750; @@ -103,11 +104,11 @@ TEST_F(CCreatureTest, JsonUpdate) EXPECT_TRUE(subject->isDoubleWide()); } -TEST_F(CCreatureTest, JsonAddBonus) +TEST_F(CCreatureTest, DISABLED_JsonAddBonus) { - JsonNode data(JsonNode::JsonType::DATA_STRUCT); + JsonNode data; - std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); + auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); JsonNode & toAdd = data["bonuses"]["toAdd"]; @@ -129,14 +130,14 @@ TEST_F(CCreatureTest, JsonAddBonus) EXPECT_TRUE(subject->hasBonus(selector)); } -TEST_F(CCreatureTest, JsonRemoveBonus) +TEST_F(CCreatureTest, DISABLED_JsonRemoveBonus) { - JsonNode data(JsonNode::JsonType::DATA_STRUCT); + JsonNode data; - std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); + auto b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b1); - std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); + auto b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b2); diff --git a/test/erm/ERM_MA.cpp b/test/erm/ERM_MA.cpp index 76c484bc3..66037374f 100644 --- a/test/erm/ERM_MA.cpp +++ b/test/erm/ERM_MA.cpp @@ -87,13 +87,13 @@ TEST_F(ERM_MA, Example) creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::FLYING, BonusSource::CREATURE_ABILITY, 0, 0)); creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, Bonus::KING, BonusSource::CREATURE_ABILITY, 0, 0)); - std::shared_ptr removed = std::make_shared(BonusDuration::PERMANENT, Bonus::MIND_IMMUNITY, BonusSource::CREATURE_ABILITY, 0, 0); + auto removed = std::make_shared(BonusDuration::PERMANENT, Bonus::MIND_IMMUNITY, BonusSource::CREATURE_ABILITY, 0, 0); creatureBonuses.addNewBonus(removed); creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, Bonus::NO_MORALE, BonusSource::CREATURE_ABILITY, 0, 0)); creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::UNDEAD, BonusSource::CREATURE_ABILITY, 0, 0)); - std::shared_ptr added = std::make_shared(BonusDuration::PERMANENT, Bonus::NO_MELEE_PENALTY, BonusSource::CREATURE_ABILITY, 0, 0); + auto added = std::make_shared(BonusDuration::PERMANENT, Bonus::NO_MELEE_PENALTY, BonusSource::CREATURE_ABILITY, 0, 0); EXPECT_CALL(oldCreature, getRecruitCost(Eq(6))).WillOnce(Return(COST)); @@ -206,13 +206,13 @@ TEST_F(ERM_MA, Bonuses) creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::FLYING, BonusSource::CREATURE_ABILITY, 0, 0)); creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, Bonus::KING, BonusSource::CREATURE_ABILITY, 0, 0)); - std::shared_ptr removed = std::make_shared(BonusDuration::PERMANENT, Bonus::MIND_IMMUNITY, BonusSource::CREATURE_ABILITY, 0, 0); + auto removed = std::make_shared(BonusDuration::PERMANENT, Bonus::MIND_IMMUNITY, BonusSource::CREATURE_ABILITY, 0, 0); creatureBonuses.addNewBonus(removed); creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, Bonus::NO_MORALE, BonusSource::CREATURE_ABILITY, 0, 0)); creatureBonuses.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::UNDEAD, BonusSource::CREATURE_ABILITY, 0, 0)); - std::shared_ptr added = std::make_shared(BonusDuration::PERMANENT, Bonus::NO_MELEE_PENALTY, BonusSource::CREATURE_ABILITY, 0, 0); + auto added = std::make_shared(BonusDuration::PERMANENT, Bonus::NO_MELEE_PENALTY, BonusSource::CREATURE_ABILITY, 0, 0); EXPECT_CALL(oldCreature, isDoubleWide()).WillRepeatedly(Return(false)); diff --git a/test/events/EventBusTest.cpp b/test/events/EventBusTest.cpp index 8aac72dbb..150731960 100644 --- a/test/events/EventBusTest.cpp +++ b/test/events/EventBusTest.cpp @@ -28,7 +28,7 @@ public: public: static SubscriptionRegistry * getRegistry() { - static std::unique_ptr> Instance = std::make_unique>(); + static auto Instance = std::make_unique>(); return Instance.get(); } diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 02222e712..aff72a61f 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -15,6 +15,7 @@ #include "mock/mock_spells_Problem.h" #include "../../lib/VCMIDirs.h" +#include "../../lib/json/JsonUtils.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/PacksForClientBattle.h" @@ -46,17 +47,14 @@ public: void SetUp() override { - IObjectInterface::cb = gameCallback.get(); - gameState = std::make_shared(); gameCallback->setGameState(gameState.get()); - gameState->preInit(&services); + gameState->preInit(&services, gameCallback.get()); } void TearDown() override { gameState.reset(); - IObjectInterface::cb = nullptr; } bool describeChanges() const override @@ -145,7 +143,7 @@ public: si.mapname = "anything";//does not matter, map service mocked si.difficulty = 0; si.mapfileChecksum = 0; - si.mode = StartInfo::NEW_GAME; + si.mode = EStartMode::NEW_GAME; si.seedToBeUsed = 42; std::unique_ptr header = mapService.loadMapHeader(ResourcePath(si.mapname)); @@ -222,7 +220,7 @@ public: }; //Issue #2765, Ghost Dragons can cast Age on Catapults -TEST_F(CGameStateTest, issue2765) +TEST_F(CGameStateTest, DISABLED_issue2765) { startTestGame(); @@ -240,7 +238,7 @@ TEST_F(CGameStateTest, issue2765) gameCallback->sendAndApply(&na); PutArtifact pack; - pack.al = ArtifactLocation(defender, ArtifactPosition::MACH1); + pack.al = ArtifactLocation(defender->id, ArtifactPosition::MACH1); pack.art = a; gameCallback->sendAndApply(&pack); } @@ -310,7 +308,7 @@ TEST_F(CGameStateTest, issue2765) } -TEST_F(CGameStateTest, battleResurrection) +TEST_F(CGameStateTest, DISABLED_battleResurrection) { startTestGame(); @@ -334,7 +332,7 @@ TEST_F(CGameStateTest, battleResurrection) gameCallback->sendAndApply(&na); PutArtifact pack; - pack.al = ArtifactLocation(attacker, ArtifactPosition::SPELLBOOK); + pack.al = ArtifactLocation(attacker->id, ArtifactPosition::SPELLBOOK); pack.art = a; gameCallback->sendAndApply(&pack); } @@ -422,6 +420,6 @@ TEST_F(CGameStateTest, updateEntity) JsonNode actual; EXPECT_CALL(services, updateEntity(Eq(Metatype::CREATURE), Eq(424242), _)).WillOnce(SaveArg<2>(&actual)); - gameState->updateEntity(Metatype::CREATURE, 424242, JsonUtils::stringNode("TEST")); + gameState->updateEntity(Metatype::CREATURE, 424242, JsonNode("TEST")); EXPECT_EQ(actual.String(), "TEST"); } diff --git a/test/map/CMapEditManagerTest.cpp b/test/map/CMapEditManagerTest.cpp index aefffdbc2..0020a6e93 100644 --- a/test/map/CMapEditManagerTest.cpp +++ b/test/map/CMapEditManagerTest.cpp @@ -14,7 +14,6 @@ #include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMap.h" #include "../lib/TerrainHandler.h" -#include "../lib/JsonNode.h" #include "../lib/mapping/CMapEditManager.h" #include "../lib/int3.h" #include "../lib/CRandomGenerator.h" @@ -25,7 +24,7 @@ TEST(MapManager, DrawTerrain_Type) { try { - auto map = std::make_unique(); + auto map = std::make_unique(nullptr); map->width = 52; map->height = 52; map->initTerrain(); @@ -34,7 +33,7 @@ TEST(MapManager, DrawTerrain_Type) // 1x1 Blow up editManager->getTerrainSelection().select(int3(5, 5, 0)); - editManager->drawTerrain(ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10); static const int3 squareCheck[] = { int3(5,5,0), int3(5,4,0), int3(4,4,0), int3(4,5,0) }; for(const auto & tile : squareCheck) { @@ -43,20 +42,20 @@ TEST(MapManager, DrawTerrain_Type) // Concat to square editManager->getTerrainSelection().select(int3(6, 5, 0)); - editManager->drawTerrain(ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10); EXPECT_EQ(map->getTile(int3(6, 4, 0)).terType->getId(), ETerrainId::GRASS); editManager->getTerrainSelection().select(int3(6, 5, 0)); - editManager->drawTerrain(ETerrainId::LAVA); + editManager->drawTerrain(ETerrainId::LAVA, 10); EXPECT_EQ(map->getTile(int3(4, 4, 0)).terType->getId(), ETerrainId::GRASS); EXPECT_EQ(map->getTile(int3(7, 4, 0)).terType->getId(), ETerrainId::LAVA); // Special case water,rock editManager->getTerrainSelection().selectRange(MapRect(int3(10, 10, 0), 10, 5)); - editManager->drawTerrain(ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10); editManager->getTerrainSelection().selectRange(MapRect(int3(15, 17, 0), 10, 5)); - editManager->drawTerrain(ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10); editManager->getTerrainSelection().select(int3(21, 16, 0)); - editManager->drawTerrain(ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10); EXPECT_EQ(map->getTile(int3(20, 15, 0)).terType->getId(), ETerrainId::GRASS); // Special case non water,rock @@ -67,16 +66,16 @@ TEST(MapManager, DrawTerrain_Type) { editManager->getTerrainSelection().select(tile); } - editManager->drawTerrain(ETerrainId::GRASS); + editManager->drawTerrain(ETerrainId::GRASS, 10); EXPECT_EQ(map->getTile(int3(35, 44, 0)).terType->getId(), ETerrainId::WATER); // Rock case editManager->getTerrainSelection().selectRange(MapRect(int3(1, 1, 1), 15, 15)); - editManager->drawTerrain(ETerrainId::SUBTERRANEAN); + editManager->drawTerrain(ETerrainId::SUBTERRANEAN, 10); std::vector vec({ int3(6, 6, 1), int3(7, 6, 1), int3(8, 6, 1), int3(5, 7, 1), int3(6, 7, 1), int3(7, 7, 1), int3(8, 7, 1), int3(4, 8, 1), int3(5, 8, 1), int3(6, 8, 1)}); editManager->getTerrainSelection().setSelection(vec); - editManager->drawTerrain(ETerrainId::ROCK); + editManager->drawTerrain(ETerrainId::ROCK, 10); EXPECT_TRUE(!map->getTile(int3(5, 6, 1)).terType->isPassable() || !map->getTile(int3(7, 8, 1)).terType->isPassable()); //todo: add checks here and enable, also use smaller size @@ -114,8 +113,8 @@ TEST(MapManager, DrawTerrain_View) const ResourcePath testMap("test/TerrainViewTest", EResType::MAP); // Load maps and json config CMapService mapService; - const auto originalMap = mapService.loadMap(testMap); - auto map = mapService.loadMap(testMap); + const auto originalMap = mapService.loadMap(testMap, nullptr); + auto map = mapService.loadMap(testMap, nullptr); // Validate edit manager auto editManager = map->getEditManager(); @@ -144,7 +143,7 @@ TEST(MapManager, DrawTerrain_View) int3 pos((si32)posVector[0].Float(), (si32)posVector[1].Float(), (si32)posVector[2].Float()); const auto & originalTile = originalMap->getTile(pos); editManager->getTerrainSelection().selectRange(MapRect(pos, 1, 1)); - editManager->drawTerrain(originalTile.terType->getId(), &gen); + editManager->drawTerrain(originalTile.terType->getId(), 10, &gen); const auto & tile = map->getTile(pos); bool isInRange = false; for(const auto & range : mapping) diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index 647a76907..176d5d916 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -9,8 +9,6 @@ */ #include "StdInc.h" -#include "../../lib/JsonDetail.h" - #include "../../lib/filesystem/CMemoryBuffer.h" #include "../../lib/filesystem/Filesystem.h" @@ -38,15 +36,15 @@ static void saveTestMap(CMemoryBuffer & serializeBuffer, const std::string & fil tmp.close(); } -TEST(MapFormat, Random) +TEST(MapFormat, DISABLED_Random) { SCOPED_TRACE("MapFormat_Random start"); CMapGenOptions opt; CRmgTemplate tmpl; - std::shared_ptr zoneOptions = std::make_shared(); + auto zoneOptions = std::make_shared(); - const_cast(tmpl.getCpuPlayers()).addRange(1, 4); + const_cast(tmpl.getHumanPlayers()).addRange(1, 4); const_cast(tmpl.getZones())[0] = zoneOptions; zoneOptions->setOwner(1); @@ -55,14 +53,14 @@ TEST(MapFormat, Random) opt.setHeight(CMapHeader::MAP_SIZE_MIDDLE); opt.setWidth(CMapHeader::MAP_SIZE_MIDDLE); opt.setHasTwoLevels(true); - opt.setPlayerCount(4); + opt.setHumanOrCpuPlayerCount(4); opt.setPlayerTypeForStandardPlayer(PlayerColor(0), EPlayerType::HUMAN); opt.setPlayerTypeForStandardPlayer(PlayerColor(1), EPlayerType::AI); opt.setPlayerTypeForStandardPlayer(PlayerColor(2), EPlayerType::AI); opt.setPlayerTypeForStandardPlayer(PlayerColor(3), EPlayerType::AI); - CMapGenerator gen(opt, TEST_RANDOM_SEED); + CMapGenerator gen(opt, nullptr, TEST_RANDOM_SEED); std::unique_ptr initialMap = gen.generate(); initialMap->name.appendRawString("Test"); @@ -80,7 +78,7 @@ TEST(MapFormat, Random) serializeBuffer.seek(0); { CMapLoaderJson loader(&serializeBuffer); - std::unique_ptr serialized = loader.loadMap(); + std::unique_ptr serialized = loader.loadMap(nullptr); MapComparer c; c(serialized, initialMap); @@ -97,14 +95,14 @@ static JsonNode getFromArchive(CZipLoader & archive, const std::string & archive auto data = archive.load(resource)->readAll(); - JsonNode res(reinterpret_cast(data.first.get()), data.second); + JsonNode res(reinterpret_cast(data.first.get()), data.second); return res; } static void addToArchive(CZipSaver & saver, const JsonNode & data, const std::string & filename) { - auto s = data.toJson(); + auto s = data.toString(); std::unique_ptr stream = saver.addFile(filename); if(stream->write((const ui8*)s.c_str(), s.size()) != s.size()) @@ -130,7 +128,7 @@ static std::unique_ptr loadOriginal(const JsonNode & header, const JsonNod CMapLoaderJson initialLoader(&initialBuffer); - return initialLoader.loadMap(); + return initialLoader.loadMap(nullptr); } static void loadActual(CMemoryBuffer * serializeBuffer, const std::unique_ptr & originalMap, JsonNode & header, JsonNode & objects, JsonNode & surface, JsonNode & underground) @@ -149,7 +147,7 @@ static void loadActual(CMemoryBuffer * serializeBuffer, const std::unique_ptrfield, expected->field) diff --git a/test/mock/BattleFake.cpp b/test/mock/BattleFake.cpp index b00631d61..c22e5e3a4 100644 --- a/test/mock/BattleFake.cpp +++ b/test/mock/BattleFake.cpp @@ -46,7 +46,7 @@ void UnitFake::expectAnyBonusSystemCall() UnitFake & UnitsFake::add(ui8 side) { - UnitFake * unit = new UnitFake(); + auto * unit = new UnitFake(); ON_CALL(*unit, unitSide()).WillByDefault(Return(side)); unit->redirectBonusesToFake(); diff --git a/test/mock/BattleFake.h b/test/mock/BattleFake.h index 8ce30c38f..4a8d04b4d 100644 --- a/test/mock/BattleFake.h +++ b/test/mock/BattleFake.h @@ -19,7 +19,6 @@ #include "mock_scripting_Pool.h" #endif -#include "../../lib/JsonNode.h" #include "../../lib/battle/CBattleInfoCallback.h" namespace test diff --git a/test/mock/mock_IBattleInfoCallback.h b/test/mock/mock_IBattleInfoCallback.h index 2bc00a19b..f28dd37f6 100644 --- a/test/mock/mock_IBattleInfoCallback.h +++ b/test/mock/mock_IBattleInfoCallback.h @@ -29,7 +29,7 @@ public: MOCK_CONST_METHOD0(battleNextUnitId, uint32_t()); - MOCK_CONST_METHOD1(battleGetUnitsIf, battle::Units(battle::UnitFilter)); + MOCK_CONST_METHOD1(battleGetUnitsIf, battle::Units(const battle::UnitFilter &)); MOCK_CONST_METHOD1(battleGetUnitByID, const battle::Unit *(uint32_t)); MOCK_CONST_METHOD2(battleGetUnitByPos, const battle::Unit *(BattleHex, bool)); diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index f92c1690c..077001c85 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -35,14 +35,16 @@ public: //TODO: fail all stub calls - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} + void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) override {} + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {} void showInfoDialog(InfoWindow * iw) override {} void showInfoDialog(const std::string & msg, PlayerColor player) override {} void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override {} bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;} - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) override {}; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {}; void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {} + void giveExperience(const CGHeroInstance * hero, TExpType val) override {} void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override {} void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {} void showBlockingDialog(BlockingDialog *iw) override {} @@ -66,8 +68,7 @@ public: void removeAfterVisit(const CGObjectInstance *object) override {} //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override {} + bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override {return false;} void removeArtifact(const ArtifactLocation &al) override {} bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) override {return false;} @@ -81,6 +82,7 @@ public: bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;} void giveHeroBonus(GiveBonus * bonus) override {} void setMovePoints(SetMovePoints * smp) override {} + void setMovePoints(ObjectInstanceID hid, int val, bool absolute) override {}; void setManaPoints(ObjectInstanceID hid, int val) override {} void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {} void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {} diff --git a/test/mock/mock_IGameInfoCallback.h b/test/mock/mock_IGameInfoCallback.h index 879f0c374..94939fc24 100644 --- a/test/mock/mock_IGameInfoCallback.h +++ b/test/mock/mock_IGameInfoCallback.h @@ -17,7 +17,10 @@ class IGameInfoCallbackMock : public IGameInfoCallback public: //various MOCK_CONST_METHOD1(getDate, int(Date)); - MOCK_CONST_METHOD2(isAllowed, bool(int32_t, int32_t)); + + MOCK_CONST_METHOD1(isAllowed, bool(SpellID)); + MOCK_CONST_METHOD1(isAllowed, bool(ArtifactID)); + MOCK_CONST_METHOD1(isAllowed, bool(SecondarySkill)); //player MOCK_CONST_METHOD1(getPlayer, const Player *(PlayerColor)); diff --git a/test/mock/mock_MapService.cpp b/test/mock/mock_MapService.cpp index d9883c9d6..375ba2c96 100644 --- a/test/mock/mock_MapService.cpp +++ b/test/mock/mock_MapService.cpp @@ -45,7 +45,7 @@ std::unique_ptr MapServiceMock::loadMap() const initialBuffer.seek(0); CMapLoaderJson initialLoader(&initialBuffer); - std::unique_ptr res = initialLoader.loadMap(); + std::unique_ptr res = initialLoader.loadMap(nullptr); if(mapListener) mapListener->mapLoaded(res.get()); @@ -53,7 +53,7 @@ std::unique_ptr MapServiceMock::loadMap() const return res; } -std::unique_ptr MapServiceMock::loadMap(const ResourcePath & name) const +std::unique_ptr MapServiceMock::loadMap(const ResourcePath & name, IGameCallback * cb) const { return loadMap(); } @@ -65,7 +65,7 @@ std::unique_ptr MapServiceMock::loadMapHeader(const ResourcePath & n return initialLoader.loadMapHeader(); } -std::unique_ptr MapServiceMock::loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const +std::unique_ptr MapServiceMock::loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding, IGameCallback * cb) const { return loadMap(); } @@ -84,7 +84,7 @@ void MapServiceMock::saveMap(const std::unique_ptr & map, boost::filesyste void MapServiceMock::addToArchive(CZipSaver & saver, const JsonNode & data, const std::string & filename) { - auto s = data.toJson(); + auto s = data.toString(); std::unique_ptr stream = saver.addFile(filename); if(stream->write((const ui8*)s.c_str(), s.size()) != s.size()) diff --git a/test/mock/mock_MapService.h b/test/mock/mock_MapService.h index cf2da28a9..3f1413683 100644 --- a/test/mock/mock_MapService.h +++ b/test/mock/mock_MapService.h @@ -29,9 +29,9 @@ class MapServiceMock : public IMapService public: MapServiceMock(const std::string & path, MapListener * mapListener_); - std::unique_ptr loadMap(const ResourcePath & name) const override; + std::unique_ptr loadMap(const ResourcePath & name, IGameCallback * cb) const override; std::unique_ptr loadMapHeader(const ResourcePath & name) const override; - std::unique_ptr loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; + std::unique_ptr loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding, IGameCallback * cb) const override; std::unique_ptr loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; void saveMap(const std::unique_ptr & map, boost::filesystem::path fullPath) const override; diff --git a/test/mock/mock_battle_IBattleState.h b/test/mock/mock_battle_IBattleState.h index 483c13b23..6ef0998d8 100644 --- a/test/mock/mock_battle_IBattleState.h +++ b/test/mock/mock_battle_IBattleState.h @@ -17,8 +17,8 @@ class BattleStateMock : public IBattleState { public: MOCK_CONST_METHOD0(getActiveStackID, int32_t()); - MOCK_CONST_METHOD1(getStacksIf, TStacks(TStackFilter)); - MOCK_CONST_METHOD1(getUnitsIf, battle::Units(battle::UnitFilter)); + MOCK_CONST_METHOD1(getStacksIf, TStacks(const TStackFilter&)); + MOCK_CONST_METHOD1(getUnitsIf, battle::Units(const battle::UnitFilter &)); MOCK_CONST_METHOD0(getBattlefieldType, BattleField()); MOCK_CONST_METHOD0(getTerrainType, TerrainId()); MOCK_CONST_METHOD0(getAllObstacles, IBattleInfo::ObstacleCList()); diff --git a/test/mock/mock_battle_Unit.h b/test/mock/mock_battle_Unit.h index 5b32c7e48..c9ea0625a 100644 --- a/test/mock/mock_battle_Unit.h +++ b/test/mock/mock_battle_Unit.h @@ -19,7 +19,7 @@ public: MOCK_CONST_METHOD0(getTreeVersion, int64_t()); MOCK_CONST_METHOD0(getCasterUnitId, int32_t()); - MOCK_CONST_METHOD2(getSpellSchoolLevel, int32_t(const spells::Spell *, int32_t *)); + MOCK_CONST_METHOD2(getSpellSchoolLevel, int32_t(const spells::Spell *, SpellSchool *)); MOCK_CONST_METHOD1(getEffectLevel, int32_t(const spells::Spell *)); MOCK_CONST_METHOD3(getSpellBonus, int64_t(const spells::Spell *, int64_t, const battle::Unit *)); MOCK_CONST_METHOD2(getSpecificSpellBonus, int64_t(const spells::Spell *, int64_t)); diff --git a/test/mock/mock_scripting_Context.h b/test/mock/mock_scripting_Context.h index a78cceaa2..365458f57 100644 --- a/test/mock/mock_scripting_Context.h +++ b/test/mock/mock_scripting_Context.h @@ -12,7 +12,7 @@ #include -#include "../../../lib/JsonNode.h" +#include "../../../lib/json/JsonNode.h" namespace scripting { diff --git a/test/mock/mock_spells_Mechanics.h b/test/mock/mock_spells_Mechanics.h index 985e093f3..288c06fa4 100644 --- a/test/mock/mock_spells_Mechanics.h +++ b/test/mock/mock_spells_Mechanics.h @@ -11,6 +11,7 @@ #pragma once #include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/CGameInfoCallback.h" namespace spells { diff --git a/test/mock/mock_spells_Spell.h b/test/mock/mock_spells_Spell.h index 3e8aa442e..ccd33d129 100644 --- a/test/mock/mock_spells_Spell.h +++ b/test/mock/mock_spells_Spell.h @@ -45,6 +45,7 @@ public: MOCK_CONST_METHOD0(isOffensive, bool()); MOCK_CONST_METHOD0(isSpecial, bool()); MOCK_CONST_METHOD0(isMagical, bool()); + MOCK_CONST_METHOD0(canCastOnSelf, bool()); MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool)); MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &)); MOCK_CONST_METHOD0(getCastSound, const std::string &()); diff --git a/test/rmg/CRmgTemplateTest.cpp b/test/rmg/CRmgTemplateTest.cpp index 02d738b17..925236f0b 100644 --- a/test/rmg/CRmgTemplateTest.cpp +++ b/test/rmg/CRmgTemplateTest.cpp @@ -34,7 +34,7 @@ protected: void testLoadSave(const std::string & id, const JsonNode & config) { - std::shared_ptr subject = std::make_shared(); + auto subject = std::make_shared(); subject->setId(id); { @@ -71,7 +71,7 @@ protected: const auto otherZone = subject->getZones().at(otherZoneId); GTEST_ASSERT_NE(otherZone, nullptr); - EXPECT_THAT(thisZone->getTreasureInfo(), ContainerEq(otherZone->getTreasureInfo()));; + EXPECT_THAT(thisZone->getTreasureInfo(), ContainerEq(otherZone->getTreasureInfo())); } } diff --git a/test/scripting/LuaSandboxTest.cpp b/test/scripting/LuaSandboxTest.cpp index 19655631b..5010a3ad4 100644 --- a/test/scripting/LuaSandboxTest.cpp +++ b/test/scripting/LuaSandboxTest.cpp @@ -30,7 +30,7 @@ protected: }; -TEST_F(LuaSandboxTest, Example) +TEST_F(LuaSandboxTest, DISABLED_Example) { loadScriptFromFile("test/lua/SandboxTest.lua"); runClientServer(); diff --git a/test/scripting/LuaSpellEffectAPITest.cpp b/test/scripting/LuaSpellEffectAPITest.cpp index 8040b99df..a144485a3 100644 --- a/test/scripting/LuaSpellEffectAPITest.cpp +++ b/test/scripting/LuaSpellEffectAPITest.cpp @@ -12,6 +12,7 @@ #include "ScriptFixture.h" #include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/json/JsonUtils.h" #include "../mock/mock_ServerCallback.h" @@ -33,7 +34,7 @@ protected: } }; -TEST_F(LuaSpellEffectAPITest, ApplicableOnExpert) +TEST_F(LuaSpellEffectAPITest, DISABLED_ApplicableOnExpert) { loadScriptFromFile("test/lua/SpellEffectAPITest.lua"); @@ -45,14 +46,14 @@ TEST_F(LuaSpellEffectAPITest, ApplicableOnExpert) JsonNode ret = context->callGlobal("applicable", params); - JsonNode expected = JsonUtils::boolNode(true); + JsonNode expected(true); JsonComparer cmp(false); cmp.compare("applicable result", ret, expected); } -TEST_F(LuaSpellEffectAPITest, NotApplicableOnAdvanced) +TEST_F(LuaSpellEffectAPITest, DISABLED_NotApplicableOnAdvanced) { loadScriptFromFile("test/lua/SpellEffectAPITest.lua"); @@ -64,13 +65,13 @@ TEST_F(LuaSpellEffectAPITest, NotApplicableOnAdvanced) JsonNode ret = context->callGlobal("applicable", params); - JsonNode expected = JsonUtils::boolNode(false); + JsonNode expected(false); JsonComparer cmp(false); cmp.compare("applicable result", ret, expected); } -TEST_F(LuaSpellEffectAPITest, ApplicableOnLeftSideOfField) +TEST_F(LuaSpellEffectAPITest, DISABLED_ApplicableOnLeftSideOfField) { loadScriptFromFile("test/lua/SpellEffectAPITest.lua"); @@ -83,8 +84,8 @@ TEST_F(LuaSpellEffectAPITest, ApplicableOnLeftSideOfField) BattleHex hex(2,2); JsonNode first; - first.Vector().push_back(JsonUtils::intNode(hex.hex)); - first.Vector().push_back(JsonNode()); + first.Vector().emplace_back(hex.hex); + first.Vector().emplace_back(); JsonNode targets; targets.Vector().push_back(first); @@ -93,13 +94,13 @@ TEST_F(LuaSpellEffectAPITest, ApplicableOnLeftSideOfField) JsonNode ret = context->callGlobal("applicableTarget", params); - JsonNode expected = JsonUtils::boolNode(true); + JsonNode expected(true); JsonComparer cmp(false); cmp.compare("applicable result", ret, expected); } -TEST_F(LuaSpellEffectAPITest, NotApplicableOnRightSideOfField) +TEST_F(LuaSpellEffectAPITest, DISABLED_NotApplicableOnRightSideOfField) { loadScriptFromFile("test/lua/SpellEffectAPITest.lua"); @@ -112,8 +113,8 @@ TEST_F(LuaSpellEffectAPITest, NotApplicableOnRightSideOfField) BattleHex hex(11,2); JsonNode first; - first.Vector().push_back(JsonUtils::intNode(hex.hex)); - first.Vector().push_back(JsonUtils::intNode(-1)); + first.Vector().emplace_back(hex.hex); + first.Vector().emplace_back(-1); JsonNode targets; targets.Vector().push_back(first); @@ -122,13 +123,13 @@ TEST_F(LuaSpellEffectAPITest, NotApplicableOnRightSideOfField) JsonNode ret = context->callGlobal("applicableTarget", params); - JsonNode expected = JsonUtils::boolNode(false); + JsonNode expected(false); JsonComparer cmp(false); cmp.compare("applicable result", ret, expected); } -TEST_F(LuaSpellEffectAPITest, ApplyMoveUnit) +TEST_F(LuaSpellEffectAPITest, DISABLED_ApplyMoveUnit) { loadScriptFromFile("test/lua/SpellEffectAPIMoveUnit.lua"); @@ -137,14 +138,14 @@ TEST_F(LuaSpellEffectAPITest, ApplyMoveUnit) BattleHex hex1(11,2); JsonNode unit; - unit.Vector().push_back(JsonUtils::intNode(hex1.hex)); - unit.Vector().push_back(JsonUtils::intNode(42)); + unit.Vector().emplace_back(hex1.hex); + unit.Vector().emplace_back(42); BattleHex hex2(5,4); JsonNode destination; - destination.Vector().push_back(JsonUtils::intNode(hex2.hex)); - destination.Vector().push_back(JsonUtils::intNode(-1)); + destination.Vector().emplace_back(hex2.hex); + destination.Vector().emplace_back(-1); JsonNode targets; targets.Vector().push_back(unit); diff --git a/test/scripting/LuaSpellEffectTest.cpp b/test/scripting/LuaSpellEffectTest.cpp index bb22c142d..a550f57cc 100644 --- a/test/scripting/LuaSpellEffectTest.cpp +++ b/test/scripting/LuaSpellEffectTest.cpp @@ -19,6 +19,7 @@ #include "../mock/mock_ServerCallback.h" #include "../../../lib/VCMI_Lib.h" +#include "../../lib/json/JsonUtils.h" #include "../../../lib/ScriptHandler.h" #include "../../../lib/CScriptingModule.h" @@ -90,7 +91,7 @@ public: JsonNode saveRequest(const std::string &, const JsonNode & parameters) { - JsonNode response = JsonUtils::boolNode(true); + JsonNode response(true); request = parameters; return response; @@ -98,7 +99,7 @@ public: JsonNode saveRequest2(ServerCallback *, const std::string &, const JsonNode & parameters) { - JsonNode response = JsonUtils::boolNode(true); + JsonNode response(true); request = parameters; return response; @@ -122,7 +123,7 @@ TEST_F(LuaSpellEffectTest, ApplicableRedirected) { setDefaultExpectations(); - JsonNode response = JsonUtils::boolNode(true); + JsonNode response(true); EXPECT_CALL(*contextMock, callGlobal(Eq("applicable"),_)).WillOnce(Return(response));//TODO: check call parameter @@ -153,12 +154,12 @@ TEST_F(LuaSpellEffectTest, ApplicableTargetRedirected) JsonNode first; - first.Vector().push_back(JsonUtils::intNode(hex1.hex)); - first.Vector().push_back(JsonUtils::intNode(id1)); + first.Vector().emplace_back(hex1.hex); + first.Vector().emplace_back(id1); JsonNode second; - second.Vector().push_back(JsonUtils::intNode(hex2.hex)); - second.Vector().push_back(JsonUtils::intNode(-1)); + second.Vector().emplace_back(hex2.hex); + second.Vector().emplace_back(-1); JsonNode targets; targets.Vector().push_back(first); @@ -192,8 +193,8 @@ TEST_F(LuaSpellEffectTest, ApplyRedirected) subject->apply(&serverMock, &mechanicsMock, target); JsonNode first; - first.Vector().push_back(JsonUtils::intNode(hex1.hex)); - first.Vector().push_back(JsonUtils::intNode(id1)); + first.Vector().emplace_back(hex1.hex); + first.Vector().emplace_back(id1); JsonNode targets; targets.Vector().push_back(first); diff --git a/test/scripting/ScriptFixture.cpp b/test/scripting/ScriptFixture.cpp index e350c6c61..0ff6e3b2b 100644 --- a/test/scripting/ScriptFixture.cpp +++ b/test/scripting/ScriptFixture.cpp @@ -23,7 +23,7 @@ ScriptFixture::~ScriptFixture() = default; void ScriptFixture::loadScriptFromFile(const std::string & path) { - JsonNode scriptConfig(JsonNode::JsonType::DATA_STRUCT); + JsonNode scriptConfig; scriptConfig["source"].String() = path; loadScript(scriptConfig); } @@ -41,7 +41,7 @@ void ScriptFixture::loadScript(const JsonNode & scriptConfig) void ScriptFixture::loadScript(ModulePtr module, const std::string & scriptSource) { - subject = std::make_shared(VLC->scriptHandler); + subject = std::make_shared(VLC->scriptHandler.get()); subject->host = module; subject->sourceText = scriptSource; diff --git a/test/scripting/ScriptFixture.h b/test/scripting/ScriptFixture.h index ea9f0cbe3..97edbe1fe 100644 --- a/test/scripting/ScriptFixture.h +++ b/test/scripting/ScriptFixture.h @@ -14,7 +14,7 @@ #include -#include "../../lib/JsonNode.h" +#include "../../../lib/json/JsonNode.h" #include "../../lib/ScriptHandler.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/bonuses/Bonus.h" diff --git a/test/spells/TargetConditionTest.cpp b/test/spells/TargetConditionTest.cpp index 2f6f604fb..7ed5d3ccf 100644 --- a/test/spells/TargetConditionTest.cpp +++ b/test/spells/TargetConditionTest.cpp @@ -122,7 +122,7 @@ TEST_F(TargetConditionTest, SerializesCorrectly) EXPECT_CALL(factoryMock, createConfigurable(Eq(""), Eq("bonus"), Eq("UNDEAD"))).WillOnce(Return(normalItem)); - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["noneOf"]["bonus.NON_LIVING"].String() = "normal"; config["anyOf"]["bonus.SIEGE_WEAPON"].String() = "absolute"; config["allOf"]["bonus.UNDEAD"].String() = "normal"; diff --git a/test/spells/effects/CatapultTest.cpp b/test/spells/effects/CatapultTest.cpp index 56356ff0b..1cb55c5f2 100644 --- a/test/spells/effects/CatapultTest.cpp +++ b/test/spells/effects/CatapultTest.cpp @@ -50,7 +50,7 @@ TEST_F(CatapultTest, NotApplicableWithoutTown) TEST_F(CatapultTest, NotApplicableInVillage) { - std::shared_ptr fakeTown = std::make_shared(); + auto fakeTown = std::make_shared(nullptr); EXPECT_CALL(*battleFake, getDefendedTown()).WillRepeatedly(Return(fakeTown.get())); EXPECT_CALL(mechanicsMock, adaptProblem(_, _)).WillOnce(Return(false)); @@ -62,7 +62,7 @@ TEST_F(CatapultTest, NotApplicableInVillage) TEST_F(CatapultTest, NotApplicableForDefenderIfSmart) { - std::shared_ptr fakeTown = std::make_shared(); + auto fakeTown = std::make_shared(nullptr); fakeTown->builtBuildings.insert(BuildingID::FORT); mechanicsMock.casterSide = BattleSide::DEFENDER; @@ -74,9 +74,9 @@ TEST_F(CatapultTest, NotApplicableForDefenderIfSmart) EXPECT_FALSE(subject->applicable(problemMock, &mechanicsMock)); } -TEST_F(CatapultTest, ApplicableInTown) +TEST_F(CatapultTest, DISABLED_ApplicableInTown) { - std::shared_ptr fakeTown = std::make_shared(); + auto fakeTown = std::make_shared(nullptr); fakeTown->builtBuildings.insert(BuildingID::FORT); EXPECT_CALL(*battleFake, getDefendedTown()).WillRepeatedly(Return(fakeTown.get())); @@ -106,17 +106,17 @@ protected: void SetUp() override { EffectFixture::setUp(); - fakeTown = std::make_shared(); + fakeTown = std::make_shared(nullptr); fakeTown->builtBuildings.insert(BuildingID::FORT); } private: std::shared_ptr fakeTown; }; -TEST_F(CatapultApplyTest, DamageToIntactPart) +TEST_F(CatapultApplyTest, DISABLED_DamageToIntactPart) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["targetsToAttack"].Integer() = 1; config["chanceToNormalHit"].Integer() = 100; EffectFixture::setupEffect(config); diff --git a/test/spells/effects/CloneTest.cpp b/test/spells/effects/CloneTest.cpp index a9599ea44..b68124242 100644 --- a/test/spells/effects/CloneTest.cpp +++ b/test/spells/effects/CloneTest.cpp @@ -14,6 +14,7 @@ #include #include "../../../lib/battle/CUnitState.h" +#include "../../../lib/json/JsonNode.h" namespace test { @@ -39,7 +40,7 @@ protected: TEST_F(CloneTest, ApplicableToValidTarget) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["maxTier"].Integer() = 7; EffectFixture::setupEffect(config); } @@ -187,7 +188,7 @@ protected: }; -TEST_F(CloneApplyTest, AddsNewUnit) +TEST_F(CloneApplyTest, DISABLED_AddsNewUnit) { setDefaultExpectations(); @@ -204,7 +205,7 @@ TEST_F(CloneApplyTest, AddsNewUnit) EXPECT_TRUE(cloneAddInfo->summoned); } -TEST_F(CloneApplyTest, SetsClonedFlag) +TEST_F(CloneApplyTest, DISABLED_SetsClonedFlag) { setDefaultExpectations(); @@ -217,7 +218,7 @@ TEST_F(CloneApplyTest, SetsClonedFlag) EXPECT_TRUE(cloneState->cloned); } -TEST_F(CloneApplyTest, SetsCloneLink) +TEST_F(CloneApplyTest, DISABLED_SetsCloneLink) { setDefaultExpectations(); @@ -228,7 +229,7 @@ TEST_F(CloneApplyTest, SetsCloneLink) EXPECT_EQ(originalState->cloneID, cloneId); } -TEST_F(CloneApplyTest, SetsLifetimeMarker) +TEST_F(CloneApplyTest, DISABLED_SetsLifetimeMarker) { setDefaultExpectations(); diff --git a/test/spells/effects/DamageTest.cpp b/test/spells/effects/DamageTest.cpp index 11a998a6c..227764405 100644 --- a/test/spells/effects/DamageTest.cpp +++ b/test/spells/effects/DamageTest.cpp @@ -14,6 +14,7 @@ #include #include "../../../lib/battle/CUnitState.h" +#include "../../../lib/json/JsonNode.h" #include "mock/mock_UnitEnvironment.h" @@ -84,7 +85,7 @@ protected: } }; -TEST_F(DamageApplyTest, DoesDamageToAliveUnit) +TEST_F(DamageApplyTest, DISABLED_DoesDamageToAliveUnit) { EffectFixture::setupEffect(JsonNode()); using namespace ::battle; @@ -104,7 +105,7 @@ TEST_F(DamageApplyTest, DoesDamageToAliveUnit) unitsFake.setDefaultBonusExpectations(); - std::shared_ptr targetUnitState = std::make_shared(&targetUnit, &targetUnit); + auto targetUnitState = std::make_shared(&targetUnit, &targetUnit); targetUnitState->localInit(&unitEnvironmentMock); EXPECT_CALL(targetUnit, acquireState()).WillOnce(Return(targetUnitState)); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId),_, Lt(0))).Times(1); @@ -121,7 +122,7 @@ TEST_F(DamageApplyTest, DoesDamageToAliveUnit) EXPECT_EQ(targetUnitState->getCount(), unitAmount - 1); } -TEST_F(DamageApplyTest, IgnoresDeadUnit) +TEST_F(DamageApplyTest, DISABLED_IgnoresDeadUnit) { EffectFixture::setupEffect(JsonNode()); using namespace ::battle; @@ -141,12 +142,12 @@ TEST_F(DamageApplyTest, IgnoresDeadUnit) subject->apply(&serverMock, &mechanicsMock, target); } -TEST_F(DamageApplyTest, DoesDamageByPercent) +TEST_F(DamageApplyTest, DISABLED_DoesDamageByPercent) { using namespace ::battle; { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["killByPercentage"].Bool() = true; EffectFixture::setupEffect(config); } @@ -168,7 +169,7 @@ TEST_F(DamageApplyTest, DoesDamageByPercent) unitsFake.setDefaultBonusExpectations(); - std::shared_ptr targetUnitState = std::make_shared(&targetUnit, &targetUnit); + auto targetUnitState = std::make_shared(&targetUnit, &targetUnit); targetUnitState->localInit(&unitEnvironmentMock); EXPECT_CALL(targetUnit, acquireState()).WillOnce(Return(targetUnitState)); @@ -186,12 +187,12 @@ TEST_F(DamageApplyTest, DoesDamageByPercent) EXPECT_EQ(targetUnitState->getCount(), unitAmount - (unitAmount * effectValue / 100)); } -TEST_F(DamageApplyTest, DoesDamageByCount) +TEST_F(DamageApplyTest, DISABLED_DoesDamageByCount) { using namespace ::battle; { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["killByCount"].Bool() = true; EffectFixture::setupEffect(config); } @@ -212,7 +213,7 @@ TEST_F(DamageApplyTest, DoesDamageByCount) unitsFake.setDefaultBonusExpectations(); - std::shared_ptr targetUnitState = std::make_shared(&targetUnit, &targetUnit); + auto targetUnitState = std::make_shared(&targetUnit, &targetUnit); targetUnitState->localInit(&unitEnvironmentMock); EXPECT_CALL(targetUnit, acquireState()).WillOnce(Return(targetUnitState)); diff --git a/test/spells/effects/DispelTest.cpp b/test/spells/effects/DispelTest.cpp index 10ac75f0f..fb53586b3 100644 --- a/test/spells/effects/DispelTest.cpp +++ b/test/spells/effects/DispelTest.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "EffectFixture.h" +#include "../../../lib/json/JsonNode.h" namespace test { @@ -64,10 +65,10 @@ class DispelTest : public DispelFixture { }; -TEST_F(DispelTest, ApplicableToAliveUnitWithTimedEffect) +TEST_F(DispelTest, DISABLED_ApplicableToAliveUnitWithTimedEffect) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["dispelNegative"].Bool() = true; EffectFixture::setupEffect(config); } @@ -90,10 +91,10 @@ TEST_F(DispelTest, ApplicableToAliveUnitWithTimedEffect) EXPECT_TRUE(subject->applicable(problemMock, &mechanicsMock, target)); } -TEST_F(DispelTest, IgnoresOwnEffects) +TEST_F(DispelTest, DISABLED_IgnoresOwnEffects) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["dispelPositive"].Bool() = true; config["dispelNegative"].Bool() = true; config["dispelNeutral"].Bool() = true; @@ -161,10 +162,10 @@ public: std::array, 2> actualBonus; }; -TEST_F(DispelApplyTest, RemovesEffects) +TEST_F(DispelApplyTest, DISABLED_RemovesEffects) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["dispelPositive"].Bool() = true; config["dispelNegative"].Bool() = true; config["dispelNeutral"].Bool() = true; diff --git a/test/spells/effects/EffectFixture.h b/test/spells/effects/EffectFixture.h index dde8e9a2d..4a2d38034 100644 --- a/test/spells/effects/EffectFixture.h +++ b/test/spells/effects/EffectFixture.h @@ -33,7 +33,6 @@ #include "../../mock/mock_ServerCallback.h" -#include "../../../lib/JsonNode.h" #include "../../../lib/battle/CBattleInfoCallback.h" namespace battle diff --git a/test/spells/effects/HealTest.cpp b/test/spells/effects/HealTest.cpp index 24da47673..8cfdb396b 100644 --- a/test/spells/effects/HealTest.cpp +++ b/test/spells/effects/HealTest.cpp @@ -14,6 +14,7 @@ #include #include "../../../lib/battle/CUnitState.h" +#include "../../../lib/json/JsonNode.h" #include "mock/mock_UnitEnvironment.h" @@ -76,7 +77,7 @@ TEST_F(HealTest, ApplicableToWoundedUnit) TEST_F(HealTest, ApplicableIfActuallyResurrects) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; config["minFullUnits"].Integer() = 5; EffectFixture::setupEffect(config); @@ -103,7 +104,7 @@ TEST_F(HealTest, ApplicableIfActuallyResurrects) TEST_F(HealTest, NotApplicableIfNotEnoughCasualties) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; config["minFullUnits"].Integer() = 1; EffectFixture::setupEffect(config); @@ -129,7 +130,7 @@ TEST_F(HealTest, NotApplicableIfNotEnoughCasualties) TEST_F(HealTest, NotApplicableIfResurrectsLessThanRequired) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; config["minFullUnits"].Integer() = 5; EffectFixture::setupEffect(config); @@ -155,7 +156,7 @@ TEST_F(HealTest, NotApplicableIfResurrectsLessThanRequired) TEST_F(HealTest, ApplicableToDeadUnit) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; EffectFixture::setupEffect(config); } @@ -183,10 +184,10 @@ TEST_F(HealTest, ApplicableToDeadUnit) EXPECT_TRUE(subject->applicable(problemMock, &mechanicsMock, target)); } -TEST_F(HealTest, NotApplicableIfDeadUnitIsBlocked) +TEST_F(HealTest, DISABLED_NotApplicableIfDeadUnitIsBlocked) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; EffectFixture::setupEffect(config); } @@ -220,10 +221,10 @@ TEST_F(HealTest, NotApplicableIfDeadUnitIsBlocked) EXPECT_FALSE(subject->applicable(problemMock, &mechanicsMock, target)); } -TEST_F(HealTest, ApplicableWithAnotherDeadUnitInSamePosition) +TEST_F(HealTest, DISABLED_ApplicableWithAnotherDeadUnitInSamePosition) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; EffectFixture::setupEffect(config); } @@ -260,7 +261,7 @@ TEST_F(HealTest, ApplicableWithAnotherDeadUnitInSamePosition) TEST_F(HealTest, NotApplicableIfEffectValueTooLow) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["minFullUnits"].Integer() = 1; EffectFixture::setupEffect(config); } @@ -324,10 +325,10 @@ protected: } }; -TEST_P(HealApplyTest, Heals) +TEST_P(HealApplyTest, DISABLED_Heals) { { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = HEAL_LEVEL_MAP.at(static_cast(healLevel)); config["healPower"].String() = HEAL_POWER_MAP.at(static_cast(healPower)); EffectFixture::setupEffect(config); @@ -352,7 +353,7 @@ TEST_P(HealApplyTest, Heals) unitsFake.setDefaultBonusExpectations(); - std::shared_ptr targetUnitState = std::make_shared(&targetUnit, &targetUnit); + auto targetUnitState = std::make_shared(&targetUnit, &targetUnit); targetUnitState->localInit(&unitEnvironmentMock); { int64_t initialDmg = unitAmount * unitHP / 2 - 1; diff --git a/test/spells/effects/SacrificeTest.cpp b/test/spells/effects/SacrificeTest.cpp index c0a3b5117..40c04bc4e 100644 --- a/test/spells/effects/SacrificeTest.cpp +++ b/test/spells/effects/SacrificeTest.cpp @@ -14,6 +14,7 @@ #include #include "../../../lib/battle/CUnitState.h" +#include "../../../lib/json/JsonNode.h" #include "mock/mock_UnitEnvironment.h" @@ -38,7 +39,7 @@ protected: EffectFixture::setUp(); { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; EffectFixture::setupEffect(config); } @@ -145,7 +146,7 @@ protected: EffectFixture::setUp(); { - JsonNode config(JsonNode::JsonType::DATA_STRUCT); + JsonNode config; config["healLevel"].String() = "resurrect"; config["healPower"].String() = "permanent"; EffectFixture::setupEffect(config); @@ -153,7 +154,7 @@ protected: } }; -TEST_F(SacrificeApplyTest, ResurrectsTarget) +TEST_F(SacrificeApplyTest, DISABLED_ResurrectsTarget) { using namespace ::battle; diff --git a/test/spells/effects/SummonTest.cpp b/test/spells/effects/SummonTest.cpp index 33e4cde61..fbc322340 100644 --- a/test/spells/effects/SummonTest.cpp +++ b/test/spells/effects/SummonTest.cpp @@ -14,6 +14,7 @@ #include #include "../../../lib/CCreatureHandler.h" +#include "../../../lib/json/JsonNode.h" namespace test { @@ -72,7 +73,7 @@ protected: toSummon = creature1; - JsonNode options(JsonNode::JsonType::DATA_STRUCT); + JsonNode options; options["id"].String() = "airElemental"; options["exclusive"].Bool() = exclusive; options["summonSameUnit"].Bool() = summonSameUnit; @@ -81,7 +82,7 @@ protected: } }; -TEST_P(SummonTest, Applicable) +TEST_P(SummonTest, DISABLED_Applicable) { const bool expectedApplicable = !exclusive || otherSummoned == CreatureID() || otherSummoned == toSummon; @@ -101,7 +102,7 @@ TEST_P(SummonTest, Applicable) EXPECT_EQ(expectedApplicable, subject->applicable(problemMock, &mechanicsMock)); } -TEST_P(SummonTest, Transform) +TEST_P(SummonTest, DISABLED_Transform) { if(otherSummoned != CreatureID()) addOtherSummoned(true); @@ -201,7 +202,7 @@ protected: permanent = ::testing::get<0>(GetParam()); summonByHealth = ::testing::get<1>(GetParam()); - JsonNode options(JsonNode::JsonType::DATA_STRUCT); + JsonNode options; options["id"].String() = "airElemental"; options["permanent"].Bool() = permanent; options["summonByHealth"].Bool() = summonByHealth; @@ -218,7 +219,7 @@ protected: } }; -TEST_P(SummonApplyTest, SpawnsNewUnit) +TEST_P(SummonApplyTest, DISABLED_SpawnsNewUnit) { setDefaultExpectaions(); @@ -239,7 +240,7 @@ TEST_P(SummonApplyTest, SpawnsNewUnit) EXPECT_EQ(unitAddInfo->type, toSummon); } -TEST_P(SummonApplyTest, UpdatesOldUnit) +TEST_P(SummonApplyTest, DISABLED_UpdatesOldUnit) { setDefaultExpectaions(); diff --git a/test/spells/effects/TeleportTest.cpp b/test/spells/effects/TeleportTest.cpp index 2bd751179..00f46bfd1 100644 --- a/test/spells/effects/TeleportTest.cpp +++ b/test/spells/effects/TeleportTest.cpp @@ -11,6 +11,7 @@ #include "EffectFixture.h" +#include "../../../lib/json/JsonNode.h" #include namespace test @@ -51,7 +52,7 @@ protected: } }; -TEST_F(TeleportApplyTest, MovesUnit) +TEST_F(TeleportApplyTest, DISABLED_MovesUnit) { battleFake->setupEmptyBattlefield(); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index f399eb81e..541a35ecf 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -12,7 +12,8 @@ #include "EffectFixture.h" #include -#include "lib/modding/ModScope.h" +#include "../../../lib/modding/ModScope.h" +#include "../../../lib/json/JsonNode.h" namespace test { @@ -69,18 +70,18 @@ protected: } }; -TEST_P(TimedApplyTest, ChangesBonuses) +TEST_P(TimedApplyTest, DISABLED_ChangesBonuses) { Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, BonusSourceID(), BonusSubtypeID(PrimarySkill::KNOWLEDGE)); Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, BonusSourceID(), BonusSubtypeID(PrimarySkill::KNOWLEDGE)); testBonus2.turnsRemain = 4; - JsonNode options(JsonNode::JsonType::DATA_STRUCT); + JsonNode options; options["cumulative"].Bool() = cumulative; options["bonus"]["test1"] = testBonus1.toJsonNode(); options["bonus"]["test2"] = testBonus2.toJsonNode(); - options.setMeta(ModScope::scopeBuiltin()); + options.setModScope(ModScope::scopeBuiltin()); setupEffect(options); const uint32_t unitId = 42; diff --git a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp index 1e062eca6..a042b51c1 100644 --- a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp @@ -40,7 +40,7 @@ public: } }; -TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) +TEST_P(AbsoluteSpellConditionTest, DISABLED_ChecksAbsoluteCase) { setDefaultExpectations(); auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); @@ -54,7 +54,7 @@ TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } -TEST_P(AbsoluteSpellConditionTest, IgnoresNormalCase) +TEST_P(AbsoluteSpellConditionTest, DISABLED_IgnoresNormalCase) { setDefaultExpectations(); auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index 4e18d1c62..bd0e7d62c 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -39,7 +39,7 @@ TEST_F(BonusConditionTest, ImmuneByDefault) EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } -TEST_F(BonusConditionTest, ReceptiveIfMatchesType) +TEST_F(BonusConditionTest, DISABLED_ReceptiveIfMatchesType) { setDefaultExpectations(); unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, BonusSourceID())); diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index 61df6eb5a..b0085dee7 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -30,8 +30,8 @@ public: EXPECT_CALL(spellMock, forEachSchool(NotNull())).Times(AtLeast(1)).WillRepeatedly([](const spells::Spell::SchoolCallback & cb) { bool stop = false; - cb(SpellSchool(SpellSchool::AIR), stop); - cb(SpellSchool(SpellSchool::FIRE), stop); + cb(SpellSchool::AIR, stop); + cb(SpellSchool::FIRE, stop); }); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); diff --git a/test/spells/targetConditions/NormalSpellConditionTest.cpp b/test/spells/targetConditions/NormalSpellConditionTest.cpp index 3abf12ae9..22ae5bd47 100644 --- a/test/spells/targetConditions/NormalSpellConditionTest.cpp +++ b/test/spells/targetConditions/NormalSpellConditionTest.cpp @@ -40,7 +40,7 @@ public: } }; -TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) +TEST_P(NormalSpellConditionTest, DISABLED_ChecksAbsoluteCase) { setDefaultExpectations(); auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); @@ -54,7 +54,7 @@ TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } -TEST_P(NormalSpellConditionTest, ChecksNormalCase) +TEST_P(NormalSpellConditionTest, DISABLED_ChecksNormalCase) { setDefaultExpectations(); auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); diff --git a/test/vcai/ResurceManagerTest.cpp b/test/vcai/ResurceManagerTest.cpp index c7a4cb840..7a4e4920e 100644 --- a/test/vcai/ResurceManagerTest.cpp +++ b/test/vcai/ResurceManagerTest.cpp @@ -87,7 +87,7 @@ TEST_F(ResourceManagerTest, notifyGoalImplemented) EXPECT_FALSE(rm->notifyGoalCompleted(invalidGoal)); EXPECT_FALSE(rm->hasTasksLeft()); - TResources res(0,0,0,0,0,0,12345);; + TResources res(0,0,0,0,0,0,12345); rm->reserveResoures(res, invalidGoal); ASSERT_FALSE(rm->hasTasksLeft()) << "Can't push Invalid goal"; EXPECT_FALSE(rm->notifyGoalCompleted(invalidGoal)); @@ -114,13 +114,13 @@ TEST_F(ResourceManagerTest, queueOrder) { ASSERT_FALSE(rm->hasTasksLeft()); - TSubgoal buildLow = sptr(StrictMock()), - buildBit = sptr(StrictMock()), - buildMed = sptr(StrictMock()), - buildHigh = sptr(StrictMock()), - buildVeryHigh = sptr(StrictMock()), - buildExtra = sptr(StrictMock()), - buildNotSoExtra = sptr(StrictMock()); + TSubgoal buildLow = sptr(StrictMock()); + TSubgoal buildBit = sptr(StrictMock()); + TSubgoal buildMed = sptr(StrictMock()); + TSubgoal buildHigh = sptr(StrictMock()); + TSubgoal buildVeryHigh = sptr(StrictMock()); + TSubgoal buildExtra = sptr(StrictMock()); + TSubgoal buildNotSoExtra = sptr(StrictMock()); buildLow->setpriority(0).setbid(1); buildLow->setpriority(1).setbid(2);