1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-04-23 12:08:45 +02:00

Merge pull request #3989 from vcmi/beta

Merge beta -> master
This commit is contained in:
Ivan Savenko 2024-05-16 13:08:20 +03:00 committed by GitHub
commit 194b3389f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
103 changed files with 1441 additions and 624 deletions

View File

@ -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

View File

@ -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();

View File

@ -96,6 +96,7 @@ public:
std::unique_ptr<boost::thread> makingTurn;
private:
boost::mutex turnInterruptionMutex;
bool announcedCheatingProblem;
public:
ObjectInstanceID selectedObject;

View File

@ -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));
}
}
}

View File

@ -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);
}

View File

@ -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()

View File

@ -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

View File

@ -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!",

View File

@ -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!",

View File

@ -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",

View File

@ -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 було вбито влучними пострілами!",

View File

@ -10,7 +10,7 @@ android {
applicationId "is.xyz.vcmi"
minSdk 19
targetSdk 33
versionCode 1511
versionCode 1513
versionName "1.5.1"
setProperty("archivesBaseName", "vcmi")
}

View File

@ -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();
}

View File

@ -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

View File

@ -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;

View File

@ -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));

View File

@ -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;

View File

@ -89,5 +89,6 @@ public:
bool optionMapViewActive();
void setState(EAdventureState newState);
EAdventureState getState();
void onMapViewMoved(const Rect & visibleArea, int mapLevel);
};

View File

@ -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

View File

@ -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());
}

View File

@ -127,7 +127,7 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
}
}
bool BattleInterface::openingPlaying()
bool BattleInterface::openingPlaying() const
{
return battleOpeningDelayActive;
}

View File

@ -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;

View File

@ -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;

View File

@ -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
{

View File

@ -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()

View File

@ -300,6 +300,11 @@ void InputHandler::fetchEvents()
}
}
bool InputHandler::isKeyboardCmdDown() const
{
return keyboardHandler->isKeyboardCmdDown();
}
bool InputHandler::isKeyboardCtrlDown() const
{
return keyboardHandler->isKeyboardCtrlDown();

View File

@ -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;
};

View File

@ -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

View File

@ -23,6 +23,7 @@ public:
void handleEventKeyUp(const SDL_KeyboardEvent & current);
bool isKeyboardAltDown() const;
bool isKeyboardCmdDown() const;
bool isKeyboardCtrlDown() const;
bool isKeyboardShiftDown() const;
};

View File

@ -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;
}

View File

@ -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;

View File

@ -29,6 +29,7 @@ struct GlobalLobbyRoom
std::string startDateFormatted;
ModCompatibilityInfo modList;
std::vector<GlobalLobbyAccount> participants;
std::vector<GlobalLobbyAccount> invited;
int playerLimit;
};

View File

@ -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();
}

View File

@ -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();
};

View File

@ -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();
}

View 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;
};

View File

@ -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())

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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;
}

View File

@ -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)

View File

@ -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"

View File

@ -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);

View File

@ -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();
}

View File

@ -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]);

View File

@ -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);
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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));

View File

@ -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()

View File

@ -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

View 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
View 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;
};

View File

@ -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;
}
}

View File

@ -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;
};

View File

@ -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();

View File

@ -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;

View File

@ -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();
}

View File

@ -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",

View File

@ -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",

View File

@ -374,7 +374,7 @@
},
"backgroundDimSmallWindows" : {
"type" : "boolean",
"default" : true
"default" : false
}
}
},

View File

@ -130,7 +130,6 @@
{
"name" : "messageInput",
"type": "textInput",
"alignment" : "left",
"rect": {"x": 440, "y": 568, "w": 377, "h": 20}
},

View File

@ -1,5 +1,6 @@
[![VCMI](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg?branch=develop&event=push)](https://github.com/vcmi/vcmi/actions/workflows/github.yml?query=branch%3Adevelop+event%3Apush)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.0)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.5.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.5.1)
[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
# VCMI Project

View File

@ -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>

View File

@ -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&apos;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>

View File

@ -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
}

View File

@ -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);

View File

@ -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);

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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);

View File

@ -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
}

View File

@ -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

View File

@ -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);

View File

@ -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;
};

View File

@ -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)

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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:

View File

@ -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;
}

View File

@ -48,6 +48,7 @@ class LobbyDatabase
SQLiteStatementPtr getAccountGameRoomStatement;
SQLiteStatementPtr getAccountDisplayNameStatement;
SQLiteStatementPtr getGameRoomPlayersStatement;
SQLiteStatementPtr getGameRoomInvitesStatement;
SQLiteStatementPtr countRoomUsedSlotsStatement;
SQLiteStatementPtr countRoomTotalSlotsStatement;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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">

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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