mirror of
https://github.com/vcmi/vcmi.git
synced 2025-04-23 12:08:45 +02:00
commit
194b3389f9
35
.github/workflows/github.yml
vendored
35
.github/workflows/github.yml
vendored
@ -201,8 +201,8 @@ jobs:
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
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}
|
||||
if [[ ${{matrix.preset}} == linux-gcc-test ]]; then GCC12=1; fi
|
||||
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }} ${GCC12:+-DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12}
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
@ -384,3 +384,34 @@ jobs:
|
||||
name: ${{ env.VCMI_PACKAGE_FILE_NAME }}
|
||||
path: |
|
||||
${{ env.ANDROID_APK_PATH }}
|
||||
|
||||
|
||||
deploy-src:
|
||||
if: always() && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Build Number
|
||||
run: |
|
||||
source '${{github.workspace}}/CI/get_package_name.sh'
|
||||
echo VCMI_PACKAGE_FILE_NAME="$VCMI_PACKAGE_FILE_NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Create source code archive (including submodules)
|
||||
run: |
|
||||
git archive HEAD -o "release.tar" --worktree-attributes -v
|
||||
git submodule update --init --recursive
|
||||
git submodule --quiet foreach 'cd "$toplevel"; tar -rvf "release.tar" "$sm_path"'
|
||||
gzip release.tar
|
||||
|
||||
- name: Upload source code archive
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.VCMI_PACKAGE_FILE_NAME }}
|
||||
path: |
|
||||
./release.tar.gz
|
||||
|
@ -77,6 +77,7 @@ AIGateway::AIGateway()
|
||||
destinationTeleport = ObjectInstanceID();
|
||||
destinationTeleportPos = int3(-1);
|
||||
nullkiller.reset(new Nullkiller());
|
||||
announcedCheatingProblem = false;
|
||||
}
|
||||
|
||||
AIGateway::~AIGateway()
|
||||
@ -828,7 +829,14 @@ void AIGateway::makeTurn()
|
||||
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
|
||||
setThreadName("AIGateway::makeTurn");
|
||||
|
||||
cb->sendMessage("vcmieagles");
|
||||
if(cb->getStartInfo()->extraOptionsInfo.cheatsAllowed)
|
||||
cb->sendMessage("vcmieagles");
|
||||
else
|
||||
{
|
||||
if(!announcedCheatingProblem)
|
||||
cb->sendMessage("Nullkiller AI currently requires the ability to cheat in order to function correctly! Please enable!");
|
||||
announcedCheatingProblem = true;
|
||||
}
|
||||
|
||||
retrieveVisitableObjs();
|
||||
|
||||
|
@ -96,6 +96,7 @@ public:
|
||||
std::unique_ptr<boost::thread> makingTurn;
|
||||
private:
|
||||
boost::mutex turnInterruptionMutex;
|
||||
bool announcedCheatingProblem;
|
||||
public:
|
||||
ObjectInstanceID selectedObject;
|
||||
|
||||
|
@ -334,13 +334,13 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
|
||||
if(!upgrade.upgradeValue
|
||||
&& armyToGetOrBuy.upgradeValue > 20000
|
||||
&& ai->heroManager->canRecruitHero(town)
|
||||
&& ai->heroManager->canRecruitHero(upgrader)
|
||||
&& path.turn() < ai->settings->getScoutHeroTurnDistanceLimit())
|
||||
{
|
||||
for(auto hero : cb->getAvailableHeroes(town))
|
||||
for(auto hero : cb->getAvailableHeroes(upgrader))
|
||||
{
|
||||
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, town)
|
||||
+ ai->armyManager->howManyReinforcementsCanGet(hero, town);
|
||||
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
|
||||
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
|
||||
|
||||
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
|
||||
&& ai->getFreeGold() >20000
|
||||
@ -348,7 +348,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
{
|
||||
Composition recruitHero;
|
||||
|
||||
recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(town, hero));
|
||||
recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(upgrader, hero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -354,7 +354,7 @@ void Nullkiller::makeTurn()
|
||||
decompose(bestTasks, sptr(GatherArmyBehavior()), MAX_DEPTH);
|
||||
decompose(bestTasks, sptr(StayAtTownBehavior()), MAX_DEPTH);
|
||||
|
||||
if(cb->getDate(Date::DAY) == 1)
|
||||
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
|
||||
{
|
||||
decompose(bestTasks, sptr(StartupBehavior()), 1);
|
||||
}
|
||||
|
@ -365,7 +365,7 @@ if(MINGW OR MSVC)
|
||||
# Prevent compiler issues when building Debug
|
||||
# Assembler might fail with "too many sections"
|
||||
# With big-obj or 64-bit build will take hours
|
||||
if(CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Og")
|
||||
endif()
|
||||
endif(MINGW)
|
||||
@ -400,9 +400,11 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR NOT WIN32)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=array-bounds") # false positives in boost::multiarray during release build, keep as warning-only
|
||||
endif()
|
||||
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT WIN32)
|
||||
# For gcc 14+ we can use -fhardened instead
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -fstack-clash-protection -fcf-protection=full")
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT WIN32)
|
||||
# For gcc 14+ we can use -fhardened instead
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -fstack-clash-protection -fcf-protection=full")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Fix string inspection with lldb
|
||||
@ -740,7 +742,7 @@ if(WIN32)
|
||||
"${CMAKE_FIND_ROOT_PATH}/bin/*.dll")
|
||||
endif()
|
||||
|
||||
if(CMAKE_BUILD_TYPE MATCHES Debug)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
# Copy debug versions of libraries if build type is debug
|
||||
set(debug_postfix d)
|
||||
endif()
|
||||
|
53
ChangeLog.md
53
ChangeLog.md
@ -2,12 +2,61 @@
|
||||
|
||||
### Stability
|
||||
* Fixed possible crash on accessing faction description
|
||||
* Fixed possible thread race on leaving to main menu
|
||||
* Fixed possible thread race on exit to main menu
|
||||
* Game will now show error message instead of silent crash on corrupted H3 data
|
||||
* Fixed possible crash on double-deletion of quest artifacts placed by RMG
|
||||
* Fixed crash on loading save made in version 1.4 with removed from map Quest Guards
|
||||
* Added workaround for crash on accessing Altar of Sacrifice on saves made in 1.4
|
||||
* Fixed possible crash on map restart request
|
||||
* Fixed crash on attempt to open scenario list with no save or map selected
|
||||
* Fixed crash on host resolving error when connecting to online lobby
|
||||
* If json file specified in mod.json is missing, vcmi will now only log an error instead of crashing
|
||||
|
||||
### Interface
|
||||
* Fixed possible freeze on attempt to move hero when hero has non-zero movement points but not enough to reach first tile in path
|
||||
* Added retaliation damage and kills preview when hovering over units that can be attacked in melee during combat
|
||||
* Clicking on combat log would now open a window with full combat log history
|
||||
* Removed message length limit in text input fields, such as global lobby chat
|
||||
* Tapping on already active text input field will display on-screen keyboard on systems with one
|
||||
* Fixed possible freeze when trying to move hero if hero has non-zero movement points but not enough to reach first tile in path
|
||||
* Fixed selection of the wrong reward in dialogs such as the level-up window when double-clicking on it
|
||||
* Fixed launch of wrong map or save when double-clicking in scenario list screen
|
||||
* Right-clicking on a hero in a tavern will now select that hero as well, in line with H3
|
||||
* Fixed slow map list parsing when hota map format is enabled
|
||||
* MacOS and iOS can now use either Ctrl or Cmd key for all keyboard shortcuts
|
||||
* Small windows no longer dim the entire screen by default
|
||||
|
||||
### Mechanics
|
||||
* Recruiting a hero will now immediately reveal the fog of war around him
|
||||
* When both a visiting hero and a garrisoned hero are in town, the garrisoned hero will visit town buildings first.
|
||||
|
||||
### Multiplayer
|
||||
* Fixed in-game chat text not being visible after switching from achannel with a long history
|
||||
* Fixed lag when switching to channel with long history
|
||||
* Game now automatically scrolls in-game chat on new messages
|
||||
* Game will now only plays chat sound for active channel and for private channels
|
||||
* Cheats are now disabled by default in multiplayer
|
||||
* Game will now show status of cheats and battle replays on map start
|
||||
* It is possible to change cheats or battle replay on game loading
|
||||
* It is now possible to join rooms hosted by different hotfix versions, e.g. 1.5.1 can join 1.5.0 games
|
||||
* Fixed game rooms remaining visible in the lobby even after they have been closed
|
||||
* Fixed possible lag when there is a player in lobby with a very slow (or dying) connection
|
||||
* Game will show correctly if player has been invited into a room
|
||||
* Fixed overflow in invite window when there are more than 8 players in the lobby
|
||||
|
||||
### Random Maps Generator
|
||||
* Generator will now prefer to place roads away from zone borders
|
||||
|
||||
### AI
|
||||
* Fixed possible crash when Nullkiller AI tries to upgrade army
|
||||
* Nullkiller AI will now recruit new heroes if he left with 0 heroes
|
||||
* AI in combat now knows when an enemy unit has used all of its retaliations.
|
||||
|
||||
### Map Editor
|
||||
* Fixed setting up hero types of heroes in Prisons placed in map editor
|
||||
* Fixed crash on setting up Seer Hut in map editor
|
||||
* Added text auto-completion hints for army widget
|
||||
* Editor will now automatically add .vmap extensions when saving map
|
||||
* Fixed text size in map validation window
|
||||
|
||||
# 1.4.5 -> 1.5.0
|
||||
|
||||
|
@ -252,6 +252,13 @@
|
||||
"vcmi.battleWindow.damageEstimation.damage.1" : "%d damage",
|
||||
"vcmi.battleWindow.damageEstimation.kills" : "%d will perish",
|
||||
"vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish",
|
||||
|
||||
"vcmi.battleWindow.damageRetaliation.will" : "Will retaliate ",
|
||||
"vcmi.battleWindow.damageRetaliation.may" : "May retaliate ",
|
||||
"vcmi.battleWindow.damageRetaliation.never" : "Will not retaliate.",
|
||||
"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
|
||||
"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
|
||||
|
||||
"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!",
|
||||
|
@ -252,6 +252,13 @@
|
||||
"vcmi.battleWindow.damageEstimation.damage.1" : "%d de dano",
|
||||
"vcmi.battleWindow.damageEstimation.kills" : "%d morrerão",
|
||||
"vcmi.battleWindow.damageEstimation.kills.1" : "%d morrerá",
|
||||
|
||||
"vcmi.battleWindow.damageRetaliation.will" : "Contra-atacará ",
|
||||
"vcmi.battleWindow.damageRetaliation.may" : "Pode contra-atacar ",
|
||||
"vcmi.battleWindow.damageRetaliation.never" : "Não contra-atacará.",
|
||||
"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
|
||||
"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
|
||||
|
||||
"vcmi.battleWindow.killed" : "Eliminados",
|
||||
"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s morreram por tiros precisos!",
|
||||
"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s morreu com um tiro preciso!",
|
||||
|
@ -180,16 +180,19 @@
|
||||
"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.melee" : "Atacar %CREATURE (%DAMAGE).",
|
||||
"vcmi.battleWindow.damageEstimation.meleeKills" : "Atacar %CREATURE (%DAMAGE, %KILLS).",
|
||||
"vcmi.battleWindow.damageEstimation.ranged" : "Disparar a %CREATURE (%SHOTS, %DAMAGE).",
|
||||
"vcmi.battleWindow.damageEstimation.rangedKills" : "Disparar a %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
|
||||
"vcmi.battleWindow.damageEstimation.shots" : "%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á",
|
||||
"vcmi.battleWindow.damageRetaliation.will" : "Contratacará ",
|
||||
"vcmi.battleWindow.damageRetaliation.may" : "Puede contratacar ",
|
||||
"vcmi.battleWindow.damageRetaliation.never" : "No contratacará.",
|
||||
"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",
|
||||
@ -352,16 +355,16 @@
|
||||
|
||||
"core.bonus.ADDITIONAL_ATTACK.name": "Doble Ataque",
|
||||
"core.bonus.ADDITIONAL_ATTACK.description": "Ataca dos veces",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.name": "Contraataques adicionales",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.description": "Puede contraatacar ${val} veces adicionales",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.name": "Contrataques adicionales",
|
||||
"core.bonus.ADDITIONAL_RETALIATION.description": "Puede contratacar ${val} veces adicionales",
|
||||
"core.bonus.AIR_IMMUNITY.name": "Inmunidad al Aire",
|
||||
"core.bonus.AIR_IMMUNITY.description": "Inmune a todos los hechizos de la escuela de Aire",
|
||||
"core.bonus.ATTACKS_ALL_ADJACENT.name": "Ataque en todas las direcciones",
|
||||
"core.bonus.ATTACKS_ALL_ADJACENT.description": "Ataca a todos los enemigos adyacentes",
|
||||
"core.bonus.BLOCKS_RETALIATION.name": "Sin contraataque",
|
||||
"core.bonus.BLOCKS_RETALIATION.description": "El enemigo no puede contraatacar",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Sin contraataque a distancia",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.description": "El enemigo no puede contraatacar disparando",
|
||||
"core.bonus.BLOCKS_RETALIATION.name": "Evita contrataque",
|
||||
"core.bonus.BLOCKS_RETALIATION.description": "El enemigo no puede contratacar",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Evita contrataque a distancia",
|
||||
"core.bonus.BLOCKS_RANGED_RETALIATION.description": "El enemigo no puede contratacar disparando",
|
||||
"core.bonus.CATAPULT.name": "Catapulta",
|
||||
"core.bonus.CATAPULT.description": "Ataca a las paredes de asedio",
|
||||
"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reducir coste del conjuro (${val})",
|
||||
@ -397,7 +400,7 @@
|
||||
"core.bonus.FIRE_SHIELD.name": "Escudo de Fuego (${val}%)",
|
||||
"core.bonus.FIRE_SHIELD.description": "Refleja una parte del daño cuerpo a cuerpo",
|
||||
"core.bonus.FIRST_STRIKE.name": "Primer Ataque",
|
||||
"core.bonus.FIRST_STRIKE.description": "Esta criatura ataca primero en lugar de contraatacar",
|
||||
"core.bonus.FIRST_STRIKE.description": "Esta criatura ataca primero en lugar de contratacar",
|
||||
"core.bonus.FEAR.name": "Miedo",
|
||||
"core.bonus.FEAR.description": "Causa miedo a un grupo enemigo",
|
||||
"core.bonus.FEARLESS.name": "Inmune al miedo",
|
||||
@ -450,8 +453,8 @@
|
||||
"core.bonus.NON_LIVING.description": "Inmunidad a muchos efectos",
|
||||
"core.bonus.RANDOM_SPELLCASTER.name": "Lanzador de hechizos aleatorio",
|
||||
"core.bonus.RANDOM_SPELLCASTER.description": "Puede lanzar hechizos aleatorios",
|
||||
"core.bonus.RANGED_RETALIATION.name": "Contraataque a distancia",
|
||||
"core.bonus.RANGED_RETALIATION.description": "Puede realizar un contraataque a distancia",
|
||||
"core.bonus.RANGED_RETALIATION.name": "Contrataque a distancia",
|
||||
"core.bonus.RANGED_RETALIATION.description": "Puede realizar un contrataque a distancia",
|
||||
"core.bonus.RECEPTIVE.name": "Receptivo",
|
||||
"core.bonus.RECEPTIVE.description": "No tiene inmunidad a hechizos amistosos",
|
||||
"core.bonus.REBIRTH.name": "Renacimiento (${val}%)",
|
||||
@ -492,8 +495,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": "Contraataques ilimitados",
|
||||
"core.bonus.UNLIMITED_RETALIATIONS.description": "Puede realizar un número ilimitado de contraataques",
|
||||
"core.bonus.UNLIMITED_RETALIATIONS.name": "Contrataques ilimitados",
|
||||
"core.bonus.UNLIMITED_RETALIATIONS.description": "Puede realizar un número ilimitado de contrataques",
|
||||
"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",
|
||||
|
@ -241,11 +241,19 @@
|
||||
"vcmi.battleWindow.damageEstimation.rangedKills" : "Стріляти в %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
|
||||
"vcmi.battleWindow.damageEstimation.shots" : "%d пострілів залишилось",
|
||||
"vcmi.battleWindow.damageEstimation.shots.1" : "%d постріл залишився",
|
||||
"vcmi.battleWindow.damageEstimation.damage" : "%d одиниць пошкоджень",
|
||||
"vcmi.battleWindow.damageEstimation.damage.1" : "%d одиниця пошкодження",
|
||||
"vcmi.battleWindow.damageEstimation.damage" : "%d пошкоджень",
|
||||
"vcmi.battleWindow.damageEstimation.damage.1" : "%d пошкодження",
|
||||
"vcmi.battleWindow.damageEstimation.kills" : "%d загинуть",
|
||||
"vcmi.battleWindow.damageEstimation.kills.1" : "%d загине",
|
||||
|
||||
"vcmi.battleWindow.damageRetaliation.will" : "Буде відповідати ",
|
||||
"vcmi.battleWindow.damageRetaliation.may" : "Може відповісти ",
|
||||
"vcmi.battleWindow.damageRetaliation.never" : "Не буде відповідати.",
|
||||
"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
|
||||
"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
|
||||
|
||||
"vcmi.battleWindow.killed" : "Загинуло",
|
||||
|
||||
"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s було вбито влучними пострілами!",
|
||||
"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s було вбито влучним пострілом!",
|
||||
"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s було вбито влучними пострілами!",
|
||||
|
@ -10,7 +10,7 @@ android {
|
||||
applicationId "is.xyz.vcmi"
|
||||
minSdk 19
|
||||
targetSdk 33
|
||||
versionCode 1511
|
||||
versionCode 1513
|
||||
versionName "1.5.1"
|
||||
setProperty("archivesBaseName", "vcmi")
|
||||
}
|
||||
|
@ -9,14 +9,10 @@
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "CFocusableHelper.h"
|
||||
#include "../Global.h"
|
||||
#include "widgets/TextControls.h"
|
||||
#include "widgets/CTextInput.h"
|
||||
|
||||
void removeFocusFromActiveInput()
|
||||
{
|
||||
if(CFocusable::inputWithFocus == nullptr)
|
||||
return;
|
||||
CFocusable::inputWithFocus->focus = false;
|
||||
CFocusable::inputWithFocus->redraw();
|
||||
CFocusable::inputWithFocus = nullptr;
|
||||
if(CFocusable::inputWithFocus != nullptr)
|
||||
CFocusable::inputWithFocus->removeFocus();
|
||||
}
|
||||
|
@ -111,6 +111,7 @@ set(client_SRCS
|
||||
widgets/CGarrisonInt.cpp
|
||||
widgets/CreatureCostBox.cpp
|
||||
widgets/ComboBox.cpp
|
||||
widgets/CTextInput.cpp
|
||||
widgets/GraphicalPrimitiveCanvas.cpp
|
||||
widgets/Images.cpp
|
||||
widgets/MiscWidgets.cpp
|
||||
@ -293,6 +294,7 @@ set(client_HEADERS
|
||||
globalLobby/GlobalLobbyLoginWindow.h
|
||||
globalLobby/GlobalLobbyRoomWindow.h
|
||||
globalLobby/GlobalLobbyServerSetup.h
|
||||
globalLobby/GlobalLobbyObserver.h
|
||||
globalLobby/GlobalLobbyWidget.h
|
||||
globalLobby/GlobalLobbyWindow.h
|
||||
|
||||
@ -303,6 +305,7 @@ set(client_HEADERS
|
||||
widgets/CGarrisonInt.h
|
||||
widgets/CreatureCostBox.h
|
||||
widgets/ComboBox.h
|
||||
widgets/CTextInput.h
|
||||
widgets/GraphicalPrimitiveCanvas.h
|
||||
widgets/Images.h
|
||||
widgets/MiscWidgets.h
|
||||
|
@ -919,7 +919,6 @@ void CServerHandler::onDisconnected(const std::shared_ptr<INetworkConnection> &
|
||||
|
||||
if(getState() == EClientState::DISCONNECTING)
|
||||
{
|
||||
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;
|
||||
|
@ -184,7 +184,7 @@ void AdventureMapInterface::dim(Canvas & to)
|
||||
}
|
||||
for (auto window : GH.windows().findWindows<CIntObject>())
|
||||
{
|
||||
if (!std::dynamic_pointer_cast<AdventureMapInterface>(window) && !std::dynamic_pointer_cast<RadialMenu>(window) && !window->isPopupWindow() && (settings["adventure"]["backgroundDimSmallWindows"].Bool() || isBigWindow(window)))
|
||||
if (!std::dynamic_pointer_cast<AdventureMapInterface>(window) && !std::dynamic_pointer_cast<RadialMenu>(window) && !window->isPopupWindow() && (settings["adventure"]["backgroundDimSmallWindows"].Bool() || isBigWindow(window) || shortcuts->getState() == EAdventureState::HOTSEAT_WAIT))
|
||||
{
|
||||
Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y);
|
||||
ColorRGBA colorToFill(0, 0, 0, std::clamp<int>(backgroundDimLevel, 0, 255));
|
||||
@ -564,7 +564,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
|
||||
}
|
||||
else
|
||||
{
|
||||
if(GH.isKeyboardCtrlDown()) //normal click behaviour (as no hero selected)
|
||||
if(GH.isKeyboardCmdDown()) //normal click behaviour (as no hero selected)
|
||||
{
|
||||
if(canSelect)
|
||||
LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
|
||||
|
@ -47,6 +47,11 @@ void AdventureMapShortcuts::setState(EAdventureState newState)
|
||||
state = newState;
|
||||
}
|
||||
|
||||
EAdventureState AdventureMapShortcuts::getState()
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
void AdventureMapShortcuts::onMapViewMoved(const Rect & visibleArea, int newMapLevel)
|
||||
{
|
||||
mapLevel = newMapLevel;
|
||||
|
@ -89,5 +89,6 @@ public:
|
||||
bool optionMapViewActive();
|
||||
|
||||
void setState(EAdventureState newState);
|
||||
EAdventureState getState();
|
||||
void onMapViewMoved(const Rect & visibleArea, int mapLevel);
|
||||
};
|
||||
|
@ -266,7 +266,13 @@ void CInGameConsole::startEnteringText()
|
||||
return;
|
||||
|
||||
if(isEnteringText())
|
||||
{
|
||||
// force-reset text input to re-show on-screen keyboard
|
||||
GH.statusbar()->setEnteringMode(false);
|
||||
GH.statusbar()->setEnteringMode(true);
|
||||
GH.statusbar()->setEnteredText(enteredText);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(currentStatusBar.expired());//effectively, nullptr check
|
||||
|
||||
|
@ -114,6 +114,22 @@ static std::string formatRangedAttack(const DamageEstimation & estimation, const
|
||||
return formatAttack(estimation, creatureName, baseTextID, shotsLeft);
|
||||
}
|
||||
|
||||
static std::string formatRetaliation(const DamageEstimation & estimation, bool mayBeKilled)
|
||||
{
|
||||
if (estimation.damage.max == 0)
|
||||
return CGI->generaltexth->translate("vcmi.battleWindow.damageRetaliation.never");
|
||||
|
||||
std::string baseTextID = estimation.kills.max == 0 ?
|
||||
"vcmi.battleWindow.damageRetaliation.damage" :
|
||||
"vcmi.battleWindow.damageRetaliation.damageKills";
|
||||
|
||||
std::string prefixTextID = mayBeKilled ?
|
||||
"vcmi.battleWindow.damageRetaliation.may" :
|
||||
"vcmi.battleWindow.damageRetaliation.will";
|
||||
|
||||
return CGI->generaltexth->translate(prefixTextID) + formatAttack(estimation, "", baseTextID, 0);
|
||||
}
|
||||
|
||||
BattleActionsController::BattleActionsController(BattleInterface & owner):
|
||||
owner(owner),
|
||||
selectedStack(nullptr),
|
||||
@ -484,21 +500,23 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
|
||||
case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
|
||||
{
|
||||
BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
|
||||
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex);
|
||||
DamageEstimation retaliation;
|
||||
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex, &retaliation);
|
||||
estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
|
||||
estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
|
||||
bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount();
|
||||
|
||||
return formatMeleeAttack(estimation, targetStack->getName());
|
||||
return formatMeleeAttack(estimation, targetStack->getName()) + "\n" + formatRetaliation(retaliation, enemyMayBeKilled);
|
||||
}
|
||||
|
||||
case PossiblePlayerBattleAction::SHOOT:
|
||||
{
|
||||
const auto * shooter = owner.stacksController->getActiveStack();
|
||||
|
||||
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition());
|
||||
DamageEstimation retaliation;
|
||||
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition(), &retaliation);
|
||||
estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
|
||||
estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
|
||||
|
||||
return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
|
||||
}
|
||||
}
|
||||
|
||||
bool BattleInterface::openingPlaying()
|
||||
bool BattleInterface::openingPlaying() const
|
||||
{
|
||||
return battleOpeningDelayActive;
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ public:
|
||||
std::shared_ptr<BattleHero> attackingHero;
|
||||
std::shared_ptr<BattleHero> defendingHero;
|
||||
|
||||
bool openingPlaying();
|
||||
bool openingPlaying() const;
|
||||
void openingEnd();
|
||||
|
||||
bool makingTurn() const;
|
||||
|
@ -33,6 +33,7 @@
|
||||
#include "../render/Graphics.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/Images.h"
|
||||
#include "../widgets/Slider.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
#include "../widgets/GraphicalPrimitiveCanvas.h"
|
||||
#include "../windows/CMessage.h"
|
||||
@ -141,8 +142,10 @@ void BattleConsole::scrollDown(ui32 by)
|
||||
redraw();
|
||||
}
|
||||
|
||||
BattleConsole::BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
|
||||
: scrollPosition(-1)
|
||||
BattleConsole::BattleConsole(const BattleInterface & owner, std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
|
||||
: CIntObject(LCLICK)
|
||||
, owner(owner)
|
||||
, scrollPosition(-1)
|
||||
, enteringText(false)
|
||||
{
|
||||
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
||||
@ -161,6 +164,14 @@ void BattleConsole::deactivate()
|
||||
CIntObject::deactivate();
|
||||
}
|
||||
|
||||
void BattleConsole::clickPressed(const Point & cursorPosition)
|
||||
{
|
||||
if(owner.makingTurn() && !owner.openingPlaying())
|
||||
{
|
||||
GH.windows().createAndPushWindow<BattleConsoleWindow>(boost::algorithm::join(logEntries, "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
void BattleConsole::setEnteringMode(bool on)
|
||||
{
|
||||
consoleText.clear();
|
||||
@ -203,6 +214,26 @@ void BattleConsole::clear()
|
||||
write({});
|
||||
}
|
||||
|
||||
BattleConsoleWindow::BattleConsoleWindow(const std::string & text)
|
||||
: CWindowObject(BORDERED)
|
||||
{
|
||||
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
||||
|
||||
pos.w = 429;
|
||||
pos.h = 434;
|
||||
|
||||
updateShadow();
|
||||
center();
|
||||
|
||||
backgroundTexture = std::make_shared<CFilledTexture>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
|
||||
buttonOk = std::make_shared<CButton>(Point(183, 388), AnimationPath::builtin("IOKAY"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_ACCEPT);
|
||||
Rect textArea(18, 17, 393, 354);
|
||||
textBoxBackgroundBorder = std::make_shared<TransparentFilledRectangle>(textArea, ColorRGBA(0, 0, 0, 75), ColorRGBA(128, 100, 75));
|
||||
textBox = std::make_shared<CTextBox>(text, textArea.resize(-5), CSlider::BROWN);
|
||||
if(textBox->slider)
|
||||
textBox->slider->scrollToMax();
|
||||
}
|
||||
|
||||
const CGHeroInstance * BattleHero::instance()
|
||||
{
|
||||
return hero;
|
||||
|
@ -47,6 +47,8 @@ class BattleRenderer;
|
||||
class BattleConsole : public CIntObject, public IStatusBar
|
||||
{
|
||||
private:
|
||||
const BattleInterface & owner;
|
||||
|
||||
std::shared_ptr<CPicture> background;
|
||||
|
||||
/// List of all texts added during battle, essentially - log of entire battle
|
||||
@ -70,11 +72,13 @@ private:
|
||||
/// select line(s) that will be visible in UI
|
||||
std::vector<std::string> getVisibleText();
|
||||
public:
|
||||
BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size);
|
||||
BattleConsole(const BattleInterface & owner, std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size);
|
||||
|
||||
void showAll(Canvas & to) override;
|
||||
void deactivate() override;
|
||||
|
||||
void clickPressed(const Point & cursorPosition) override;
|
||||
|
||||
bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters)
|
||||
void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions
|
||||
void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions
|
||||
@ -87,6 +91,17 @@ public:
|
||||
void setEnteredText(const std::string & text) override;
|
||||
};
|
||||
|
||||
class BattleConsoleWindow : public CWindowObject
|
||||
{
|
||||
private:
|
||||
std::shared_ptr<CFilledTexture> backgroundTexture;
|
||||
std::shared_ptr<CButton> buttonOk;
|
||||
std::shared_ptr<TransparentFilledRectangle> textBoxBackgroundBorder;
|
||||
std::shared_ptr<CTextBox> textBox;
|
||||
public:
|
||||
BattleConsoleWindow(const std::string & text);
|
||||
};
|
||||
|
||||
/// Hero battle animation
|
||||
class BattleHero : public CIntObject
|
||||
{
|
||||
|
@ -199,7 +199,7 @@ std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode &
|
||||
auto rect = readRect(config["rect"]);
|
||||
auto offset = readPosition(config["imagePosition"]);
|
||||
auto background = widget<CPicture>("menuBattle");
|
||||
return std::make_shared<BattleConsole>(background, rect.topLeft(), offset, rect.dimensions() );
|
||||
return std::make_shared<BattleConsole>(owner, background, rect.topLeft(), offset, rect.dimensions() );
|
||||
}
|
||||
|
||||
void BattleWindow::toggleQueueVisibility()
|
||||
|
@ -300,6 +300,11 @@ void InputHandler::fetchEvents()
|
||||
}
|
||||
}
|
||||
|
||||
bool InputHandler::isKeyboardCmdDown() const
|
||||
{
|
||||
return keyboardHandler->isKeyboardCmdDown();
|
||||
}
|
||||
|
||||
bool InputHandler::isKeyboardCtrlDown() const
|
||||
{
|
||||
return keyboardHandler->isKeyboardCtrlDown();
|
||||
|
@ -88,6 +88,7 @@ public:
|
||||
|
||||
/// returns true if chosen keyboard key is currently pressed down
|
||||
bool isKeyboardAltDown() const;
|
||||
bool isKeyboardCmdDown() const;
|
||||
bool isKeyboardCtrlDown() const;
|
||||
bool isKeyboardShiftDown() const;
|
||||
};
|
||||
|
@ -120,10 +120,20 @@ void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key)
|
||||
GH.events().dispatchShortcutReleased(shortcutsVector);
|
||||
}
|
||||
|
||||
bool InputSourceKeyboard::isKeyboardCmdDown() const
|
||||
{
|
||||
#ifdef VCMI_APPLE
|
||||
return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LGUI] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RGUI];
|
||||
#else
|
||||
return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL];
|
||||
#endif
|
||||
}
|
||||
|
||||
bool InputSourceKeyboard::isKeyboardCtrlDown() const
|
||||
{
|
||||
#ifdef VCMI_MAC
|
||||
return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LGUI] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RGUI];
|
||||
#ifdef VCMI_APPLE
|
||||
return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL] ||
|
||||
SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LGUI] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RGUI];
|
||||
#else
|
||||
return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL];
|
||||
#endif
|
||||
|
@ -23,6 +23,7 @@ public:
|
||||
void handleEventKeyUp(const SDL_KeyboardEvent & current);
|
||||
|
||||
bool isKeyboardAltDown() const;
|
||||
bool isKeyboardCmdDown() const;
|
||||
bool isKeyboardCtrlDown() const;
|
||||
bool isKeyboardShiftDown() const;
|
||||
};
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
#include "GlobalLobbyInviteWindow.h"
|
||||
#include "GlobalLobbyLoginWindow.h"
|
||||
#include "GlobalLobbyObserver.h"
|
||||
#include "GlobalLobbyWindow.h"
|
||||
|
||||
#include "../CGameInfo.h"
|
||||
@ -144,6 +145,9 @@ void GlobalLobbyClient::receiveChatHistory(const JsonNode & json)
|
||||
if(lobbyWindowPtr && lobbyWindowPtr->isChannelOpen(channelType, channelName))
|
||||
lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
|
||||
}
|
||||
|
||||
if(lobbyWindowPtr && lobbyWindowPtr->isChannelOpen(channelType, channelName))
|
||||
lobbyWindowPtr->refreshChatText();
|
||||
}
|
||||
|
||||
void GlobalLobbyClient::receiveChatMessage(const JsonNode & json)
|
||||
@ -163,9 +167,13 @@ void GlobalLobbyClient::receiveChatMessage(const JsonNode & json)
|
||||
|
||||
auto lobbyWindowPtr = lobbyWindow.lock();
|
||||
if(lobbyWindowPtr)
|
||||
{
|
||||
lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
|
||||
lobbyWindowPtr->refreshChatText();
|
||||
|
||||
CCS->soundh->playSound(AudioPath::builtin("CHAT"));
|
||||
if(channelType == "player" || lobbyWindowPtr->isChannelOpen(channelType, channelName))
|
||||
CCS->soundh->playSound(AudioPath::builtin("CHAT"));
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json)
|
||||
@ -186,6 +194,9 @@ void GlobalLobbyClient::receiveActiveAccounts(const JsonNode & json)
|
||||
auto lobbyWindowPtr = lobbyWindow.lock();
|
||||
if(lobbyWindowPtr)
|
||||
lobbyWindowPtr->onActiveAccounts(activeAccounts);
|
||||
|
||||
for (auto const & window : GH.windows().findWindows<GlobalLobbyObserver>())
|
||||
window->onActiveAccounts(activeAccounts);
|
||||
}
|
||||
|
||||
void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
|
||||
@ -213,6 +224,15 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
|
||||
account.displayName = jsonParticipant["displayName"].String();
|
||||
room.participants.push_back(account);
|
||||
}
|
||||
|
||||
for(const auto & jsonParticipant : jsonEntry["invited"].Vector())
|
||||
{
|
||||
GlobalLobbyAccount account;
|
||||
account.accountID = jsonParticipant["accountID"].String();
|
||||
account.displayName = jsonParticipant["displayName"].String();
|
||||
room.invited.push_back(account);
|
||||
}
|
||||
|
||||
room.playerLimit = jsonEntry["playerLimit"].Integer();
|
||||
|
||||
activeRooms.push_back(room);
|
||||
@ -220,7 +240,10 @@ void GlobalLobbyClient::receiveActiveGameRooms(const JsonNode & json)
|
||||
|
||||
auto lobbyWindowPtr = lobbyWindow.lock();
|
||||
if(lobbyWindowPtr)
|
||||
lobbyWindowPtr->onActiveRooms(activeRooms);
|
||||
lobbyWindowPtr->onActiveGameRooms(activeRooms);
|
||||
|
||||
for (auto const & window : GH.windows().findWindows<GlobalLobbyObserver>())
|
||||
window->onActiveGameRooms(activeRooms);
|
||||
}
|
||||
|
||||
void GlobalLobbyClient::receiveMatchesHistory(const JsonNode & json)
|
||||
@ -246,6 +269,7 @@ void GlobalLobbyClient::receiveMatchesHistory(const JsonNode & json)
|
||||
account.displayName = jsonParticipant["displayName"].String();
|
||||
room.participants.push_back(account);
|
||||
}
|
||||
|
||||
room.playerLimit = jsonEntry["playerLimit"].Integer();
|
||||
|
||||
matchesHistory.push_back(room);
|
||||
@ -270,6 +294,7 @@ void GlobalLobbyClient::receiveInviteReceived(const JsonNode & json)
|
||||
|
||||
lobbyWindowPtr->onGameChatMessage("System", message, time, "player", accountID);
|
||||
lobbyWindowPtr->onInviteReceived(gameRoomID);
|
||||
lobbyWindowPtr->refreshChatText();
|
||||
}
|
||||
|
||||
CCS->soundh->playSound(AudioPath::builtin("CHAT"));
|
||||
@ -552,6 +577,11 @@ void GlobalLobbyClient::resetMatchState()
|
||||
currentGameRoomUUID.clear();
|
||||
}
|
||||
|
||||
const std::string & GlobalLobbyClient::getCurrentGameRoomID() const
|
||||
{
|
||||
return currentGameRoomUUID;
|
||||
}
|
||||
|
||||
void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
|
||||
{
|
||||
if (!isLoggedIn())
|
||||
@ -573,5 +603,15 @@ void GlobalLobbyClient::sendMatchChatMessage(const std::string & messageText)
|
||||
|
||||
bool GlobalLobbyClient::isInvitedToRoom(const std::string & gameRoomID)
|
||||
{
|
||||
return activeInvites.count(gameRoomID) > 0;
|
||||
if (activeInvites.count(gameRoomID) > 0)
|
||||
return true;
|
||||
|
||||
const auto & gameRoom = CSH->getGlobalLobby().getActiveRoomByName(gameRoomID);
|
||||
for (auto const & invited : gameRoom.invited)
|
||||
{
|
||||
if (invited.accountID == getAccountID())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ public:
|
||||
/// Returns active room by ID. Throws out-of-range on failure
|
||||
const GlobalLobbyRoom & getActiveRoomByName(const std::string & roomUUID) const;
|
||||
|
||||
const std::string & getCurrentGameRoomID() const;
|
||||
const std::string & getAccountID() const;
|
||||
const std::string & getAccountCookie() const;
|
||||
const std::string & getAccountDisplayName() const;
|
||||
|
@ -29,6 +29,7 @@ struct GlobalLobbyRoom
|
||||
std::string startDateFormatted;
|
||||
ModCompatibilityInfo modList;
|
||||
std::vector<GlobalLobbyAccount> participants;
|
||||
std::vector<GlobalLobbyAccount> invited;
|
||||
int playerLimit;
|
||||
};
|
||||
|
||||
|
@ -31,10 +31,33 @@ GlobalLobbyInviteAccountCard::GlobalLobbyInviteAccountCard(const GlobalLobbyAcco
|
||||
pos.h = 40;
|
||||
addUsedEvents(LCLICK);
|
||||
|
||||
bool thisAccountInvited = false;
|
||||
const auto & myRoomID = CSH->getGlobalLobby().getCurrentGameRoomID();
|
||||
if (!myRoomID.empty())
|
||||
{
|
||||
const auto & myRoom = CSH->getGlobalLobby().getActiveRoomByName(myRoomID);
|
||||
for (auto const & invited : myRoom.invited)
|
||||
{
|
||||
if (invited.accountID == accountID)
|
||||
{
|
||||
thisAccountInvited = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
||||
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
|
||||
if (thisAccountInvited)
|
||||
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::WHITE, 1);
|
||||
else
|
||||
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
|
||||
|
||||
labelName = std::make_shared<CLabel>(5, 10, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::WHITE, accountDescription.displayName);
|
||||
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
|
||||
|
||||
if (thisAccountInvited)
|
||||
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.room.state.invited").toString());
|
||||
else
|
||||
labelStatus = std::make_shared<CLabel>(5, 30, FONT_SMALL, ETextAlignment::CENTERLEFT, Colors::YELLOW, accountDescription.status);
|
||||
}
|
||||
|
||||
void GlobalLobbyInviteAccountCard::clickPressed(const Point & cursorPosition)
|
||||
@ -52,7 +75,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
|
||||
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
||||
|
||||
pos.w = 236;
|
||||
pos.h = 400;
|
||||
pos.h = 420;
|
||||
|
||||
filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
|
||||
filledBackground->playerColored(PlayerColor(1));
|
||||
@ -69,10 +92,25 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
|
||||
return std::make_shared<CIntObject>();
|
||||
};
|
||||
|
||||
listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 304), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
|
||||
accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 300, 300));
|
||||
listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
|
||||
accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 320, 320));
|
||||
|
||||
buttonClose = std::make_shared<CButton>(Point(86, 364), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); } );
|
||||
buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); } );
|
||||
|
||||
center();
|
||||
}
|
||||
|
||||
void GlobalLobbyInviteWindow::onActiveGameRooms(const std::vector<GlobalLobbyRoom> & rooms)
|
||||
{
|
||||
accountList->reset();
|
||||
redraw();
|
||||
}
|
||||
|
||||
void GlobalLobbyInviteWindow::onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts)
|
||||
{
|
||||
if (accountList->size() == accounts.size())
|
||||
accountList->reset();
|
||||
else
|
||||
accountList->resize(accounts.size());
|
||||
redraw();
|
||||
}
|
||||
|
@ -9,6 +9,8 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "GlobalLobbyObserver.h"
|
||||
|
||||
#include "../windows/CWindowObject.h"
|
||||
|
||||
class CLabel;
|
||||
@ -18,7 +20,7 @@ class CListBox;
|
||||
class CButton;
|
||||
struct GlobalLobbyAccount;
|
||||
|
||||
class GlobalLobbyInviteWindow : public CWindowObject
|
||||
class GlobalLobbyInviteWindow final : public CWindowObject, public GlobalLobbyObserver
|
||||
{
|
||||
std::shared_ptr<FilledTexturePlayerColored> filledBackground;
|
||||
std::shared_ptr<CLabel> labelTitle;
|
||||
@ -26,6 +28,9 @@ class GlobalLobbyInviteWindow : public CWindowObject
|
||||
std::shared_ptr<TransparentFilledRectangle> listBackground;
|
||||
std::shared_ptr<CButton> buttonClose;
|
||||
|
||||
void onActiveGameRooms(const std::vector<GlobalLobbyRoom> & rooms) override;
|
||||
void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts) override;
|
||||
|
||||
public:
|
||||
GlobalLobbyInviteWindow();
|
||||
};
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/WindowHandler.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/Images.h"
|
||||
#include "../widgets/GraphicalPrimitiveCanvas.h"
|
||||
#include "../widgets/MiscWidgets.h"
|
||||
@ -45,7 +46,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
|
||||
labelUsernameTitle = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
|
||||
labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
|
||||
backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
|
||||
inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
|
||||
inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true);
|
||||
buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
|
||||
buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
|
||||
labelStatus = std::make_shared<CTextBox>( "", Rect(15, 115, 255, 60), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
|
||||
@ -71,10 +72,10 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
|
||||
toggleMode->setSelected(1);
|
||||
|
||||
filledBackground->playerColored(PlayerColor(1));
|
||||
inputUsername->cb += [this](const std::string & text)
|
||||
inputUsername->setCallback([this](const std::string & text)
|
||||
{
|
||||
this->buttonLogin->block(text.empty());
|
||||
};
|
||||
});
|
||||
|
||||
center();
|
||||
}
|
||||
|
23
client/globalLobby/GlobalLobbyObserver.h
Normal file
23
client/globalLobby/GlobalLobbyObserver.h
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* GlobalLobbyObserver.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;
|
||||
struct GlobalLobbyRoom;
|
||||
|
||||
/// Interface for windows that want to receive updates whenever state of global lobby changes
|
||||
class GlobalLobbyObserver
|
||||
{
|
||||
public:
|
||||
virtual void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts) {}
|
||||
virtual void onActiveGameRooms(const std::vector<GlobalLobbyRoom> & rooms) {}
|
||||
|
||||
virtual ~GlobalLobbyObserver() = default;
|
||||
};
|
@ -76,7 +76,11 @@ static const std::string getJoinRoomErrorMessage(const GlobalLobbyRoom & roomDes
|
||||
if (gameStarted)
|
||||
return "vcmi.lobby.preview.error.busy";
|
||||
|
||||
if (VCMI_VERSION_STRING != roomDescription.gameVersion)
|
||||
CModVersion localVersion = CModVersion::fromString(VCMI_VERSION_STRING);
|
||||
CModVersion hostVersion = CModVersion::fromString(roomDescription.gameVersion);
|
||||
|
||||
// 1.5.X can play with each other, but not with 1.X.Y
|
||||
if (localVersion.major != hostVersion.major || localVersion.minor != hostVersion.minor)
|
||||
return "vcmi.lobby.preview.error.version";
|
||||
|
||||
if (roomDescription.playerLimit == roomDescription.participants.size())
|
||||
|
@ -20,7 +20,9 @@
|
||||
#include "../CServerHandler.h"
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/WindowHandler.h"
|
||||
#include "../render/Colors.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/GraphicalPrimitiveCanvas.h"
|
||||
#include "../widgets/Images.h"
|
||||
#include "../widgets/MiscWidgets.h"
|
||||
@ -231,7 +233,7 @@ GlobalLobbyRoomCard::GlobalLobbyRoomCard(GlobalLobbyWindow * window, const Globa
|
||||
pos.w = 230;
|
||||
pos.h = 40;
|
||||
|
||||
if (window->isInviteUnread(roomDescription.gameRoomID))
|
||||
if (hasInvite)
|
||||
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), Colors::WHITE, 1);
|
||||
else
|
||||
backgroundOverlay = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 64, 64, 64), 1);
|
||||
|
@ -18,10 +18,11 @@
|
||||
#include "../CServerHandler.h"
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/WindowHandler.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/Slider.h"
|
||||
#include "../widgets/ObjectLists.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
|
||||
#include "../../lib/CConfigHandler.h"
|
||||
#include "../../lib/Languages.h"
|
||||
#include "../../lib/MetaString.h"
|
||||
#include "../../lib/TextOperations.h"
|
||||
@ -51,13 +52,14 @@ void GlobalLobbyWindow::doOpenChannel(const std::string & channelType, const std
|
||||
currentChannelName = channelName;
|
||||
chatHistory.clear();
|
||||
unreadChannels.erase(channelType + "_" + channelName);
|
||||
widget->getGameChat()->setText("");
|
||||
|
||||
auto history = CSH->getGlobalLobby().getChannelHistory(channelType, channelName);
|
||||
|
||||
for(const auto & entry : history)
|
||||
onGameChatMessage(entry.displayName, entry.messageText, entry.timeFormatted, channelType, channelName);
|
||||
|
||||
refreshChatText();
|
||||
|
||||
MetaString text;
|
||||
text.appendTextID("vcmi.lobby.header.chat." + channelType);
|
||||
text.replaceRawString(roomDescription);
|
||||
@ -105,8 +107,6 @@ void GlobalLobbyWindow::doInviteAccount(const std::string & accountID)
|
||||
|
||||
void GlobalLobbyWindow::doJoinRoom(const std::string & roomID)
|
||||
{
|
||||
unreadInvites.erase(roomID);
|
||||
|
||||
JsonNode toSend;
|
||||
toSend["type"].String() = "joinGameRoom";
|
||||
toSend["gameRoomID"].String() = roomID;
|
||||
@ -133,8 +133,12 @@ void GlobalLobbyWindow::onGameChatMessage(const std::string & sender, const std:
|
||||
chatMessageFormatted.replaceRawString(message);
|
||||
|
||||
chatHistory += chatMessageFormatted.toString();
|
||||
|
||||
}
|
||||
void GlobalLobbyWindow::refreshChatText()
|
||||
{
|
||||
widget->getGameChat()->setText(chatHistory);
|
||||
if (widget->getGameChat()->slider)
|
||||
widget->getGameChat()->slider->scrollToMax();
|
||||
}
|
||||
|
||||
bool GlobalLobbyWindow::isChannelUnread(const std::string & channelType, const std::string & channelName) const
|
||||
@ -154,7 +158,7 @@ void GlobalLobbyWindow::onActiveAccounts(const std::vector<GlobalLobbyAccount> &
|
||||
widget->getAccountListHeader()->setText(text.toString());
|
||||
}
|
||||
|
||||
void GlobalLobbyWindow::onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms)
|
||||
void GlobalLobbyWindow::onActiveGameRooms(const std::vector<GlobalLobbyRoom> & rooms)
|
||||
{
|
||||
if (rooms.size() == widget->getRoomList()->size())
|
||||
widget->getRoomList()->reset();
|
||||
@ -180,15 +184,9 @@ void GlobalLobbyWindow::onMatchesHistory(const std::vector<GlobalLobbyRoom> & hi
|
||||
|
||||
void GlobalLobbyWindow::onInviteReceived(const std::string & invitedRoomID)
|
||||
{
|
||||
unreadInvites.insert(invitedRoomID);
|
||||
widget->getRoomList()->reset();
|
||||
}
|
||||
|
||||
bool GlobalLobbyWindow::isInviteUnread(const std::string & gameRoomID) const
|
||||
{
|
||||
return unreadInvites.count(gameRoomID) > 0;
|
||||
}
|
||||
|
||||
void GlobalLobbyWindow::onJoinedRoom()
|
||||
{
|
||||
widget->getAccountList()->reset();
|
||||
|
@ -9,13 +9,14 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "GlobalLobbyObserver.h"
|
||||
#include "../windows/CWindowObject.h"
|
||||
|
||||
class GlobalLobbyWidget;
|
||||
struct GlobalLobbyAccount;
|
||||
struct GlobalLobbyRoom;
|
||||
|
||||
class GlobalLobbyWindow : public CWindowObject
|
||||
class GlobalLobbyWindow final : public CWindowObject, public GlobalLobbyObserver
|
||||
{
|
||||
std::string chatHistory;
|
||||
std::string currentChannelType;
|
||||
@ -23,7 +24,6 @@ class GlobalLobbyWindow : public CWindowObject
|
||||
|
||||
std::shared_ptr<GlobalLobbyWidget> widget;
|
||||
std::set<std::string> unreadChannels;
|
||||
std::set<std::string> unreadInvites;
|
||||
|
||||
public:
|
||||
GlobalLobbyWindow();
|
||||
@ -38,14 +38,15 @@ public:
|
||||
|
||||
/// Returns true if provided chat channel is the one that is currently open in UI
|
||||
bool isChannelOpen(const std::string & channelType, const std::string & channelName) const;
|
||||
/// Returns true if provided channel has unread messages (only messages that were received after login)
|
||||
bool isChannelUnread(const std::string & channelType, const std::string & channelName) const;
|
||||
bool isInviteUnread(const std::string & gameRoomID) const;
|
||||
|
||||
// Callbacks for network packs
|
||||
|
||||
void onGameChatMessage(const std::string & sender, const std::string & message, const std::string & when, const std::string & channelType, const std::string & channelName);
|
||||
void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts);
|
||||
void onActiveRooms(const std::vector<GlobalLobbyRoom> & rooms);
|
||||
void refreshChatText();
|
||||
void onActiveAccounts(const std::vector<GlobalLobbyAccount> & accounts) override;
|
||||
void onActiveGameRooms(const std::vector<GlobalLobbyRoom> & rooms) override;
|
||||
void onMatchesHistory(const std::vector<GlobalLobbyRoom> & history);
|
||||
void onInviteReceived(const std::string & invitedRoomID);
|
||||
void onJoinedRoom();
|
||||
|
@ -165,6 +165,11 @@ bool CGuiHandler::isKeyboardCtrlDown() const
|
||||
return inputHandlerInstance->isKeyboardCtrlDown();
|
||||
}
|
||||
|
||||
bool CGuiHandler::isKeyboardCmdDown() const
|
||||
{
|
||||
return inputHandlerInstance->isKeyboardCmdDown();
|
||||
}
|
||||
|
||||
bool CGuiHandler::isKeyboardAltDown() const
|
||||
{
|
||||
return inputHandlerInstance->isKeyboardAltDown();
|
||||
|
@ -62,9 +62,17 @@ public:
|
||||
/// May not match size of window if user has UI scaling different from 100%
|
||||
Point screenDimensions() const;
|
||||
|
||||
/// returns true if chosen keyboard key is currently pressed down
|
||||
/// returns true if Alt is currently pressed down
|
||||
bool isKeyboardAltDown() const;
|
||||
/// returns true if Ctrl is currently pressed down
|
||||
/// on Apple system, this also tests for Cmd key
|
||||
/// For use with keyboard-based events
|
||||
bool isKeyboardCtrlDown() const;
|
||||
/// on Apple systems, returns true if Cmd key is pressed
|
||||
/// on other systems, returns true if Ctrl is pressed
|
||||
/// /// For use with mouse-based events
|
||||
bool isKeyboardCmdDown() const;
|
||||
/// returns true if Shift is currently pressed down
|
||||
bool isKeyboardShiftDown() const;
|
||||
|
||||
void startTextInput(const Rect & where);
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/ComboBox.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/GraphicalPrimitiveCanvas.h"
|
||||
#include "../widgets/ObjectLists.h"
|
||||
#include "../widgets/Slider.h"
|
||||
@ -610,19 +611,17 @@ std::shared_ptr<CTextInput> InterfaceObjectConfigurable::buildTextInput(const Js
|
||||
auto rect = readRect(config["rect"]);
|
||||
auto offset = readPosition(config["backgroundOffset"]);
|
||||
auto bgName = ImagePath::fromJson(config["background"]);
|
||||
auto result = std::make_shared<CTextInput>(rect, offset, bgName, 0);
|
||||
auto result = std::make_shared<CTextInput>(rect, offset, bgName);
|
||||
if(!config["alignment"].isNull())
|
||||
result->alignment = readTextAlignment(config["alignment"]);
|
||||
result->setAlignment(readTextAlignment(config["alignment"]));
|
||||
if(!config["font"].isNull())
|
||||
result->font = readFont(config["font"]);
|
||||
result->setFont(readFont(config["font"]));
|
||||
if(!config["color"].isNull())
|
||||
result->setColor(readColor(config["color"]));
|
||||
if(!config["text"].isNull() && config["text"].isString())
|
||||
result->setText(config["text"].String()); //for input field raw string is taken
|
||||
if(!config["callback"].isNull())
|
||||
result->cb += callbacks_string.at(config["callback"].String());
|
||||
if(!config["help"].isNull())
|
||||
result->setHelpText(readText(config["help"]));
|
||||
result->setCallback(callbacks_string.at(config["callback"].String()));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -230,6 +230,16 @@ void CLobbyScreen::toggleChat()
|
||||
|
||||
void CLobbyScreen::updateAfterStateChange()
|
||||
{
|
||||
if(CSH->isHost() && screenType == ESelectionScreen::newGame)
|
||||
{
|
||||
bool isMultiplayer = CSH->loadMode == ELoadMode::MULTI;
|
||||
ExtraOptionsInfo info = SEL->getStartInfo()->extraOptionsInfo;
|
||||
info.cheatsAllowed = isMultiplayer ? persistentStorage["startExtraOptions"]["multiPlayer"]["cheatsAllowed"].Bool() : !persistentStorage["startExtraOptions"]["singlePlayer"]["cheatsNotAllowed"].Bool();
|
||||
info.unlimitedReplay = persistentStorage["startExtraOptions"][isMultiplayer ? "multiPlayer" : "singlePlayer"]["unlimitedReplay"].Bool();
|
||||
if(info.cheatsAllowed != CSH->si->extraOptionsInfo.cheatsAllowed || info.unlimitedReplay != CSH->si->extraOptionsInfo.unlimitedReplay)
|
||||
CSH->setExtraOptionsInfo(info);
|
||||
}
|
||||
|
||||
if(CSH->mi)
|
||||
{
|
||||
if (tabOpt)
|
||||
|
@ -17,7 +17,7 @@
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/Shortcut.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
|
||||
#include "../../CCallback.h"
|
||||
#include "../../lib/CConfigHandler.h"
|
||||
|
@ -30,6 +30,7 @@
|
||||
#include "../mainmenu/CMainMenu.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/GraphicalPrimitiveCanvas.h"
|
||||
#include "../widgets/Images.h"
|
||||
#include "../widgets/ObjectLists.h"
|
||||
@ -362,7 +363,7 @@ CChatBox::CChatBox(const Rect & rect)
|
||||
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<TransparentFilledRectangle>(textInputArea, ColorRGBA(0,0,0,192));
|
||||
inputBox = std::make_shared<CTextInput>(textInputArea, EFonts::FONT_SMALL, nullptr, ETextAlignment::TOPLEFT, true);
|
||||
inputBox = std::make_shared<CTextInput>(textInputArea, EFonts::FONT_SMALL, ETextAlignment::CENTERLEFT, true);
|
||||
inputBox->removeUsedEvents(KEYBOARD);
|
||||
chatHistory = std::make_shared<CTextBox>("", chatHistoryArea, 1);
|
||||
|
||||
|
@ -10,9 +10,11 @@
|
||||
|
||||
#include "StdInc.h"
|
||||
#include "ExtraOptionsTab.h"
|
||||
#include "../widgets/Images.h"
|
||||
|
||||
ExtraOptionsTab::ExtraOptionsTab()
|
||||
: OptionsTabBase(JsonPath::builtin("config/widgets/extraOptionsTab.json"))
|
||||
{
|
||||
|
||||
if(auto textureCampaignOverdraw = widget<CFilledTexture>("textureCampaignOverdraw"))
|
||||
textureCampaignOverdraw->disable();
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "../render/IFont.h"
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/ComboBox.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/Images.h"
|
||||
#include "../widgets/MiscWidgets.h"
|
||||
@ -892,7 +893,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con
|
||||
labelPlayerName = std::make_shared<CLabel>(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95);
|
||||
else
|
||||
{
|
||||
labelPlayerNameEdit = std::make_shared<CTextInput>(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, ETextAlignment::CENTER, false);
|
||||
labelPlayerNameEdit = std::make_shared<CTextInput>(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, ETextAlignment::CENTER, false);
|
||||
labelPlayerNameEdit->setText(name);
|
||||
}
|
||||
labelWhoCanPlay = std::make_shared<CMultiLineLabel>(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]);
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include "CSelectionBase.h"
|
||||
|
||||
#include "../widgets/ComboBox.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/Images.h"
|
||||
#include "../widgets/Slider.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
@ -22,6 +23,7 @@
|
||||
#include "../../lib/Languages.h"
|
||||
#include "../../lib/MetaString.h"
|
||||
#include "../../lib/CGeneralTextHandler.h"
|
||||
#include "../../lib/CConfigHandler.h"
|
||||
|
||||
static std::string timeToString(int time)
|
||||
{
|
||||
@ -100,12 +102,18 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
|
||||
});
|
||||
|
||||
addCallback("setCheatAllowed", [&](int index){
|
||||
bool isMultiplayer = CSH->loadMode == ELoadMode::MULTI;
|
||||
Settings entry = persistentStorage.write["startExtraOptions"][isMultiplayer ? "multiPlayer" : "singlePlayer"][isMultiplayer ? "cheatsAllowed" : "cheatsNotAllowed"];
|
||||
entry->Bool() = isMultiplayer ? index : !index;
|
||||
ExtraOptionsInfo info = SEL->getStartInfo()->extraOptionsInfo;
|
||||
info.cheatsAllowed = index;
|
||||
CSH->setExtraOptionsInfo(info);
|
||||
});
|
||||
|
||||
addCallback("setUnlimitedReplay", [&](int index){
|
||||
bool isMultiplayer = CSH->loadMode == ELoadMode::MULTI;
|
||||
Settings entry = persistentStorage.write["startExtraOptions"][isMultiplayer ? "multiPlayer" : "singlePlayer"]["unlimitedReplay"];
|
||||
entry->Bool() = index;
|
||||
ExtraOptionsInfo info = SEL->getStartInfo()->extraOptionsInfo;
|
||||
info.unlimitedReplay = index;
|
||||
CSH->setExtraOptionsInfo(info);
|
||||
@ -173,7 +181,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
|
||||
tinfo.baseTimer = time;
|
||||
CSH->setTurnTimerInfo(tinfo);
|
||||
if(auto ww = widget<CTextInput>("chessFieldBase"))
|
||||
ww->setText(timeToString(time), false);
|
||||
ww->setText(timeToString(time));
|
||||
}
|
||||
});
|
||||
addCallback("parseAndSetTimer_turn", [this, parseTimerString](const std::string & str){
|
||||
@ -184,7 +192,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
|
||||
tinfo.turnTimer = time;
|
||||
CSH->setTurnTimerInfo(tinfo);
|
||||
if(auto ww = widget<CTextInput>("chessFieldTurn"))
|
||||
ww->setText(timeToString(time), false);
|
||||
ww->setText(timeToString(time));
|
||||
}
|
||||
});
|
||||
addCallback("parseAndSetTimer_battle", [this, parseTimerString](const std::string & str){
|
||||
@ -195,7 +203,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
|
||||
tinfo.battleTimer = time;
|
||||
CSH->setTurnTimerInfo(tinfo);
|
||||
if(auto ww = widget<CTextInput>("chessFieldBattle"))
|
||||
ww->setText(timeToString(time), false);
|
||||
ww->setText(timeToString(time));
|
||||
}
|
||||
});
|
||||
addCallback("parseAndSetTimer_unit", [this, parseTimerString](const std::string & str){
|
||||
@ -206,7 +214,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath)
|
||||
tinfo.unitTimer = time;
|
||||
CSH->setTurnTimerInfo(tinfo);
|
||||
if(auto ww = widget<CTextInput>("chessFieldUnit"))
|
||||
ww->setText(timeToString(time), false);
|
||||
ww->setText(timeToString(time));
|
||||
}
|
||||
});
|
||||
|
||||
@ -389,13 +397,13 @@ void OptionsTabBase::recreate(bool campaign)
|
||||
}
|
||||
|
||||
if(auto ww = widget<CTextInput>("chessFieldBase"))
|
||||
ww->setText(timeToString(turnTimerRemote.baseTimer), false);
|
||||
ww->setText(timeToString(turnTimerRemote.baseTimer));
|
||||
if(auto ww = widget<CTextInput>("chessFieldTurn"))
|
||||
ww->setText(timeToString(turnTimerRemote.turnTimer), false);
|
||||
ww->setText(timeToString(turnTimerRemote.turnTimer));
|
||||
if(auto ww = widget<CTextInput>("chessFieldBattle"))
|
||||
ww->setText(timeToString(turnTimerRemote.battleTimer), false);
|
||||
ww->setText(timeToString(turnTimerRemote.battleTimer));
|
||||
if(auto ww = widget<CTextInput>("chessFieldUnit"))
|
||||
ww->setText(timeToString(turnTimerRemote.unitTimer), false);
|
||||
ww->setText(timeToString(turnTimerRemote.unitTimer));
|
||||
|
||||
if(auto w = widget<ComboBox>("timerModeSwitch"))
|
||||
{
|
||||
@ -410,18 +418,15 @@ void OptionsTabBase::recreate(bool campaign)
|
||||
if(auto buttonCheatAllowed = widget<CToggleButton>("buttonCheatAllowed"))
|
||||
{
|
||||
buttonCheatAllowed->setSelectedSilent(SEL->getStartInfo()->extraOptionsInfo.cheatsAllowed);
|
||||
buttonCheatAllowed->block(SEL->screenType == ESelectionScreen::loadGame);
|
||||
buttonCheatAllowed->block(CSH->isGuest());
|
||||
}
|
||||
|
||||
if(auto buttonUnlimitedReplay = widget<CToggleButton>("buttonUnlimitedReplay"))
|
||||
{
|
||||
buttonUnlimitedReplay->setSelectedSilent(SEL->getStartInfo()->extraOptionsInfo.unlimitedReplay);
|
||||
buttonUnlimitedReplay->block(SEL->screenType == ESelectionScreen::loadGame);
|
||||
buttonUnlimitedReplay->block(CSH->isGuest());
|
||||
}
|
||||
|
||||
if(auto textureCampaignOverdraw = widget<CFilledTexture>("textureCampaignOverdraw"))
|
||||
{
|
||||
if(!campaign)
|
||||
textureCampaignOverdraw->disable();
|
||||
}
|
||||
textureCampaignOverdraw->setEnabled(campaign);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "../gui/WindowHandler.h"
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/MiscWidgets.h"
|
||||
#include "../widgets/ObjectLists.h"
|
||||
#include "../widgets/Slider.h"
|
||||
@ -163,8 +164,8 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
|
||||
{
|
||||
background = std::make_shared<CPicture>(ImagePath::builtin("SCSELBCK.bmp"), 0, 6);
|
||||
pos = background->pos;
|
||||
inputName = std::make_shared<CTextInput>(inputNameRect, Point(-32, -25), ImagePath::builtin("GSSTRIP.bmp"), 0);
|
||||
inputName->filters += CTextInput::filenameFilter;
|
||||
inputName = std::make_shared<CTextInput>(inputNameRect, Point(-32, -25), ImagePath::builtin("GSSTRIP.bmp"));
|
||||
inputName->setFilterFilename();
|
||||
labelMapSizes = std::make_shared<CLabel>(87, 62, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]);
|
||||
|
||||
// TODO: Global constants?
|
||||
@ -313,7 +314,7 @@ void SelectionTab::clickReleased(const Point & cursorPosition)
|
||||
{
|
||||
select(line);
|
||||
}
|
||||
#ifdef VCMI_IOS
|
||||
#ifdef VCMI_MOBILE
|
||||
// focus input field if clicked inside it
|
||||
else if(inputName && inputName->isActive() && inputNameRect.isInside(cursorPosition))
|
||||
inputName->giveFocus();
|
||||
@ -358,6 +359,17 @@ void SelectionTab::clickDouble(const Point & cursorPosition)
|
||||
if(itemIndex >= curItems.size())
|
||||
return;
|
||||
|
||||
auto clickedItem = curItems[itemIndex];
|
||||
auto selectedItem = getSelectedMapInfo();
|
||||
|
||||
if (clickedItem != selectedItem)
|
||||
{
|
||||
// double-click BUT player hit different item than he had selected
|
||||
// ignore - clickReleased would still trigger and update selection.
|
||||
// After which another (3rd) click if it happens would still register as double-click
|
||||
return;
|
||||
}
|
||||
|
||||
if(itemIndex >= 0 && curItems[itemIndex]->isFolder)
|
||||
{
|
||||
select(position);
|
||||
|
@ -14,11 +14,12 @@
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/WindowHandler.h"
|
||||
#include "../gui/Shortcut.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/Images.h"
|
||||
#include "../widgets/GraphicalPrimitiveCanvas.h"
|
||||
#include "../windows/InfoWindows.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
#include "../render/Canvas.h"
|
||||
|
||||
#include "../CGameInfo.h"
|
||||
@ -372,7 +373,7 @@ CHighScoreInput::CHighScoreInput(std::string playerName, std::function<void(std:
|
||||
buttonOk = std::make_shared<CButton>(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT);
|
||||
buttonCancel = std::make_shared<CButton>(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<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
|
||||
textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, nullptr, ETextAlignment::CENTER, true);
|
||||
textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, ETextAlignment::CENTER, true);
|
||||
textInput->setText(playerName);
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@
|
||||
#include "../globalLobby/GlobalLobbyWindow.h"
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/MiscWidgets.h"
|
||||
#include "../widgets/ObjectLists.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
@ -454,7 +455,7 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType)
|
||||
statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 465, 440, 18), 7, 465));
|
||||
playerName = std::make_shared<CTextInput>(Rect(19, 436, 334, 16), background->getSurface());
|
||||
playerName->setText(getPlayerName());
|
||||
playerName->cb += std::bind(&CMultiMode::onNameChange, this, _1);
|
||||
playerName->setCallback(std::bind(&CMultiMode::onNameChange, this, _1));
|
||||
|
||||
buttonHotseat = std::make_shared<CButton>(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this));
|
||||
buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this));
|
||||
@ -513,15 +514,15 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S
|
||||
for(int i = 0; i < inputNames.size(); i++)
|
||||
{
|
||||
inputNames[i] = std::make_shared<CTextInput>(Rect(60, 85 + i * 30, 280, 16), background->getSurface());
|
||||
inputNames[i]->cb += std::bind(&CMultiPlayers::onChange, this, _1);
|
||||
inputNames[i]->setCallback(std::bind(&CMultiPlayers::onChange, this, _1));
|
||||
}
|
||||
|
||||
buttonOk = std::make_shared<CButton>(Point(95, 338), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), EShortcut::GLOBAL_ACCEPT);
|
||||
buttonCancel = std::make_shared<CButton>(Point(205, 338), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
|
||||
statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 381, 348, 18), 7, 381));
|
||||
|
||||
inputNames[0]->setText(firstPlayer, true);
|
||||
#ifndef VCMI_IOS
|
||||
inputNames[0]->setText(firstPlayer);
|
||||
#ifndef VCMI_MOBILE
|
||||
inputNames[0]->giveFocus();
|
||||
#endif
|
||||
}
|
||||
@ -564,13 +565,14 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host)
|
||||
else
|
||||
{
|
||||
textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverAddressEnter"));
|
||||
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);
|
||||
inputAddress->setCallback(std::bind(&CSimpleJoinScreen::onChange, this, _1));
|
||||
inputPort->setCallback(std::bind(&CSimpleJoinScreen::onChange, this, _1));
|
||||
inputPort->setFilterNumber(0, 65535);
|
||||
inputAddress->giveFocus();
|
||||
}
|
||||
inputAddress->setText(host ? CSH->getLocalHostname() : CSH->getRemoteHostname(), true);
|
||||
inputPort->setText(std::to_string(host ? CSH->getLocalPort() : CSH->getRemotePort()), true);
|
||||
inputAddress->setText(host ? CSH->getLocalHostname() : CSH->getRemoteHostname());
|
||||
inputPort->setText(std::to_string(host ? CSH->getLocalPort() : CSH->getRemotePort()));
|
||||
buttonOk->block(inputAddress->getText().empty() || inputPort->getText().empty());
|
||||
|
||||
buttonCancel = std::make_shared<CButton>(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<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
|
||||
|
@ -335,8 +335,15 @@ void CSelectableComponent::clickPressed(const Point & cursorPosition)
|
||||
|
||||
void CSelectableComponent::clickDouble(const Point & cursorPosition)
|
||||
{
|
||||
if(onChoose)
|
||||
onChoose();
|
||||
if (!selected)
|
||||
{
|
||||
clickPressed(cursorPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (onChoose)
|
||||
onChoose();
|
||||
}
|
||||
}
|
||||
|
||||
void CSelectableComponent::init()
|
||||
|
@ -485,7 +485,7 @@ bool CGarrisonSlot::handleSplittingShortcuts()
|
||||
{
|
||||
const bool isAlt = GH.isKeyboardAltDown();
|
||||
const bool isLShift = GH.isKeyboardShiftDown();
|
||||
const bool isLCtrl = GH.isKeyboardCtrlDown();
|
||||
const bool isLCtrl = GH.isKeyboardCmdDown();
|
||||
|
||||
if(!isAlt && !isLShift && !isLCtrl)
|
||||
return false; // This is only case when return false
|
||||
|
373
client/widgets/CTextInput.cpp
Normal file
373
client/widgets/CTextInput.cpp
Normal file
@ -0,0 +1,373 @@
|
||||
/*
|
||||
* CTextInput.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 "CTextInput.h"
|
||||
|
||||
#include "Images.h"
|
||||
#include "TextControls.h"
|
||||
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/Shortcut.h"
|
||||
#include "../render/Graphics.h"
|
||||
#include "../render/IFont.h"
|
||||
|
||||
#include "../../lib/TextOperations.h"
|
||||
|
||||
std::list<CFocusable *> CFocusable::focusables;
|
||||
CFocusable * CFocusable::inputWithFocus;
|
||||
|
||||
CTextInput::CTextInput(const Rect & Pos)
|
||||
:originalAlignment(ETextAlignment::CENTERLEFT)
|
||||
{
|
||||
pos += Pos.topLeft();
|
||||
pos.h = Pos.h;
|
||||
pos.w = Pos.w;
|
||||
|
||||
addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT);
|
||||
}
|
||||
|
||||
void CTextInput::createLabel(bool giveFocusToInput)
|
||||
{
|
||||
OBJ_CONSTRUCTION;
|
||||
label = std::make_shared<CLabel>();
|
||||
label->pos = pos;
|
||||
label->alignment = originalAlignment;
|
||||
|
||||
#if !defined(VCMI_MOBILE)
|
||||
if(giveFocusToInput)
|
||||
giveFocus();
|
||||
#endif
|
||||
}
|
||||
|
||||
CTextInput::CTextInput(const Rect & Pos, EFonts font, ETextAlignment alignment, bool giveFocusToInput)
|
||||
: CTextInput(Pos)
|
||||
{
|
||||
originalAlignment = alignment;
|
||||
setRedrawParent(true);
|
||||
createLabel(giveFocusToInput);
|
||||
setFont(font);
|
||||
setAlignment(alignment);
|
||||
}
|
||||
|
||||
CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName)
|
||||
: CTextInput(Pos)
|
||||
{
|
||||
OBJ_CONSTRUCTION;
|
||||
if (!bgName.empty())
|
||||
background = std::make_shared<CPicture>(bgName, bgOffset.x, bgOffset.y);
|
||||
else
|
||||
setRedrawParent(true);
|
||||
|
||||
createLabel(true);
|
||||
}
|
||||
|
||||
CTextInput::CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf)
|
||||
: CTextInput(Pos)
|
||||
{
|
||||
OBJ_CONSTRUCTION;
|
||||
background = std::make_shared<CPicture>(srf, Pos);
|
||||
pos.w = background->pos.w;
|
||||
pos.h = background->pos.h;
|
||||
background->pos = pos;
|
||||
createLabel(true);
|
||||
}
|
||||
|
||||
void CTextInput::setFont(EFonts font)
|
||||
{
|
||||
label->font = font;
|
||||
}
|
||||
|
||||
void CTextInput::setColor(const ColorRGBA & color)
|
||||
{
|
||||
label->color = color;
|
||||
}
|
||||
|
||||
void CTextInput::setAlignment(ETextAlignment alignment)
|
||||
{
|
||||
originalAlignment = alignment;
|
||||
label->alignment = alignment;
|
||||
}
|
||||
|
||||
const std::string & CTextInput::getText() const
|
||||
{
|
||||
return currentText;
|
||||
}
|
||||
|
||||
void CTextInput::setCallback(const TextEditedCallback & cb)
|
||||
{
|
||||
assert(!onTextEdited);
|
||||
onTextEdited = cb;
|
||||
}
|
||||
|
||||
void CTextInput::setFilterFilename()
|
||||
{
|
||||
assert(!onTextFiltering);
|
||||
onTextFiltering = std::bind(&CTextInput::filenameFilter, _1, _2);
|
||||
}
|
||||
|
||||
void CTextInput::setFilterNumber(int minValue, int maxValue)
|
||||
{
|
||||
onTextFiltering = std::bind(&CTextInput::numberFilter, _1, _2, minValue, maxValue);
|
||||
}
|
||||
|
||||
std::string CTextInput::getVisibleText()
|
||||
{
|
||||
return hasFocus() ? currentText + composedText + "_" : currentText;
|
||||
}
|
||||
|
||||
void CTextInput::clickPressed(const Point & cursorPosition)
|
||||
{
|
||||
// attempt to give focus unconditionally, even if we already have it
|
||||
// this forces on-screen keyboard to show up again, even if player have closed it before
|
||||
giveFocus();
|
||||
}
|
||||
|
||||
void CTextInput::keyPressed(EShortcut key)
|
||||
{
|
||||
if(!hasFocus())
|
||||
return;
|
||||
|
||||
if(key == EShortcut::GLOBAL_MOVE_FOCUS)
|
||||
{
|
||||
moveFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
bool redrawNeeded = false;
|
||||
|
||||
switch(key)
|
||||
{
|
||||
case EShortcut::GLOBAL_BACKSPACE:
|
||||
if(!composedText.empty())
|
||||
{
|
||||
TextOperations::trimRightUnicode(composedText);
|
||||
redrawNeeded = true;
|
||||
}
|
||||
else if(!currentText.empty())
|
||||
{
|
||||
TextOperations::trimRightUnicode(currentText);
|
||||
redrawNeeded = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(redrawNeeded)
|
||||
{
|
||||
updateLabel();
|
||||
if(onTextEdited)
|
||||
onTextEdited(currentText);
|
||||
}
|
||||
}
|
||||
|
||||
void CTextInput::setText(const std::string & nText)
|
||||
{
|
||||
currentText = nText;
|
||||
updateLabel();
|
||||
}
|
||||
|
||||
void CTextInput::updateLabel()
|
||||
{
|
||||
std::string visibleText = getVisibleText();
|
||||
|
||||
label->alignment = originalAlignment;
|
||||
|
||||
while (graphics->fonts[label->font]->getStringWidth(visibleText) > pos.w)
|
||||
{
|
||||
label->alignment = ETextAlignment::CENTERRIGHT;
|
||||
visibleText = visibleText.substr(TextOperations::getUnicodeCharacterSize(visibleText[0]));
|
||||
}
|
||||
|
||||
label->setText(visibleText);
|
||||
}
|
||||
|
||||
void CTextInput::textInputed(const std::string & enteredText)
|
||||
{
|
||||
if(!hasFocus())
|
||||
return;
|
||||
std::string oldText = currentText;
|
||||
|
||||
setText(getText() + enteredText);
|
||||
|
||||
if(onTextFiltering)
|
||||
onTextFiltering(currentText, oldText);
|
||||
|
||||
if(currentText != oldText)
|
||||
{
|
||||
updateLabel();
|
||||
if(onTextEdited)
|
||||
onTextEdited(currentText);
|
||||
}
|
||||
composedText.clear();
|
||||
}
|
||||
|
||||
void CTextInput::textEdited(const std::string & enteredText)
|
||||
{
|
||||
if(!hasFocus())
|
||||
return;
|
||||
|
||||
composedText = enteredText;
|
||||
updateLabel();
|
||||
//onTextEdited(currentText + composedText);
|
||||
}
|
||||
|
||||
void CTextInput::filenameFilter(std::string & text, const std::string &oldText)
|
||||
{
|
||||
static const std::string forbiddenChars = "<>:\"/\\|?*\r\n"; //if we are entering a filename, some special characters won't be allowed
|
||||
size_t pos;
|
||||
while((pos = text.find_first_of(forbiddenChars)) != std::string::npos)
|
||||
text.erase(pos, 1);
|
||||
}
|
||||
|
||||
void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue)
|
||||
{
|
||||
assert(minValue < maxValue);
|
||||
|
||||
if(text.empty())
|
||||
text = "0";
|
||||
|
||||
size_t pos = 0;
|
||||
if(text[0] == '-') //allow '-' sign as first symbol only
|
||||
pos++;
|
||||
|
||||
while(pos < text.size())
|
||||
{
|
||||
if(text[pos] < '0' || text[pos] > '9')
|
||||
{
|
||||
text = oldText;
|
||||
return; //new text is not number.
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
try
|
||||
{
|
||||
int value = boost::lexical_cast<int>(text);
|
||||
if(value < minValue)
|
||||
text = std::to_string(minValue);
|
||||
else if(value > maxValue)
|
||||
text = std::to_string(maxValue);
|
||||
}
|
||||
catch(boost::bad_lexical_cast &)
|
||||
{
|
||||
//Should never happen. Unless I missed some cases
|
||||
logGlobal->warn("Warning: failed to convert %s to number!", text);
|
||||
text = oldText;
|
||||
}
|
||||
}
|
||||
|
||||
void CTextInput::activate()
|
||||
{
|
||||
CFocusable::activate();
|
||||
if (hasFocus())
|
||||
{
|
||||
#if defined(VCMI_MOBILE)
|
||||
//giveFocus();
|
||||
#else
|
||||
GH.startTextInput(pos);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void CTextInput::deactivate()
|
||||
{
|
||||
CFocusable::deactivate();
|
||||
if (hasFocus())
|
||||
{
|
||||
#if defined(VCMI_MOBILE)
|
||||
removeFocus();
|
||||
#else
|
||||
GH.stopTextInput();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void CTextInput::onFocusGot()
|
||||
{
|
||||
updateLabel();
|
||||
}
|
||||
|
||||
void CTextInput::onFocusLost()
|
||||
{
|
||||
updateLabel();
|
||||
}
|
||||
|
||||
void CFocusable::focusGot()
|
||||
{
|
||||
if (isActive())
|
||||
GH.startTextInput(pos);
|
||||
onFocusGot();
|
||||
}
|
||||
|
||||
void CFocusable::focusLost()
|
||||
{
|
||||
if (isActive())
|
||||
GH.stopTextInput();
|
||||
onFocusLost();
|
||||
}
|
||||
|
||||
CFocusable::CFocusable()
|
||||
{
|
||||
focusables.push_back(this);
|
||||
}
|
||||
|
||||
CFocusable::~CFocusable()
|
||||
{
|
||||
if(hasFocus())
|
||||
inputWithFocus = nullptr;
|
||||
|
||||
focusables -= this;
|
||||
}
|
||||
|
||||
bool CFocusable::hasFocus() const
|
||||
{
|
||||
return inputWithFocus == this;
|
||||
}
|
||||
|
||||
void CFocusable::giveFocus()
|
||||
{
|
||||
auto previousInput = inputWithFocus;
|
||||
inputWithFocus = this;
|
||||
|
||||
if(previousInput)
|
||||
previousInput->focusLost();
|
||||
|
||||
focusGot();
|
||||
}
|
||||
|
||||
void CFocusable::moveFocus()
|
||||
{
|
||||
auto i = vstd::find(focusables, this);
|
||||
auto ourIt = i;
|
||||
|
||||
for(i++; i != ourIt; i++)
|
||||
{
|
||||
if(i == focusables.end())
|
||||
i = focusables.begin();
|
||||
|
||||
if(*i == this)
|
||||
return;
|
||||
|
||||
if((*i)->isActive())
|
||||
{
|
||||
(*i)->giveFocus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CFocusable::removeFocus()
|
||||
{
|
||||
if(this == inputWithFocus)
|
||||
{
|
||||
inputWithFocus = nullptr;
|
||||
focusLost();
|
||||
}
|
||||
}
|
106
client/widgets/CTextInput.h
Normal file
106
client/widgets/CTextInput.h
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* CTextInput.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"
|
||||
#include "../gui/TextAlignment.h"
|
||||
#include "../render/EFont.h"
|
||||
|
||||
#include "../../lib/filesystem/ResourcePath.h"
|
||||
|
||||
class CLabel;
|
||||
class IImage;
|
||||
|
||||
/// UIElement which can get input focus
|
||||
class CFocusable : public CIntObject
|
||||
{
|
||||
friend void removeFocusFromActiveInput();
|
||||
|
||||
static std::atomic<int> usageIndex;
|
||||
static std::list<CFocusable *> focusables; //all existing objs
|
||||
static CFocusable * inputWithFocus; //who has focus now
|
||||
|
||||
void focusGot();
|
||||
void focusLost();
|
||||
|
||||
virtual void onFocusGot() = 0;
|
||||
virtual void onFocusLost() = 0;
|
||||
|
||||
public:
|
||||
void giveFocus(); //captures focus
|
||||
void moveFocus(); //moves focus to next active control (may be used for tab switching)
|
||||
void removeFocus(); //remove focus
|
||||
bool hasFocus() const;
|
||||
|
||||
CFocusable();
|
||||
~CFocusable();
|
||||
};
|
||||
|
||||
/// Text input box where players can enter text
|
||||
class CTextInput final : public CFocusable
|
||||
{
|
||||
using TextEditedCallback = std::function<void(const std::string &)>;
|
||||
using TextFilterCallback = std::function<void(std::string &, const std::string &)>;
|
||||
|
||||
private:
|
||||
std::string currentText;
|
||||
std::string composedText;
|
||||
ETextAlignment originalAlignment;
|
||||
|
||||
std::shared_ptr<CPicture> background;
|
||||
std::shared_ptr<CLabel> label;
|
||||
|
||||
TextEditedCallback onTextEdited;
|
||||
TextFilterCallback onTextFiltering;
|
||||
|
||||
//Filter that will block all characters not allowed in filenames
|
||||
static void filenameFilter(std::string & text, const std::string & oldText);
|
||||
//Filter that will allow only input of numbers in range min-max (min-max are allowed)
|
||||
//min-max should be set via something like std::bind
|
||||
static void numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue);
|
||||
|
||||
std::string getVisibleText();
|
||||
void createLabel(bool giveFocusToInput);
|
||||
void updateLabel();
|
||||
|
||||
void clickPressed(const Point & cursorPosition) final;
|
||||
void textInputed(const std::string & enteredText) final;
|
||||
void textEdited(const std::string & enteredText) final;
|
||||
void onFocusGot() final;
|
||||
void onFocusLost() final;
|
||||
|
||||
CTextInput(const Rect & Pos);
|
||||
public:
|
||||
CTextInput(const Rect & Pos, EFonts font, ETextAlignment alignment, bool giveFocusToInput);
|
||||
CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName);
|
||||
CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf);
|
||||
|
||||
/// Returns currently entered text. May not match visible text
|
||||
const std::string & getText() const;
|
||||
|
||||
void setText(const std::string & nText);
|
||||
|
||||
/// Set callback that will be called whenever player enters new text
|
||||
void setCallback(const TextEditedCallback & cb);
|
||||
|
||||
/// Enables filtering entered text that ensures that text is valid filename (existing or not)
|
||||
void setFilterFilename();
|
||||
/// Enable filtering entered text that ensures that text is valid number in provided range [min, max]
|
||||
void setFilterNumber(int minValue, int maxValue);
|
||||
|
||||
void setFont(EFonts Font);
|
||||
void setColor(const ColorRGBA & Color);
|
||||
void setAlignment(ETextAlignment alignment);
|
||||
|
||||
// CIntObject interface impl
|
||||
void keyPressed(EShortcut key) final;
|
||||
void activate() final;
|
||||
void deactivate() final;
|
||||
};
|
@ -15,7 +15,6 @@
|
||||
|
||||
#include "../CPlayerInterface.h"
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/Shortcut.h"
|
||||
#include "../windows/CMessage.h"
|
||||
#include "../windows/InfoWindows.h"
|
||||
#include "../adventureMap/CInGameConsole.h"
|
||||
@ -30,9 +29,6 @@
|
||||
#include "lib/CAndroidVMHelper.h"
|
||||
#endif
|
||||
|
||||
std::list<CFocusable*> CFocusable::focusables;
|
||||
CFocusable * CFocusable::inputWithFocus;
|
||||
|
||||
std::string CLabel::visibleText()
|
||||
{
|
||||
return text;
|
||||
@ -409,6 +405,7 @@ void CTextBox::setText(const std::string & text)
|
||||
{
|
||||
// slider is no longer needed
|
||||
slider.reset();
|
||||
label->scrollTextTo(0);
|
||||
}
|
||||
else if(slider)
|
||||
{
|
||||
@ -571,283 +568,3 @@ Point CGStatusBar::getBorderSize()
|
||||
assert(0);
|
||||
return Point();
|
||||
}
|
||||
|
||||
CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList<void(const std::string &)> & 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);
|
||||
|
||||
#if !defined(VCMI_MOBILE)
|
||||
if(giveFocusToInput)
|
||||
giveFocus();
|
||||
#endif
|
||||
}
|
||||
|
||||
CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList<void(const std::string &)> & CB)
|
||||
:cb(CB)
|
||||
{
|
||||
pos += Pos.topLeft();
|
||||
pos.h = Pos.h;
|
||||
pos.w = Pos.w;
|
||||
maxWidth = Pos.w;
|
||||
|
||||
OBJ_CONSTRUCTION;
|
||||
background = std::make_shared<CPicture>(bgName, bgOffset.x, bgOffset.y);
|
||||
addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT);
|
||||
|
||||
#if !defined(VCMI_MOBILE)
|
||||
giveFocus();
|
||||
#endif
|
||||
}
|
||||
|
||||
CTextInput::CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf)
|
||||
{
|
||||
pos += Pos.topLeft();
|
||||
OBJ_CONSTRUCTION;
|
||||
background = std::make_shared<CPicture>(srf, Pos);
|
||||
pos.w = background->pos.w;
|
||||
pos.h = background->pos.h;
|
||||
maxWidth = Pos.w;
|
||||
background->pos = pos;
|
||||
addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT);
|
||||
|
||||
#if !defined(VCMI_MOBILE)
|
||||
giveFocus();
|
||||
#endif
|
||||
}
|
||||
|
||||
std::atomic<int> CFocusable::usageIndex(0);
|
||||
|
||||
void CFocusable::focusGot()
|
||||
{
|
||||
GH.startTextInput(pos);
|
||||
usageIndex++;
|
||||
}
|
||||
|
||||
void CFocusable::focusLost()
|
||||
{
|
||||
if(0 == --usageIndex)
|
||||
{
|
||||
GH.stopTextInput();
|
||||
}
|
||||
}
|
||||
|
||||
std::string CTextInput::visibleText()
|
||||
{
|
||||
return focus ? text + newText + "_" : text;
|
||||
}
|
||||
|
||||
void CTextInput::clickPressed(const Point & cursorPosition)
|
||||
{
|
||||
if(!focus)
|
||||
giveFocus();
|
||||
}
|
||||
|
||||
void CTextInput::keyPressed(EShortcut key)
|
||||
{
|
||||
if(!focus)
|
||||
return;
|
||||
|
||||
if(key == EShortcut::GLOBAL_MOVE_FOCUS)
|
||||
{
|
||||
moveFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
bool redrawNeeded = false;
|
||||
|
||||
switch(key)
|
||||
{
|
||||
case EShortcut::GLOBAL_BACKSPACE:
|
||||
if(!newText.empty())
|
||||
{
|
||||
TextOperations::trimRightUnicode(newText);
|
||||
redrawNeeded = true;
|
||||
}
|
||||
else if(!text.empty())
|
||||
{
|
||||
TextOperations::trimRightUnicode(text);
|
||||
redrawNeeded = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if(redrawNeeded)
|
||||
{
|
||||
redraw();
|
||||
cb(text);
|
||||
}
|
||||
}
|
||||
|
||||
void CTextInput::showPopupWindow(const Point & cursorPosition)
|
||||
{
|
||||
if(!helpBox.empty()) //there is no point to show window with nothing inside...
|
||||
CRClickPopup::createAndPush(helpBox);
|
||||
}
|
||||
|
||||
|
||||
void CTextInput::setText(const std::string & nText)
|
||||
{
|
||||
setText(nText, false);
|
||||
}
|
||||
|
||||
void CTextInput::setText(const std::string & nText, bool callCb)
|
||||
{
|
||||
CLabel::setText(nText);
|
||||
if(callCb)
|
||||
cb(text);
|
||||
}
|
||||
|
||||
void CTextInput::setHelpText(const std::string & text)
|
||||
{
|
||||
helpBox = text;
|
||||
}
|
||||
|
||||
void CTextInput::textInputed(const std::string & enteredText)
|
||||
{
|
||||
if(!focus)
|
||||
return;
|
||||
std::string oldText = text;
|
||||
|
||||
setText(getText() + enteredText);
|
||||
|
||||
filters(text, oldText);
|
||||
if(text != oldText)
|
||||
{
|
||||
redraw();
|
||||
cb(text);
|
||||
}
|
||||
newText.clear();
|
||||
}
|
||||
|
||||
void CTextInput::textEdited(const std::string & enteredText)
|
||||
{
|
||||
if(!focus)
|
||||
return;
|
||||
|
||||
newText = enteredText;
|
||||
redraw();
|
||||
cb(text + newText);
|
||||
}
|
||||
|
||||
void CTextInput::filenameFilter(std::string & text, const std::string &)
|
||||
{
|
||||
static const std::string forbiddenChars = "<>:\"/\\|?*\r\n"; //if we are entering a filename, some special characters won't be allowed
|
||||
size_t pos;
|
||||
while((pos = text.find_first_of(forbiddenChars)) != std::string::npos)
|
||||
text.erase(pos, 1);
|
||||
}
|
||||
|
||||
void CTextInput::numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue)
|
||||
{
|
||||
assert(minValue < maxValue);
|
||||
|
||||
if(text.empty())
|
||||
text = "0";
|
||||
|
||||
size_t pos = 0;
|
||||
if(text[0] == '-') //allow '-' sign as first symbol only
|
||||
pos++;
|
||||
|
||||
while(pos < text.size())
|
||||
{
|
||||
if(text[pos] < '0' || text[pos] > '9')
|
||||
{
|
||||
text = oldText;
|
||||
return; //new text is not number.
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
try
|
||||
{
|
||||
int value = boost::lexical_cast<int>(text);
|
||||
if(value < minValue)
|
||||
text = std::to_string(minValue);
|
||||
else if(value > maxValue)
|
||||
text = std::to_string(maxValue);
|
||||
}
|
||||
catch(boost::bad_lexical_cast &)
|
||||
{
|
||||
//Should never happen. Unless I missed some cases
|
||||
logGlobal->warn("Warning: failed to convert %s to number!", text);
|
||||
text = oldText;
|
||||
}
|
||||
}
|
||||
|
||||
CFocusable::CFocusable()
|
||||
{
|
||||
focus = false;
|
||||
focusables.push_back(this);
|
||||
}
|
||||
|
||||
CFocusable::~CFocusable()
|
||||
{
|
||||
if(hasFocus())
|
||||
{
|
||||
inputWithFocus = nullptr;
|
||||
focusLost();
|
||||
}
|
||||
|
||||
focusables -= this;
|
||||
}
|
||||
|
||||
bool CFocusable::hasFocus() const
|
||||
{
|
||||
return inputWithFocus == this;
|
||||
}
|
||||
|
||||
void CFocusable::giveFocus()
|
||||
{
|
||||
focus = true;
|
||||
focusGot();
|
||||
redraw();
|
||||
|
||||
if(inputWithFocus)
|
||||
{
|
||||
inputWithFocus->focus = false;
|
||||
inputWithFocus->focusLost();
|
||||
inputWithFocus->redraw();
|
||||
}
|
||||
|
||||
inputWithFocus = this;
|
||||
}
|
||||
|
||||
void CFocusable::moveFocus()
|
||||
{
|
||||
auto i = vstd::find(focusables, this),
|
||||
ourIt = i;
|
||||
for(i++; i != ourIt; i++)
|
||||
{
|
||||
if(i == focusables.end())
|
||||
i = focusables.begin();
|
||||
|
||||
if (*i == this)
|
||||
return;
|
||||
|
||||
if((*i)->isActive())
|
||||
{
|
||||
(*i)->giveFocus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CFocusable::removeFocus()
|
||||
{
|
||||
if(this == inputWithFocus)
|
||||
{
|
||||
focus = false;
|
||||
focusLost();
|
||||
redraw();
|
||||
|
||||
inputWithFocus = nullptr;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@
|
||||
#include "../gui/TextAlignment.h"
|
||||
#include "../render/Colors.h"
|
||||
#include "../render/EFont.h"
|
||||
#include "../../lib/FunctionList.h"
|
||||
#include "../../lib/filesystem/ResourcePath.h"
|
||||
|
||||
class IImage;
|
||||
@ -168,64 +167,3 @@ public:
|
||||
void setEnteringMode(bool on) override;
|
||||
void setEnteredText(const std::string & text) override;
|
||||
};
|
||||
|
||||
/// UIElement which can get input focus
|
||||
class CFocusable : public virtual CIntObject
|
||||
{
|
||||
static std::atomic<int> usageIndex;
|
||||
public:
|
||||
bool focus; //only one focusable control can have focus at one moment
|
||||
|
||||
void giveFocus(); //captures focus
|
||||
void moveFocus(); //moves focus to next active control (may be used for tab switching)
|
||||
void removeFocus(); //remove focus
|
||||
bool hasFocus() const;
|
||||
|
||||
void focusGot();
|
||||
void focusLost();
|
||||
|
||||
static std::list<CFocusable *> focusables; //all existing objs
|
||||
static CFocusable * inputWithFocus; //who has focus now
|
||||
|
||||
CFocusable();
|
||||
~CFocusable();
|
||||
};
|
||||
|
||||
/// 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<void(const std::string &)> cb;
|
||||
CFunctionList<void(std::string &, const std::string &)> 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<void(const std::string &)> & CB, ETextAlignment alignment, bool giveFocusToInput);
|
||||
CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList<void(const std::string &)> & CB);
|
||||
CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf);
|
||||
|
||||
void clickPressed(const Point & cursorPosition) override;
|
||||
void keyPressed(EShortcut key) override;
|
||||
void showPopupWindow(const Point & cursorPosition) override;
|
||||
|
||||
//bool captureThisKey(EShortcut key) override;
|
||||
|
||||
void textInputed(const std::string & enteredText) override;
|
||||
void textEdited(const std::string & enteredText) override;
|
||||
|
||||
//Filter that will block all characters not allowed in filenames
|
||||
static void filenameFilter(std::string & text, const std::string & oldText);
|
||||
//Filter that will allow only input of numbers in range min-max (min-max are allowed)
|
||||
//min-max should be set via something like std::bind
|
||||
static void numberFilter(std::string & text, const std::string & oldText, int minValue, int maxValue);
|
||||
|
||||
friend class CKeyboardFocusListener;
|
||||
};
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include "../gui/WindowHandler.h"
|
||||
#include "../widgets/GraphicalPrimitiveCanvas.h"
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
#include "../adventureMap/AdventureMapInterface.h"
|
||||
#include "../render/CAnimation.h"
|
||||
@ -137,7 +138,8 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
|
||||
searchBoxRectangle = std::make_shared<TransparentFilledRectangle>(r.resize(1), rectangleColor, borderColor);
|
||||
searchBoxDescription = std::make_shared<CLabel>(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, CGI->generaltexth->translate("vcmi.spellBook.search"));
|
||||
|
||||
searchBox = std::make_shared<CTextInput>(r, FONT_SMALL, std::bind(&CSpellWindow::searchInput, this), ETextAlignment::CENTER, true);
|
||||
searchBox = std::make_shared<CTextInput>(r, FONT_SMALL, ETextAlignment::CENTER, true);
|
||||
searchBox->setCallback(std::bind(&CSpellWindow::searchInput, this));
|
||||
}
|
||||
|
||||
processSpells();
|
||||
|
@ -151,7 +151,7 @@ void CWindowWithArtifacts::clickPressedArtPlaceHero(const CArtifactsOfHeroBase &
|
||||
{
|
||||
assert(artSetPtr->getHero()->getSlotByInstance(art) != ArtifactPosition::PRE_FIRST);
|
||||
|
||||
if(GH.isKeyboardCtrlDown())
|
||||
if(GH.isKeyboardCmdDown())
|
||||
{
|
||||
std::shared_ptr<CArtifactsOfHeroMain> anotherHeroEquipmentPointer = nullptr;
|
||||
|
||||
|
@ -31,6 +31,7 @@
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/CGarrisonInt.h"
|
||||
#include "../widgets/CreatureCostBox.h"
|
||||
#include "../widgets/CTextInput.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/Slider.h"
|
||||
#include "../widgets/TextControls.h"
|
||||
@ -328,15 +329,18 @@ CSplitWindow::CSplitWindow(const CCreature * creature, std::function<void(int, i
|
||||
|
||||
int sliderPosition = total - leftMin - rightMin;
|
||||
|
||||
leftInput = std::make_shared<CTextInput>(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true), ETextAlignment::CENTER, true);
|
||||
rightInput = std::make_shared<CTextInput>(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false), ETextAlignment::CENTER, true);
|
||||
leftInput = std::make_shared<CTextInput>(Rect(20, 218, 100, 36), FONT_BIG, ETextAlignment::CENTER, true);
|
||||
rightInput = std::make_shared<CTextInput>(Rect(176, 218, 100, 36), FONT_BIG, ETextAlignment::CENTER, true);
|
||||
|
||||
leftInput->setCallback(std::bind(&CSplitWindow::setAmountText, this, _1, true));
|
||||
rightInput->setCallback(std::bind(&CSplitWindow::setAmountText, this, _1, false));
|
||||
|
||||
//add filters to allow only number input
|
||||
leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax);
|
||||
rightInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, rightMin, rightMax);
|
||||
leftInput->setFilterNumber(leftMin, leftMax);
|
||||
rightInput->setFilterNumber(rightMin, rightMax);
|
||||
|
||||
leftInput->setText(std::to_string(leftAmount), false);
|
||||
rightInput->setText(std::to_string(rightAmount), false);
|
||||
leftInput->setText(std::to_string(leftAmount));
|
||||
rightInput->setText(std::to_string(rightAmount));
|
||||
|
||||
animLeft = std::make_shared<CCreaturePic>(20, 54, creature, true, false);
|
||||
animRight = std::make_shared<CCreaturePic>(177, 54,creature, true, false);
|
||||
@ -616,6 +620,9 @@ void CTavernWindow::HeroPortrait::clickDouble(const Point & cursorPosition)
|
||||
|
||||
void CTavernWindow::HeroPortrait::showPopupWindow(const Point & cursorPosition)
|
||||
{
|
||||
// h3 behavior - right-click also selects hero
|
||||
clickPressed(cursorPosition);
|
||||
|
||||
if(h)
|
||||
GH.windows().createAndPushWindow<CRClickPopupInt>(std::make_shared<CHeroWindow>(h));
|
||||
}
|
||||
@ -848,7 +855,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
|
||||
bool moveEquipped = true;
|
||||
bool moveBackpack = true;
|
||||
|
||||
if(GH.isKeyboardCtrlDown())
|
||||
if(GH.isKeyboardCmdDown())
|
||||
moveBackpack = false;
|
||||
else if(GH.isKeyboardShiftDown())
|
||||
moveEquipped = false;
|
||||
@ -1712,6 +1719,12 @@ void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition)
|
||||
|
||||
void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition)
|
||||
{
|
||||
if (parent->selected != index)
|
||||
{
|
||||
clickPressed(cursorPosition);
|
||||
return;
|
||||
}
|
||||
|
||||
parent->elementSelected();
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"invited" :
|
||||
{
|
||||
"type" : "array",
|
||||
"description" : "List of accounts that were invited to this room",
|
||||
"items" :
|
||||
{
|
||||
"type" : "object",
|
||||
"additionalProperties" : false,
|
||||
"required" : [ "accountID", "displayName" ],
|
||||
"properties" : {
|
||||
"accountID" :
|
||||
{
|
||||
"type" : "string",
|
||||
"description" : "Unique ID of an account"
|
||||
},
|
||||
"displayName" :
|
||||
{
|
||||
"type" : "string",
|
||||
"description" : "Display name of an account"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods" :
|
||||
{
|
||||
"type" : "array",
|
||||
|
@ -78,6 +78,11 @@
|
||||
"maximum" : 8,
|
||||
"description" : "Maximum number of players that can join this room, including host"
|
||||
},
|
||||
"version" :
|
||||
{
|
||||
"type" : "string",
|
||||
"description" : "Version of match server, e.g. 1.5.0"
|
||||
},
|
||||
"ageSeconds" :
|
||||
{
|
||||
"type" : "number",
|
||||
|
@ -374,7 +374,7 @@
|
||||
},
|
||||
"backgroundDimSmallWindows" : {
|
||||
"type" : "boolean",
|
||||
"default" : true
|
||||
"default" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -130,7 +130,6 @@
|
||||
{
|
||||
"name" : "messageInput",
|
||||
"type": "textInput",
|
||||
"alignment" : "left",
|
||||
"rect": {"x": 440, "y": 568, "w": 377, "h": 20}
|
||||
},
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
[](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
|
||||
[](https://github.com/vcmi/vcmi/releases/tag/1.5.1)
|
||||
[](https://github.com/vcmi/vcmi/releases)
|
||||
|
||||
# VCMI Project
|
||||
|
@ -123,6 +123,7 @@
|
||||
<control>pointing</control>
|
||||
<control>keyboard</control>
|
||||
<control>touch</control>
|
||||
<control>gamepad</control>
|
||||
</recommends>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="violence-cartoon">moderate</content_attribute>
|
||||
|
@ -254,7 +254,7 @@
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.ui" line="373"/>
|
||||
<source>Install from file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Instalar a partir de arquivo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.ui" line="424"/>
|
||||
@ -350,18 +350,18 @@
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="310"/>
|
||||
<source>please upgrade mod</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>por favor, atualize o mod</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="182"/>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="796"/>
|
||||
<source>mods repository index</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>índice do repositório de mods</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="312"/>
|
||||
<source>or newer</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>ou mais recente</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="315"/>
|
||||
@ -416,42 +416,42 @@
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="627"/>
|
||||
<source>All supported files</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Todos os arquivos suportados</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="627"/>
|
||||
<source>Maps</source>
|
||||
<translation type="unfinished">Mapas</translation>
|
||||
<translation>Mapas</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="627"/>
|
||||
<source>Campaigns</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Campanhas</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="627"/>
|
||||
<source>Configs</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Configurações</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="627"/>
|
||||
<source>Mods</source>
|
||||
<translation type="unfinished">Mods</translation>
|
||||
<translation>Mods</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="628"/>
|
||||
<source>Select files (configs, mods, maps, campaigns) to install...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Selecione arquivos (configurações, mods, mapas, campanhas) para instalar...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="654"/>
|
||||
<source>Replace config file?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Substituir arquivo de configuração?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="654"/>
|
||||
<source>Do you want to replace %1?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Você deseja substituir %1?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="693"/>
|
||||
@ -505,7 +505,7 @@ Instalar o download realizado com sucesso?</translation>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="960"/>
|
||||
<source>screenshots</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>capturas de tela</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistview_moc.cpp" line="966"/>
|
||||
@ -841,12 +841,12 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu
|
||||
<message>
|
||||
<location filename="../settingsView/csettingsview_moc.cpp" line="437"/>
|
||||
<source>Enable</source>
|
||||
<translation>Habilitar</translation>
|
||||
<translation>Ativar</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingsView/csettingsview_moc.cpp" line="442"/>
|
||||
<source>Not Installed</source>
|
||||
<translation>Não instalado</translation>
|
||||
<translation>Não Instalado</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../settingsView/csettingsview_moc.cpp" line="443"/>
|
||||
@ -859,27 +859,27 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu
|
||||
<message>
|
||||
<location filename="../modManager/cmodlist.cpp" line="21"/>
|
||||
<source>%1 B</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>%1 B</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlist.cpp" line="22"/>
|
||||
<source>%1 KiB</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>%1 KiB</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlist.cpp" line="23"/>
|
||||
<source>%1 MiB</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>%1 MiB</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlist.cpp" line="24"/>
|
||||
<source>%1 GiB</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>%1 GiB</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlist.cpp" line="25"/>
|
||||
<source>%1 TiB</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>%1 TiB</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@ -935,7 +935,7 @@ Heroes® of Might and Magic® III HD atualmente não é suportado!</translation>
|
||||
<location filename="../firstLaunch/firstlaunch_moc.ui" line="288"/>
|
||||
<location filename="../firstLaunch/firstlaunch_moc.cpp" line="350"/>
|
||||
<source>Install gog.com files</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Instalar arquivos do gog.com</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../firstLaunch/firstlaunch_moc.ui" line="362"/>
|
||||
@ -1021,7 +1021,7 @@ Heroes® of Might and Magic® III HD atualmente não é suportado!</translation>
|
||||
<message>
|
||||
<location filename="../firstLaunch/firstlaunch_moc.ui" line="346"/>
|
||||
<source>If you don't have a copy of Heroes III installed, VCMI can import your Heroes III data using the offline installer from gog.com.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Se você não tiver uma cópia do Heroes III instalada, o VCMI pode importar seus dados do Heroes III usando o instalador offline do gog.com.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../firstLaunch/firstlaunch_moc.ui" line="384"/>
|
||||
@ -1316,17 +1316,17 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistmodel_moc.cpp" line="172"/>
|
||||
<source>Name</source>
|
||||
<translation type="unfinished">Nome</translation>
|
||||
<translation>Nome</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistmodel_moc.cpp" line="175"/>
|
||||
<source>Type</source>
|
||||
<translation type="unfinished">Tipo</translation>
|
||||
<translation>Tipo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../modManager/cmodlistmodel_moc.cpp" line="176"/>
|
||||
<source>Version</source>
|
||||
<translation type="unfinished">Versão</translation>
|
||||
<translation>Versão</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@ -1349,12 +1349,12 @@ Por favor, selecione o diretório com Heroes III: Complete Edition ou Heroes III
|
||||
<message>
|
||||
<location filename="../updatedialog_moc.cpp" line="64"/>
|
||||
<source>Network error</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Erro de rede</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../updatedialog_moc.cpp" line="101"/>
|
||||
<source>Cannot read JSON from url or incorrect JSON data</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Não é possível ler JSON a partir do URL ou os dados JSON estão incorretos</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
@ -107,10 +107,14 @@ void setThreadName(const std::string &name)
|
||||
|
||||
#elif defined(VCMI_APPLE)
|
||||
pthread_setname_np(name.c_str());
|
||||
#elif defined(VCMI_FREEBSD)
|
||||
pthread_setname_np(pthread_self(), 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);
|
||||
#else
|
||||
#error "Failed to find method to set thread name on this system. Please provide one (or disable this line if you just want code to compile)"
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -760,7 +760,7 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInf
|
||||
|
||||
DamageEstimation ret = calculateDmgRange(bai);
|
||||
|
||||
if(retaliationDmg)
|
||||
if(retaliationDmg && bai.defender->ableToRetaliate())
|
||||
{
|
||||
if(bai.shooting)
|
||||
{
|
||||
@ -782,7 +782,7 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInf
|
||||
};
|
||||
|
||||
DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min);
|
||||
DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min);
|
||||
DamageEstimation retaliationMax = estimateRetaliation(ret.damage.max);
|
||||
|
||||
retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min);
|
||||
retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max);
|
||||
|
@ -66,7 +66,9 @@ void CBufferedStream::ensureSize(si64 size)
|
||||
{
|
||||
si64 initialSize = buffer.size();
|
||||
si64 currentStep = std::min<si64>(size, buffer.size());
|
||||
vstd::amax(currentStep, 1024); // to avoid large number of calls at start
|
||||
// to avoid large number of calls at start
|
||||
// this is often used to load h3m map headers, most of which are ~300 bytes in size
|
||||
vstd::amax(currentStep, 512);
|
||||
|
||||
buffer.resize(initialSize + currentStep);
|
||||
|
||||
|
@ -294,6 +294,7 @@ void CGameState::updateOnLoad(StartInfo * si)
|
||||
scenarioOps->playerInfos = si->playerInfos;
|
||||
for(auto & i : si->playerInfos)
|
||||
gs->players[i.first].human = i.second.isControlledByHuman();
|
||||
scenarioOps->extraOptionsInfo = si->extraOptionsInfo;
|
||||
}
|
||||
|
||||
void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking)
|
||||
|
@ -392,9 +392,15 @@ void CGameStateCampaign::transferMissingArtifacts(const CampaignTravel & travelO
|
||||
|
||||
auto * donorHero = campaignHeroReplacement.hero;
|
||||
|
||||
if (!donorHero)
|
||||
throw std::runtime_error("Failed to find hero to take artifacts from! Scenario: " + gameState->map->name.toString());
|
||||
|
||||
for (auto const & artLocation : campaignHeroReplacement.transferrableArtifacts)
|
||||
{
|
||||
auto * artifact = donorHero->getArt(artLocation);
|
||||
if (!donorHero)
|
||||
throw std::runtime_error("Failed to find artifacts to transfer to travelling hero! Scenario: " + gameState->map->name.toString());
|
||||
|
||||
artifact->removeFrom(*donorHero, artLocation);
|
||||
|
||||
if (receiver)
|
||||
|
@ -544,12 +544,12 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const
|
||||
|
||||
//get Mana Vortex or Stables bonuses
|
||||
//same code is in the CGameHandler::buildStructure method
|
||||
if (garrisonHero != nullptr) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
|
||||
cb->visitCastleObjects(this, garrisonHero);
|
||||
|
||||
if (visitingHero != nullptr)
|
||||
cb->visitCastleObjects(this, visitingHero);
|
||||
|
||||
if (garrisonHero != nullptr)
|
||||
cb->visitCastleObjects(this, garrisonHero);
|
||||
|
||||
if (tempOwner == PlayerColor::NEUTRAL) //garrison growth for neutral towns
|
||||
{
|
||||
std::vector<SlotID> nativeCrits; //slots
|
||||
|
@ -124,6 +124,49 @@ void CMapLoaderH3M::init()
|
||||
//map->banWaterContent(); //Not sure if force this for custom scenarios
|
||||
}
|
||||
|
||||
static MapIdentifiersH3M generateMapping(EMapFormat format)
|
||||
{
|
||||
auto features = MapFormatFeaturesH3M::find(format, 0);
|
||||
MapIdentifiersH3M identifierMapper;
|
||||
|
||||
if(features.levelROE)
|
||||
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA));
|
||||
if(features.levelAB)
|
||||
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE));
|
||||
if(features.levelSOD)
|
||||
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH));
|
||||
if(features.levelWOG)
|
||||
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));
|
||||
|
||||
return identifierMapper;
|
||||
}
|
||||
|
||||
static std::map<EMapFormat, MapIdentifiersH3M> generateMappings()
|
||||
{
|
||||
std::map<EMapFormat, MapIdentifiersH3M> result;
|
||||
auto addMapping = [&result](EMapFormat format)
|
||||
{
|
||||
try
|
||||
{
|
||||
result[format] = generateMapping(format);
|
||||
}
|
||||
catch(const std::runtime_error &)
|
||||
{
|
||||
// unsupported map format - skip
|
||||
}
|
||||
};
|
||||
|
||||
addMapping(EMapFormat::ROE);
|
||||
addMapping(EMapFormat::AB);
|
||||
addMapping(EMapFormat::SOD);
|
||||
addMapping(EMapFormat::HOTA);
|
||||
addMapping(EMapFormat::WOG);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CMapLoaderH3M::readHeader()
|
||||
{
|
||||
// Map version
|
||||
@ -159,18 +202,10 @@ void CMapLoaderH3M::readHeader()
|
||||
features = MapFormatFeaturesH3M::find(mapHeader->version, 0);
|
||||
reader->setFormatLevel(features);
|
||||
}
|
||||
MapIdentifiersH3M identifierMapper;
|
||||
|
||||
if (features.levelROE)
|
||||
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA));
|
||||
if (features.levelAB)
|
||||
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE));
|
||||
if (features.levelSOD)
|
||||
identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH));
|
||||
if (features.levelWOG)
|
||||
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));
|
||||
// optimization - load mappings only once to avoid slow parsing of map headers for map list
|
||||
static const std::map<EMapFormat, MapIdentifiersH3M> identifierMappers = generateMappings();
|
||||
const MapIdentifiersH3M & identifierMapper = identifierMappers.at(mapHeader->version);
|
||||
|
||||
reader->setIdentifierRemapper(identifierMapper);
|
||||
|
||||
|
@ -12,12 +12,12 @@
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
NetworkConnection::NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr<NetworkSocket> & socket)
|
||||
NetworkConnection::NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr<NetworkSocket> & socket, const std::shared_ptr<NetworkContext> & context)
|
||||
: socket(socket)
|
||||
, timer(std::make_shared<NetworkTimer>(*context))
|
||||
, listener(listener)
|
||||
{
|
||||
socket->set_option(boost::asio::ip::tcp::no_delay(true));
|
||||
socket->set_option(boost::asio::socket_base::keep_alive(true));
|
||||
|
||||
// iOS throws exception on attempt to set buffer size
|
||||
constexpr auto bufferSize = 4 * 1024 * 1024;
|
||||
@ -42,6 +42,12 @@ NetworkConnection::NetworkConnection(INetworkConnectionListener & listener, cons
|
||||
}
|
||||
|
||||
void NetworkConnection::start()
|
||||
{
|
||||
heartbeat();
|
||||
startReceiving();
|
||||
}
|
||||
|
||||
void NetworkConnection::startReceiving()
|
||||
{
|
||||
boost::asio::async_read(*socket,
|
||||
readBuffer,
|
||||
@ -49,11 +55,30 @@ void NetworkConnection::start()
|
||||
[self = shared_from_this()](const auto & ec, const auto & endpoint) { self->onHeaderReceived(ec); });
|
||||
}
|
||||
|
||||
void NetworkConnection::heartbeat()
|
||||
{
|
||||
constexpr auto heartbeatInterval = std::chrono::seconds(10);
|
||||
|
||||
timer->expires_after(heartbeatInterval);
|
||||
timer->async_wait( [self = weak_from_this()](const auto & ec)
|
||||
{
|
||||
if (ec)
|
||||
return;
|
||||
|
||||
auto locked = self.lock();
|
||||
if (!locked)
|
||||
return;
|
||||
|
||||
locked->sendPacket({});
|
||||
locked->heartbeat();
|
||||
});
|
||||
}
|
||||
|
||||
void NetworkConnection::onHeaderReceived(const boost::system::error_code & ecHeader)
|
||||
{
|
||||
if (ecHeader)
|
||||
{
|
||||
listener.onDisconnected(shared_from_this(), ecHeader.message());
|
||||
onError(ecHeader.message());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -65,14 +90,14 @@ void NetworkConnection::onHeaderReceived(const boost::system::error_code & ecHea
|
||||
|
||||
if (messageSize > messageMaxSize)
|
||||
{
|
||||
listener.onDisconnected(shared_from_this(), "Invalid packet size!");
|
||||
onError("Invalid packet size!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageSize == 0)
|
||||
{
|
||||
// Zero-sized packet. Strange, but safe to ignore. Start reading next packet
|
||||
start();
|
||||
//heartbeat package with no payload - wait for next packet
|
||||
startReceiving();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -86,7 +111,7 @@ void NetworkConnection::onPacketReceived(const boost::system::error_code & ec, u
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
listener.onDisconnected(shared_from_this(), ec.message());
|
||||
onError(ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -99,29 +124,80 @@ void NetworkConnection::onPacketReceived(const boost::system::error_code & ec, u
|
||||
readBuffer.sgetn(reinterpret_cast<char *>(message.data()), expectedPacketSize);
|
||||
listener.onPacketReceived(shared_from_this(), message);
|
||||
|
||||
start();
|
||||
startReceiving();
|
||||
}
|
||||
|
||||
void NetworkConnection::setAsyncWritesEnabled(bool on)
|
||||
{
|
||||
asyncWritesEnabled = on;
|
||||
}
|
||||
|
||||
void NetworkConnection::sendPacket(const std::vector<std::byte> & message)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(writeMutex);
|
||||
std::vector<std::byte> headerVector(sizeof(uint32_t));
|
||||
uint32_t messageSize = message.size();
|
||||
std::memcpy(headerVector.data(), &messageSize, sizeof(uint32_t));
|
||||
|
||||
boost::system::error_code ec;
|
||||
// At the moment, vcmilobby *requires* async writes in order to handle multiple connections with different speeds and at optimal performance
|
||||
// However server (and potentially - client) can not handle this mode and may shutdown either socket or entire asio service too early, before all writes are performed
|
||||
if (asyncWritesEnabled)
|
||||
{
|
||||
|
||||
// create array with single element - boost::asio::buffer can be constructed from containers, but not from plain integer
|
||||
std::array<uint32_t, 1> messageSize{static_cast<uint32_t>(message.size())};
|
||||
bool messageQueueEmpty = dataToSend.empty();
|
||||
dataToSend.push_back(headerVector);
|
||||
if (message.size() > 0)
|
||||
dataToSend.push_back(message);
|
||||
|
||||
boost::asio::write(*socket, boost::asio::buffer(messageSize), ec );
|
||||
if (message.size() > 0)
|
||||
boost::asio::write(*socket, boost::asio::buffer(message), ec );
|
||||
if (messageQueueEmpty)
|
||||
doSendData();
|
||||
//else - data sending loop is still active and still sending previous messages
|
||||
}
|
||||
else
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
boost::asio::write(*socket, boost::asio::buffer(headerVector), ec );
|
||||
if (message.size() > 0)
|
||||
boost::asio::write(*socket, boost::asio::buffer(message), ec );
|
||||
}
|
||||
}
|
||||
|
||||
//Note: ignoring error code, intended
|
||||
void NetworkConnection::doSendData()
|
||||
{
|
||||
if (dataToSend.empty())
|
||||
throw std::runtime_error("Attempting to sent data but there is no data to send!");
|
||||
|
||||
boost::asio::async_write(*socket, boost::asio::buffer(dataToSend.front()), [self = shared_from_this()](const auto & error, const auto & )
|
||||
{
|
||||
self->onDataSent(error);
|
||||
});
|
||||
}
|
||||
|
||||
void NetworkConnection::onDataSent(const boost::system::error_code & ec)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(writeMutex);
|
||||
dataToSend.pop_front();
|
||||
if (ec)
|
||||
{
|
||||
onError(ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataToSend.empty())
|
||||
doSendData();
|
||||
}
|
||||
|
||||
void NetworkConnection::onError(const std::string & message)
|
||||
{
|
||||
listener.onDisconnected(shared_from_this(), message);
|
||||
close();
|
||||
}
|
||||
|
||||
void NetworkConnection::close()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
socket->close(ec);
|
||||
timer->cancel(ec);
|
||||
|
||||
//NOTE: ignoring error code, intended
|
||||
}
|
||||
|
@ -13,26 +13,37 @@
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
class NetworkConnection : public INetworkConnection, public std::enable_shared_from_this<NetworkConnection>
|
||||
class NetworkConnection final : public INetworkConnection, public std::enable_shared_from_this<NetworkConnection>
|
||||
{
|
||||
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::list<std::vector<std::byte>> dataToSend;
|
||||
std::shared_ptr<NetworkSocket> socket;
|
||||
std::shared_ptr<NetworkTimer> timer;
|
||||
std::mutex writeMutex;
|
||||
|
||||
NetworkBuffer readBuffer;
|
||||
INetworkConnectionListener & listener;
|
||||
bool asyncWritesEnabled = false;
|
||||
|
||||
void heartbeat();
|
||||
void onError(const std::string & message);
|
||||
|
||||
void startReceiving();
|
||||
void onHeaderReceived(const boost::system::error_code & ec);
|
||||
void onPacketReceived(const boost::system::error_code & ec, uint32_t expectedPacketSize);
|
||||
|
||||
void doSendData();
|
||||
void onDataSent(const boost::system::error_code & ec);
|
||||
|
||||
public:
|
||||
NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr<NetworkSocket> & socket);
|
||||
NetworkConnection(INetworkConnectionListener & listener, const std::shared_ptr<NetworkSocket> & socket, const std::shared_ptr<NetworkContext> & context);
|
||||
|
||||
void start();
|
||||
void close() override;
|
||||
void sendPacket(const std::vector<std::byte> & message) override;
|
||||
void setAsyncWritesEnabled(bool on) override;
|
||||
};
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
||||
|
@ -35,7 +35,7 @@ void NetworkHandler::connectToRemote(INetworkClientListener & listener, const st
|
||||
auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(*io);
|
||||
|
||||
resolver->async_resolve(host, std::to_string(port),
|
||||
[&listener, resolver, socket](const boost::system::error_code& error, const boost::asio::ip::tcp::resolver::results_type & endpoints)
|
||||
[this, &listener, resolver, socket](const boost::system::error_code& error, const boost::asio::ip::tcp::resolver::results_type & endpoints)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
@ -43,14 +43,14 @@ void NetworkHandler::connectToRemote(INetworkClientListener & listener, const st
|
||||
return;
|
||||
}
|
||||
|
||||
boost::asio::async_connect(*socket, endpoints, [socket, &listener](const boost::system::error_code& error, const boost::asio::ip::tcp::endpoint& endpoint)
|
||||
boost::asio::async_connect(*socket, endpoints, [this, 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<NetworkConnection>(listener, socket);
|
||||
auto connection = std::make_shared<NetworkConnection>(listener, socket, io);
|
||||
connection->start();
|
||||
|
||||
listener.onConnectionEstablished(connection);
|
||||
|
@ -17,6 +17,7 @@ class DLL_LINKAGE INetworkConnection : boost::noncopyable
|
||||
public:
|
||||
virtual ~INetworkConnection() = default;
|
||||
virtual void sendPacket(const std::vector<std::byte> & message) = 0;
|
||||
virtual void setAsyncWritesEnabled(bool on) = 0;
|
||||
virtual void close() = 0;
|
||||
};
|
||||
|
||||
|
@ -39,7 +39,7 @@ void NetworkServer::connectionAccepted(std::shared_ptr<NetworkSocket> upcomingCo
|
||||
}
|
||||
|
||||
logNetwork->info("We got a new connection! :)");
|
||||
auto connection = std::make_shared<NetworkConnection>(*this, upcomingConnection);
|
||||
auto connection = std::make_shared<NetworkConnection>(*this, upcomingConnection, io);
|
||||
connections.insert(connection);
|
||||
connection->start();
|
||||
listener.onNewConnection(connection);
|
||||
@ -49,9 +49,11 @@ void NetworkServer::connectionAccepted(std::shared_ptr<NetworkSocket> upcomingCo
|
||||
void NetworkServer::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
|
||||
{
|
||||
logNetwork->info("Connection lost! Reason: %s", errorMessage);
|
||||
assert(connections.count(connection));
|
||||
connections.erase(connection);
|
||||
listener.onDisconnected(connection, errorMessage);
|
||||
if (connections.count(connection))
|
||||
{
|
||||
connections.erase(connection);
|
||||
listener.onDisconnected(connection, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkServer::onPacketReceived(const std::shared_ptr<INetworkConnection> & connection, const std::vector<std::byte> & message)
|
||||
|
@ -254,6 +254,11 @@ bool Area::overlap(const Area & area) const
|
||||
return overlap(area.getTilesVector());
|
||||
}
|
||||
|
||||
int Area::distance(const int3 & tile) const
|
||||
{
|
||||
return nearest(tile).dist2d(tile);
|
||||
}
|
||||
|
||||
int Area::distanceSqr(const int3 & tile) const
|
||||
{
|
||||
return nearest(tile).dist2dSQ(tile);
|
||||
|
@ -51,6 +51,7 @@ namespace rmg
|
||||
bool contains(const Area & area) const;
|
||||
bool overlap(const Area & area) const;
|
||||
bool overlap(const std::vector<int3> & tiles) const;
|
||||
int distance(const int3 & tile) const;
|
||||
int distanceSqr(const int3 & tile) const;
|
||||
int distanceSqr(const Area & area) const;
|
||||
int3 nearest(const int3 & tile) const;
|
||||
|
@ -64,16 +64,16 @@ const rmg::Area & RoadPlacer::getRoads() const
|
||||
return roads;
|
||||
}
|
||||
|
||||
bool RoadPlacer::createRoad(const int3 & dst)
|
||||
bool RoadPlacer::createRoad(const int3 & destination)
|
||||
{
|
||||
auto searchArea = zone.areaPossible() + zone.freePaths() + areaRoads + roads;
|
||||
|
||||
rmg::Area border(zone.area()->getBorder());
|
||||
|
||||
rmg::Path path(searchArea);
|
||||
path.connect(roads);
|
||||
|
||||
const float VISITABLE_PENALTY = 1.33f;
|
||||
|
||||
auto simpleRoutig = [this, VISITABLE_PENALTY](const int3& src, const int3& dst)
|
||||
auto simpleRoutig = [this, &border](const int3& src, const int3& dst)
|
||||
{
|
||||
if(areaIsolated().contains(dst))
|
||||
{
|
||||
@ -81,52 +81,28 @@ bool RoadPlacer::createRoad(const int3 & dst)
|
||||
}
|
||||
else
|
||||
{
|
||||
float weight = dst.dist2dSQ(src);
|
||||
auto ret = weight * weight;
|
||||
float ret = dst.dist2d(src);
|
||||
|
||||
if (visitableTiles.contains(src) || visitableTiles.contains(dst))
|
||||
{
|
||||
ret *= VISITABLE_PENALTY;
|
||||
}
|
||||
float dist = border.distance(dst);
|
||||
if(dist > 1)
|
||||
{
|
||||
ret /= dist;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
auto res = path.search(dst, true, simpleRoutig);
|
||||
auto res = path.search(destination, true, simpleRoutig);
|
||||
if(!res.valid())
|
||||
{
|
||||
auto desperateRoutig = [this, VISITABLE_PENALTY](const int3& src, const int3& dst) -> float
|
||||
res = createRoadDesperate(path, destination);
|
||||
if (!res.valid())
|
||||
{
|
||||
//Do not allow connections straight up through object not visitable from top
|
||||
if(std::abs((src - dst).y) == 1)
|
||||
{
|
||||
if(areaIsolated().contains(dst) || areaIsolated().contains(src))
|
||||
{
|
||||
return 1e12;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(areaIsolated().contains(dst))
|
||||
{
|
||||
return 1e6;
|
||||
}
|
||||
}
|
||||
|
||||
float weight = dst.dist2dSQ(src);
|
||||
|
||||
auto ret = weight * weight;
|
||||
if (visitableTiles.contains(src) || visitableTiles.contains(dst))
|
||||
{
|
||||
ret *= VISITABLE_PENALTY;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
res = path.search(dst, false, desperateRoutig);
|
||||
|
||||
if(!res.valid())
|
||||
{
|
||||
logGlobal->warn("Failed to create road to node %s", dst.toString());
|
||||
logGlobal->warn("Failed to create road to node %s", destination.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -135,6 +111,38 @@ bool RoadPlacer::createRoad(const int3 & dst)
|
||||
|
||||
}
|
||||
|
||||
rmg::Path RoadPlacer::createRoadDesperate(rmg::Path & path, const int3 & destination)
|
||||
{
|
||||
auto desperateRoutig = [this](const int3& src, const int3& dst) -> float
|
||||
{
|
||||
//Do not allow connections straight up through object not visitable from top
|
||||
if(std::abs((src - dst).y) == 1)
|
||||
{
|
||||
if(areaIsolated().contains(dst) || areaIsolated().contains(src))
|
||||
{
|
||||
return 1e12;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(areaIsolated().contains(dst))
|
||||
{
|
||||
return 1e6;
|
||||
}
|
||||
}
|
||||
|
||||
float weight = dst.dist2dSQ(src);
|
||||
auto ret = weight * weight; // Still prefer straight paths
|
||||
|
||||
if (visitableTiles.contains(src) || visitableTiles.contains(dst))
|
||||
{
|
||||
ret *= VISITABLE_PENALTY;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
return path.search(destination, false, desperateRoutig);
|
||||
}
|
||||
|
||||
void RoadPlacer::drawRoads(bool secondary)
|
||||
{
|
||||
//Do not draw roads on underground rock or water
|
||||
|
@ -13,6 +13,8 @@
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
const float VISITABLE_PENALTY = 1.33f;
|
||||
|
||||
class RoadPlacer: public Modificator
|
||||
{
|
||||
public:
|
||||
@ -34,6 +36,7 @@ public:
|
||||
|
||||
protected:
|
||||
bool createRoad(const int3 & dst);
|
||||
rmg::Path createRoadDesperate(rmg::Path & path, const int3 & destination);
|
||||
void drawRoads(bool secondary = false); //actually updates tiles
|
||||
|
||||
protected:
|
||||
|
@ -272,6 +272,13 @@ void LobbyDatabase::prepareStatements()
|
||||
ORDER BY secondsElapsed ASC
|
||||
)");
|
||||
|
||||
getGameRoomInvitesStatement = database->prepare(R"(
|
||||
SELECT a.accountID, a.displayName
|
||||
FROM gameRoomInvites gri
|
||||
LEFT JOIN accounts a ON a.accountID = gri.accountID
|
||||
WHERE roomID = ?
|
||||
)");
|
||||
|
||||
getGameRoomPlayersStatement = database->prepare(R"(
|
||||
SELECT a.accountID, a.displayName
|
||||
FROM gameRoomPlayers grp
|
||||
@ -581,6 +588,19 @@ std::vector<LobbyGameRoom> LobbyDatabase::getActiveGameRooms()
|
||||
}
|
||||
getGameRoomPlayersStatement->reset();
|
||||
}
|
||||
|
||||
for (auto & room : result)
|
||||
{
|
||||
getGameRoomInvitesStatement->setBinds(room.roomID);
|
||||
while(getGameRoomInvitesStatement->execute())
|
||||
{
|
||||
LobbyAccount account;
|
||||
getGameRoomInvitesStatement->getColumns(account.accountID, account.displayName);
|
||||
room.invited.push_back(account);
|
||||
}
|
||||
getGameRoomInvitesStatement->reset();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ class LobbyDatabase
|
||||
SQLiteStatementPtr getAccountGameRoomStatement;
|
||||
SQLiteStatementPtr getAccountDisplayNameStatement;
|
||||
SQLiteStatementPtr getGameRoomPlayersStatement;
|
||||
SQLiteStatementPtr getGameRoomInvitesStatement;
|
||||
SQLiteStatementPtr countRoomUsedSlotsStatement;
|
||||
SQLiteStatementPtr countRoomTotalSlotsStatement;
|
||||
|
||||
|
@ -47,6 +47,7 @@ struct LobbyGameRoom
|
||||
std::string version;
|
||||
std::string modsJson;
|
||||
std::vector<LobbyAccount> participants;
|
||||
std::vector<LobbyAccount> invited;
|
||||
LobbyRoomState roomState;
|
||||
uint32_t playerLimit;
|
||||
std::chrono::seconds age;
|
||||
|
@ -212,11 +212,15 @@ static JsonNode loadLobbyGameRoomToJson(const LobbyGameRoom & gameRoom)
|
||||
jsonEntry["status"].String() = LOBBY_ROOM_STATE_NAMES[vstd::to_underlying(gameRoom.roomState)];
|
||||
jsonEntry["playerLimit"].Integer() = gameRoom.playerLimit;
|
||||
jsonEntry["ageSeconds"].Integer() = gameRoom.age.count();
|
||||
jsonEntry["mods"] = JsonNode(reinterpret_cast<const std::byte *>(gameRoom.modsJson.data()), gameRoom.modsJson.size());
|
||||
if (!gameRoom.modsJson.empty()) // not present in match history
|
||||
jsonEntry["mods"] = JsonNode(reinterpret_cast<const std::byte *>(gameRoom.modsJson.data()), gameRoom.modsJson.size());
|
||||
|
||||
for(const auto & account : gameRoom.participants)
|
||||
jsonEntry["participants"].Vector().push_back(loadLobbyAccountToJson(account));
|
||||
|
||||
for(const auto & account : gameRoom.invited)
|
||||
jsonEntry["invited"].Vector().push_back(loadLobbyAccountToJson(account));
|
||||
|
||||
return jsonEntry;
|
||||
}
|
||||
|
||||
@ -288,6 +292,7 @@ void LobbyServer::sendChatMessage(const NetworkConnectionPtr & target, const std
|
||||
|
||||
void LobbyServer::onNewConnection(const NetworkConnectionPtr & connection)
|
||||
{
|
||||
connection->setAsyncWritesEnabled(true);
|
||||
// no-op - waiting for incoming data
|
||||
}
|
||||
|
||||
@ -815,6 +820,7 @@ void LobbyServer::receiveSendInvite(const NetworkConnectionPtr & connection, con
|
||||
|
||||
database->insertGameRoomInvite(accountID, gameRoomID);
|
||||
sendInviteReceived(targetAccountConnection, senderName, gameRoomID);
|
||||
broadcastActiveGameRooms();
|
||||
}
|
||||
|
||||
LobbyServer::~LobbyServer() = default;
|
||||
|
@ -40,6 +40,9 @@ ArmyWidget::ArmyWidget(CArmedInstance & a, QWidget *parent) :
|
||||
uiSlots[i]->insertItem(c + 1, creature->getNamePluralTranslated().c_str());
|
||||
uiSlots[i]->setItemData(c + 1, creature->getIndex());
|
||||
}
|
||||
|
||||
uiSlots[i]->completer()->setCompletionMode(QCompleter::PopupCompletion);
|
||||
uiSlots[i]->completer()->setFilterMode(Qt::MatchContains);
|
||||
}
|
||||
|
||||
ui->formationTight->setChecked(true);
|
||||
|
@ -34,6 +34,9 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
@ -44,6 +47,9 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
@ -54,6 +60,9 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
@ -64,6 +73,9 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
@ -74,6 +86,9 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
@ -84,6 +99,9 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
@ -94,6 +112,9 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
|
@ -317,7 +317,7 @@ void Inspector::updateProperties(CGHeroInstance * o)
|
||||
addProperty("Skills", PropertyEditorPlaceholder(), delegate, false);
|
||||
addProperty("Spells", PropertyEditorPlaceholder(), new HeroSpellDelegate(*o), false);
|
||||
|
||||
if(o->type)
|
||||
if(o->type || o->ID == Obj::PRISON)
|
||||
{ //Hero type
|
||||
auto * delegate = new InspectorDelegate;
|
||||
for(int i = 0; i < VLC->heroh->objects.size(); ++i)
|
||||
@ -330,7 +330,7 @@ void Inspector::updateProperties(CGHeroInstance * o)
|
||||
}
|
||||
}
|
||||
}
|
||||
addProperty("Hero type", o->type->getNameTranslated(), delegate, false);
|
||||
addProperty("Hero type", o->type ? o->type->getNameTranslated() : "", delegate, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) :
|
||||
|
||||
//fill core elements
|
||||
for(const auto & s : Rewardable::VisitModeString)
|
||||
ui->selectMode->addItem(QString::fromUtf8(s.data(), s.size()));
|
||||
ui->visitMode->addItem(QString::fromUtf8(s.data(), s.size()));
|
||||
|
||||
for(const auto & s : Rewardable::SelectModeString)
|
||||
ui->selectMode->addItem(QString::fromUtf8(s.data(), s.size()));
|
||||
@ -636,10 +636,12 @@ void RewardsWidget::on_visitInfoList_itemSelectionChanged()
|
||||
if(ui->visitInfoList->currentItem() == nullptr)
|
||||
{
|
||||
ui->eventInfoGroup->hide();
|
||||
ui->removeVisitInfo->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ui->eventInfoGroup->show();
|
||||
ui->removeVisitInfo->setEnabled(true);
|
||||
}
|
||||
|
||||
void RewardsWidget::on_visitInfoList_currentItemChanged(QListWidgetItem * current, QListWidgetItem * previous)
|
||||
|
@ -36,6 +36,9 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeVisitInfo">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
|
@ -13,6 +13,10 @@
|
||||
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
#endif
|
||||
|
||||
QApplication vcmieditor(argc, argv);
|
||||
MainWindow mainWindow;
|
||||
return vcmieditor.exec();
|
||||
|
@ -90,6 +90,9 @@ void MainWindow::loadUserSettings()
|
||||
{
|
||||
move(position);
|
||||
}
|
||||
lastSavingDir = s.value(lastDirectorySetting).toString();
|
||||
if(lastSavingDir.isEmpty())
|
||||
lastSavingDir = QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string());
|
||||
}
|
||||
|
||||
void MainWindow::saveUserSettings()
|
||||
@ -97,6 +100,7 @@ void MainWindow::saveUserSettings()
|
||||
QSettings s(Ui::teamName, Ui::appName);
|
||||
s.setValue(mainWindowSizeSetting, size());
|
||||
s.setValue(mainWindowPositionSetting, pos());
|
||||
s.setValue(lastDirectorySetting, lastSavingDir);
|
||||
}
|
||||
|
||||
void MainWindow::parseCommandLine(ExtractionOptions & extractionOptions)
|
||||
@ -382,7 +386,7 @@ void MainWindow::on_actionOpen_triggered()
|
||||
return;
|
||||
|
||||
auto filenameSelect = QFileDialog::getOpenFileName(this, tr("Open map"),
|
||||
QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()),
|
||||
QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()),
|
||||
tr("All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m)"));
|
||||
if(filenameSelect.isEmpty())
|
||||
return;
|
||||
@ -439,11 +443,13 @@ void MainWindow::on_actionSave_as_triggered()
|
||||
if(filenameSelect.isNull())
|
||||
return;
|
||||
|
||||
if(filenameSelect == filename)
|
||||
return;
|
||||
QFileInfo fileInfo(filenameSelect);
|
||||
lastSavingDir = fileInfo.dir().path();
|
||||
|
||||
if(fileInfo.suffix().toLower() != "vmap")
|
||||
filenameSelect += ".vmap";
|
||||
|
||||
filename = filenameSelect;
|
||||
lastSavingDir = filenameSelect.remove(QUrl(filenameSelect).fileName());
|
||||
|
||||
saveMap();
|
||||
}
|
||||
@ -1171,7 +1177,7 @@ void MainWindow::on_actionTranslations_triggered()
|
||||
void MainWindow::on_actionh3m_converter_triggered()
|
||||
{
|
||||
auto mapFiles = QFileDialog::getOpenFileNames(this, tr("Select maps to convert"),
|
||||
QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()),
|
||||
QString::fromStdString(VCMIDirs::get().userDataPath().make_preferred().string()),
|
||||
tr("HoMM3 maps(*.h3m)"));
|
||||
if(mapFiles.empty())
|
||||
return;
|
||||
|
@ -27,6 +27,7 @@ class MainWindow : public QMainWindow
|
||||
|
||||
const QString mainWindowSizeSetting = "MainWindow/Size";
|
||||
const QString mainWindowPositionSetting = "MainWindow/Position";
|
||||
const QString lastDirectorySetting = "MainWindow/Directory";
|
||||
|
||||
#ifdef ENABLE_QT_TRANSLATIONS
|
||||
QTranslator translator;
|
||||
|
@ -1797,7 +1797,7 @@
|
||||
<message>
|
||||
<location filename="../windownewmap.cpp" line="296"/>
|
||||
<source>RMG failure</source>
|
||||
<translation>Falha do RMG</translation>
|
||||
<translation>Falha do GMA</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
|
@ -900,7 +900,7 @@ void CGameHandler::onNewTurn()
|
||||
{
|
||||
if (getPlayerStatus(player.first) == EPlayerStatus::INGAME &&
|
||||
getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES)
|
||||
changeFogOfWar(t->visitablePos(), t->getFirstBonus(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN);
|
||||
changeFogOfWar(t->getSightCenter(), t->getFirstBonus(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1550,7 +1550,7 @@ void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInsta
|
||||
|
||||
//Reveal fow around new hero, especially released from Prison
|
||||
auto h = getHero(id);
|
||||
changeFogOfWar(h->pos, h->getSightRadius(), player, ETileVisibility::REVEALED);
|
||||
changeFogOfWar(h->getSightCenter(), h->getSightRadius(), player, ETileVisibility::REVEALED);
|
||||
}
|
||||
|
||||
void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)
|
||||
@ -2434,10 +2434,10 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID,
|
||||
// now when everything is built - reveal tiles for lookout tower
|
||||
changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED);
|
||||
|
||||
if(t->garrisonHero) //garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
|
||||
visitCastleObjects(t, t->garrisonHero);
|
||||
if(t->visitingHero)
|
||||
visitCastleObjects(t, t->visitingHero);
|
||||
if(t->garrisonHero)
|
||||
visitCastleObjects(t, t->garrisonHero);
|
||||
|
||||
checkVictoryLossConditionsForPlayer(t->tempOwner);
|
||||
return true;
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include "processors/PlayerMessageProcessor.h"
|
||||
|
||||
#include "../lib/CHeroHandler.h"
|
||||
#include "../lib/CPlayerState.h"
|
||||
#include "../lib/MetaString.h"
|
||||
#include "../lib/registerTypes/RegisterTypesLobbyPacks.h"
|
||||
#include "../lib/serializer/CMemorySerializer.h"
|
||||
@ -332,6 +333,8 @@ void CVCMIServer::startGameImmediately()
|
||||
setState(EServerState::GAMEPLAY);
|
||||
lastTimerUpdateTime = gameplayStartTime = std::chrono::steady_clock::now();
|
||||
onTimer();
|
||||
|
||||
multiplayerWelcomeMessage();
|
||||
}
|
||||
|
||||
void CVCMIServer::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
|
||||
@ -979,6 +982,38 @@ ui8 CVCMIServer::getIdOfFirstUnallocatedPlayer() const
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CVCMIServer::multiplayerWelcomeMessage()
|
||||
{
|
||||
int humanPlayer = 0;
|
||||
for (auto & pi : si->playerInfos)
|
||||
if(gh->getPlayerState(pi.first)->isHuman())
|
||||
humanPlayer++;
|
||||
|
||||
if(humanPlayer < 2) // Singleplayer
|
||||
return;
|
||||
|
||||
std::vector<std::string> optionIds;
|
||||
if(si->extraOptionsInfo.cheatsAllowed)
|
||||
optionIds.push_back("vcmi.optionsTab.cheatAllowed.hover");
|
||||
if(si->extraOptionsInfo.unlimitedReplay)
|
||||
optionIds.push_back("vcmi.optionsTab.unlimitedReplay.hover");
|
||||
|
||||
if(!optionIds.size()) // No settings to publish
|
||||
return;
|
||||
|
||||
MetaString str;
|
||||
str.appendTextID("vcmi.optionsTab.extraOptions.hover");
|
||||
str.appendRawString(": ");
|
||||
for(int i = 0; i < optionIds.size(); i++)
|
||||
{
|
||||
str.appendTextID(optionIds[i]);
|
||||
if(i < optionIds.size() - 1)
|
||||
str.appendRawString(", ");
|
||||
}
|
||||
|
||||
gh->playerMessages->broadcastSystemMessage(str);
|
||||
}
|
||||
|
||||
INetworkHandler & CVCMIServer::getNetworkHandler()
|
||||
{
|
||||
return *networkHandler;
|
||||
|
@ -130,4 +130,6 @@ public:
|
||||
void setCampaignBonus(int bonusId);
|
||||
|
||||
ui8 getIdOfFirstUnallocatedPlayer() const;
|
||||
|
||||
void multiplayerWelcomeMessage();
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user