From be7d27234fb9a5f07e40a3638c2552c6aae56a9e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:17:05 +0200 Subject: [PATCH 01/20] map drag with right mouse --- client/eventsSDL/InputSourceMouse.cpp | 4 ++- client/gui/EventDispatcher.cpp | 8 ++++++ client/gui/EventDispatcher.h | 2 ++ client/gui/EventsReceiver.h | 4 ++- client/mapView/MapViewActions.cpp | 7 +++++- client/mapView/MapViewActions.h | 1 + client/windows/InfoWindows.cpp | 36 ++++++++++++++++++++++++++- client/windows/InfoWindows.h | 8 ++++++ config/schemas/settings.json | 2 +- 9 files changed, 67 insertions(+), 5 deletions(-) diff --git a/client/eventsSDL/InputSourceMouse.cpp b/client/eventsSDL/InputSourceMouse.cpp index d043fa30f..d23f6877d 100644 --- a/client/eventsSDL/InputSourceMouse.cpp +++ b/client/eventsSDL/InputSourceMouse.cpp @@ -33,7 +33,7 @@ InputSourceMouse::InputSourceMouse() void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motion) { Point newPosition = Point(motion.x, motion.y) / GH.screenHandler().getScalingFactor(); - Point distance= Point(-motion.xrel, -motion.yrel) / GH.screenHandler().getScalingFactor(); + Point distance = Point(-motion.xrel, -motion.yrel) / GH.screenHandler().getScalingFactor(); mouseButtonsMask = motion.state; @@ -41,6 +41,8 @@ void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motio GH.events().dispatchGesturePanning(middleClickPosition, newPosition, distance); else if (mouseButtonsMask & SDL_BUTTON(SDL_BUTTON_LEFT)) GH.events().dispatchMouseDragged(newPosition, distance); + else if (mouseButtonsMask & SDL_BUTTON(SDL_BUTTON_RIGHT)) + GH.events().dispatchMouseDraggedPopup(newPosition, distance); else GH.input().setCursorPosition(newPosition); } diff --git a/client/gui/EventDispatcher.cpp b/client/gui/EventDispatcher.cpp index 0518d305f..b43f8d209 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -35,6 +35,7 @@ void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb) processList(AEventsReceiver::HOVER, hoverable); processList(AEventsReceiver::MOVE, motioninterested); processList(AEventsReceiver::DRAG, draginterested); + processList(AEventsReceiver::DRAG_POPUP, dragPopupInterested); processList(AEventsReceiver::KEYBOARD, keyinterested); processList(AEventsReceiver::TIME, timeinterested); processList(AEventsReceiver::WHEEL, wheelInterested); @@ -433,3 +434,10 @@ void EventDispatcher::dispatchMouseDragged(const Point & currentPosition, const elem->mouseDragged(currentPosition, lastUpdateDistance); } } + +void EventDispatcher::dispatchMouseDraggedPopup(const Point & currentPosition, const Point & lastUpdateDistance) +{ + EventReceiversList diCopy = dragPopupInterested; + for(auto & elem : diCopy) + elem->mouseDraggedPopup(currentPosition, lastUpdateDistance); +} diff --git a/client/gui/EventDispatcher.h b/client/gui/EventDispatcher.h index cd61a6b0c..4c212779e 100644 --- a/client/gui/EventDispatcher.h +++ b/client/gui/EventDispatcher.h @@ -30,6 +30,7 @@ class EventDispatcher EventReceiversList keyinterested; EventReceiversList motioninterested; EventReceiversList draginterested; + EventReceiversList dragPopupInterested; EventReceiversList timeinterested; EventReceiversList wheelInterested; EventReceiversList doubleClickInterested; @@ -66,6 +67,7 @@ public: void dispatchMouseMoved(const Point & distance, const Point & position); void dispatchMouseDragged(const Point & currentPosition, const Point & lastUpdateDistance); + void dispatchMouseDraggedPopup(const Point & currentPosition, const Point & lastUpdateDistance); void dispatchShowPopup(const Point & position, int tolerance); void dispatchClosePopup(const Point & position); diff --git a/client/gui/EventsReceiver.h b/client/gui/EventsReceiver.h index 6727dec87..da194890f 100644 --- a/client/gui/EventsReceiver.h +++ b/client/gui/EventsReceiver.h @@ -61,6 +61,7 @@ public: virtual void wheelScrolled(int distance) {} virtual void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) {} virtual void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) {} + virtual void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) {} /// Called when UI element hover status changes virtual void hover(bool on) {} @@ -97,7 +98,8 @@ public: TEXTINPUT = 512, GESTURE = 1024, DRAG = 2048, - INPUT_MODE_CHANGE = 4096 + INPUT_MODE_CHANGE = 4096, + DRAG_POPUP = 8192 }; /// Returns true if element is currently hovered by mouse diff --git a/client/mapView/MapViewActions.cpp b/client/mapView/MapViewActions.cpp index 6c87edba4..6b2041a57 100644 --- a/client/mapView/MapViewActions.cpp +++ b/client/mapView/MapViewActions.cpp @@ -34,7 +34,7 @@ MapViewActions::MapViewActions(MapView & owner, const std::shared_ptrgetPixelsVisibleDimensions().x; pos.h = model->getPixelsVisibleDimensions().y; - addUsedEvents(LCLICK | SHOW_POPUP | DRAG | GESTURE | HOVER | MOVE | WHEEL); + addUsedEvents(LCLICK | SHOW_POPUP | DRAG | DRAG_POPUP | GESTURE | HOVER | MOVE | WHEEL); } void MapViewActions::setContext(const std::shared_ptr & context) @@ -101,6 +101,11 @@ void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & la owner.onMapSwiped(lastUpdateDistance); } +void MapViewActions::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + owner.onMapSwiped(lastUpdateDistance); +} + void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) { owner.onMapSwiped(lastUpdateDistance); diff --git a/client/mapView/MapViewActions.h b/client/mapView/MapViewActions.h index 02dd877aa..463f70780 100644 --- a/client/mapView/MapViewActions.h +++ b/client/mapView/MapViewActions.h @@ -42,6 +42,7 @@ public: void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override; + void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override; void wheelScrolled(int distance) override; bool dragActive; diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 4eaa690be..ba15715b8 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -245,8 +245,11 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, } } -CRClickPopupInt::CRClickPopupInt(const std::shared_ptr & our) +CRClickPopupInt::CRClickPopupInt(const std::shared_ptr & our) : + dragDistance(Point(0, 0)) { + addUsedEvents(DRAG_POPUP); + CCS->curh->hide(); inner = our; addChild(our.get(), false); @@ -257,6 +260,17 @@ CRClickPopupInt::~CRClickPopupInt() CCS->curh->show(); } +void CRClickPopupInt::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + if(!settings["adventure"]["rightButtonDrag"].Bool()) + return; + + dragDistance += lastUpdateDistance; + + if(dragDistance.length() > 16) + close(); +} + Point CInfoBoxPopup::toScreen(Point p) { auto bounds = adventureInt->terrainAreaPixels(); @@ -267,6 +281,18 @@ Point CInfoBoxPopup::toScreen(Point p) return p; } +void CInfoBoxPopup::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + if(!settings["adventure"]["rightButtonDrag"].Bool()) + return; + + dragDistance += lastUpdateDistance; + + if(dragDistance.length() > 16) + close(); +} + + CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) { @@ -275,6 +301,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), iah); + + addUsedEvents(DRAG_POPUP); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) @@ -285,6 +313,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), iah); + + addUsedEvents(DRAG_POPUP); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) @@ -295,6 +325,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), iah); + + addUsedEvents(DRAG_POPUP); } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature) @@ -302,6 +334,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature) { OBJECT_CONSTRUCTION; tooltip = std::make_shared(Point(9, 10), creature); + + addUsedEvents(DRAG_POPUP); } std::shared_ptr diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index 5da10014b..a06416934 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -78,9 +78,13 @@ class CRClickPopupInt : public CRClickPopup { std::shared_ptr inner; + Point dragDistance; + public: CRClickPopupInt(const std::shared_ptr & our); ~CRClickPopupInt(); + + void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override; }; /// popup on adventure map for town\hero and other objects with customized popup content @@ -89,11 +93,15 @@ class CInfoBoxPopup : public CWindowObject std::shared_ptr tooltip; Point toScreen(Point pos); + Point dragDistance; + public: CInfoBoxPopup(Point position, const CGTownInstance * town); CInfoBoxPopup(Point position, const CGHeroInstance * hero); CInfoBoxPopup(Point position, const CGGarrison * garr); CInfoBoxPopup(Point position, const CGCreature * creature); + + void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override; }; /// component selection window diff --git a/config/schemas/settings.json b/config/schemas/settings.json index d1e70bc4f..5dc55b828 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -316,7 +316,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "rightButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows" ], "properties" : { "heroMoveTime" : { "type" : "number", From 8f24778e9db714c88fb94eeebca914b2245c613b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:36:41 +0200 Subject: [PATCH 02/20] config for right drag --- Mods/vcmi/config/vcmi/english.json | 6 +++-- Mods/vcmi/config/vcmi/german.json | 6 +++-- .../windows/settings/AdventureOptionsTab.cpp | 8 ++++++ .../widgets/settings/adventureOptionsTab.json | 26 +++++++++++++++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 1cf794845..85e283e34 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -234,8 +234,10 @@ "vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nScroll adventure map when cursor is adjacent to window edge. Can be disabled by holding down CTRL key.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Right Click Drag", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Right Click Drag}\n\nWhen enabled, moving mouse with right button pressed will drag adventure map view.", "vcmi.adventureOptions.smoothDragging.hover" : "Smooth Map Dragging", "vcmi.adventureOptions.smoothDragging.help" : "{Smooth Map Dragging}\n\nWhen enabled, map dragging has a modern run out effect.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skip fading effects", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 3a8b9cea7..46ee28c79 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -234,8 +234,10 @@ "vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen mit Links", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in der Kartenansicht gezogen", + "vcmi.adventureOptions.rightButtonDrag.hover" : "Ziehen mit Rechts", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Ziehen mit Rechts}\n\nWenn aktiviert, wird die Maus bei gedrückter rechter Taste in der Kartenansicht gezogen", "vcmi.adventureOptions.smoothDragging.hover" : "Nahtloses Ziehen der Karte", "vcmi.adventureOptions.smoothDragging.help" : "{Nahtloses Ziehen der Karte}\n\nWenn aktiviert hat das Ziehen der Karte einen sanften Auslaufeffekt.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fading-Effekte überspringen", diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index 21e289b60..30f98f76c 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -126,6 +126,10 @@ AdventureOptionsTab::AdventureOptionsTab() { return setBoolSetting("adventure", "leftButtonDrag", value); }); + addCallback("rightButtonDragChanged", [](bool value) + { + return setBoolSetting("adventure", "rightButtonDrag", value); + }); addCallback("smoothDraggingChanged", [](bool value) { return setBoolSetting("adventure", "smoothDragging", value); @@ -177,6 +181,10 @@ AdventureOptionsTab::AdventureOptionsTab() if (leftButtonDragCheckbox) leftButtonDragCheckbox->setSelected(settings["adventure"]["leftButtonDrag"].Bool()); + std::shared_ptr rightButtonDragCheckbox = widget("rightButtonDragCheckbox"); + if (rightButtonDragCheckbox) + rightButtonDragCheckbox->setSelected(settings["adventure"]["rightButtonDrag"].Bool()); + std::shared_ptr smoothDraggingCheckbox = widget("smoothDraggingCheckbox"); if (smoothDraggingCheckbox) smoothDraggingCheckbox->setSelected(settings["adventure"]["smoothDragging"].Bool()); diff --git a/config/widgets/settings/adventureOptionsTab.json b/config/widgets/settings/adventureOptionsTab.json index 57886e5e2..33b48a884 100644 --- a/config/widgets/settings/adventureOptionsTab.json +++ b/config/widgets/settings/adventureOptionsTab.json @@ -371,6 +371,18 @@ } ] }, + { + "type": "verticalLayout", + "customType": "labelDescription", + "position": {"x": 225, "y": 415}, + "items": + [ + { + "text": "vcmi.adventureOptions.rightButtonDrag.hover", + "created" : "desktop" + } + ] + }, { "type": "verticalLayout", "customType": "checkbox", @@ -419,6 +431,20 @@ "callback": "smoothDraggingChanged" } ] + }, + { + "type": "verticalLayout", + "customType": "checkbox", + "position": {"x": 190, "y": 413}, + "items": + [ + { + "name": "rightButtonDragCheckbox", + "help": "vcmi.adventureOptions.rightButtonDrag", + "callback": "rightButtonDragChanged", + "created" : "desktop" + } + ] } ] } From 8ae4a4ab8ab7e7e88fc5e430f0bacbba10addd49 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 28 Aug 2024 22:49:14 +0200 Subject: [PATCH 03/20] better german --- Mods/vcmi/config/vcmi/german.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 46ee28c79..dde909330 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -235,9 +235,9 @@ "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement", "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln", "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen mit Links", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in der Kartenansicht gezogen", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen mit Links}\n\nWenn aktiviert, kann mit gedrückter linker Taste die Kartenansicht gezogen werden", "vcmi.adventureOptions.rightButtonDrag.hover" : "Ziehen mit Rechts", - "vcmi.adventureOptions.rightButtonDrag.help" : "{Ziehen mit Rechts}\n\nWenn aktiviert, wird die Maus bei gedrückter rechter Taste in der Kartenansicht gezogen", + "vcmi.adventureOptions.rightButtonDrag.help" : "{Ziehen mit Rechts}\n\nWenn aktiviert, kann mit gedrückter rechter Taste die Kartenansicht gezogen werden", "vcmi.adventureOptions.smoothDragging.hover" : "Nahtloses Ziehen der Karte", "vcmi.adventureOptions.smoothDragging.help" : "{Nahtloses Ziehen der Karte}\n\nWenn aktiviert hat das Ziehen der Karte einen sanften Auslaufeffekt.", "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fading-Effekte überspringen", From aa6cbdf13b959e07496442a2fff401cb32e8b7c9 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:07:00 +0200 Subject: [PATCH 04/20] make it also usable on android --- client/windows/settings/AdventureOptionsTab.cpp | 4 ++++ client/windows/settings/GeneralOptionsTab.cpp | 4 +++- client/windows/settings/SettingsMainWindow.cpp | 5 +++++ client/windows/settings/SettingsMainWindow.h | 1 + config/widgets/settings/adventureOptionsTab.json | 8 ++++---- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index 30f98f76c..0fa168a7b 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -11,6 +11,7 @@ #include "AdventureOptionsTab.h" +#include "../../eventsSDL/InputHandler.h" #include "../../../lib/filesystem/ResourcePath.h" #include "../../gui/CGuiHandler.h" #include "../../widgets/Buttons.h" @@ -36,6 +37,9 @@ AdventureOptionsTab::AdventureOptionsTab() OBJECT_CONSTRUCTION; setRedrawParent(true); + addConditional("touchscreen", GH.input().getCurrentInputMode() == InputMode::TOUCH); + addConditional("keyboardMouse", GH.input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE); + addConditional("controller", GH.input().getCurrentInputMode() == InputMode::CONTROLLER); #ifdef VCMI_MOBILE addConditional("mobile", true); addConditional("desktop", false); diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index b0a965414..d8ead20f8 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -97,7 +97,9 @@ GeneralOptionsTab::GeneralOptionsTab() OBJECT_CONSTRUCTION; setRedrawParent(true); - addConditional("touchscreen", GH.input().hasTouchInputDevice()); + addConditional("touchscreen", GH.input().getCurrentInputMode() == InputMode::TOUCH); + addConditional("keyboardMouse", GH.input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE); + addConditional("controller", GH.input().getCurrentInputMode() == InputMode::CONTROLLER); #ifdef VCMI_MOBILE addConditional("mobile", true); addConditional("desktop", false); diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index 6b7b53a1b..af6b4769d 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -196,3 +196,8 @@ void SettingsMainWindow::onScreenResize() if (tab) tab->updateResolutionSelector(); } + +void SettingsMainWindow::inputModeChanged(InputMode mode) +{ + tabContentArea->reset(); +} diff --git a/client/windows/settings/SettingsMainWindow.h b/client/windows/settings/SettingsMainWindow.h index 7b26921df..5813ac46b 100644 --- a/client/windows/settings/SettingsMainWindow.h +++ b/client/windows/settings/SettingsMainWindow.h @@ -42,5 +42,6 @@ public: void showAll(Canvas & to) override; void onScreenResize() override; + void inputModeChanged(InputMode mode) override; }; diff --git a/config/widgets/settings/adventureOptionsTab.json b/config/widgets/settings/adventureOptionsTab.json index 33b48a884..3cddfb4ac 100644 --- a/config/widgets/settings/adventureOptionsTab.json +++ b/config/widgets/settings/adventureOptionsTab.json @@ -364,7 +364,7 @@ }, { "text": "vcmi.adventureOptions.leftButtonDrag.hover", - "created" : "desktop" + "created" : "keyboardMouse" }, { "text": "vcmi.adventureOptions.smoothDragging.hover" @@ -379,7 +379,7 @@ [ { "text": "vcmi.adventureOptions.rightButtonDrag.hover", - "created" : "desktop" + "created" : "keyboardMouse" } ] }, @@ -423,7 +423,7 @@ "name": "leftButtonDragCheckbox", "help": "vcmi.adventureOptions.leftButtonDrag", "callback": "leftButtonDragChanged", - "created" : "desktop" + "created" : "keyboardMouse" }, { "name": "smoothDraggingCheckbox", @@ -442,7 +442,7 @@ "name": "rightButtonDragCheckbox", "help": "vcmi.adventureOptions.rightButtonDrag", "callback": "rightButtonDragChanged", - "created" : "desktop" + "created" : "keyboardMouse" } ] } From 091523d67b49fbde38046251c6fd4a37eaca93c1 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:37:38 +0200 Subject: [PATCH 05/20] add missing event --- client/windows/settings/SettingsMainWindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index af6b4769d..05562bae5 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -45,6 +45,8 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter addCallback("closeWindow", [this](int) { backButtonCallback(); }); build(config); + addUsedEvents(INPUT_MODE_CHANGE); + std::shared_ptr background = widget("background"); pos.w = background->pos.w; pos.h = background->pos.h; From 822fa61bf2799616d7b7ecfe862477539c153ff6 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 12 Sep 2024 22:28:45 +0200 Subject: [PATCH 06/20] intro rework --- client/CServerHandler.cpp | 6 +++--- client/NetPacksLobbyClient.cpp | 4 ++-- client/lobby/CBonusSelection.cpp | 25 +++++++++++++++++++++---- client/lobby/CBonusSelection.h | 5 ++++- client/mainmenu/CMainMenu.cpp | 23 +++++++++++++++++++---- client/mainmenu/CMainMenu.h | 6 ++++-- client/media/CEmptyVideoPlayer.h | 8 +------- client/media/CVideoHandler.cpp | 23 +++++++++-------------- client/media/CVideoHandler.h | 7 +++---- client/media/IVideoPlayer.h | 5 +---- client/widgets/VideoWidget.cpp | 7 ++++++- client/widgets/VideoWidget.h | 2 ++ client/windows/CWindowObject.cpp | 2 +- clientapp/EntryPoint.cpp | 24 +++--------------------- 14 files changed, 79 insertions(+), 68 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 4e05ce505..681d3e84c 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -664,7 +664,7 @@ void CServerHandler::endGameplay() } else { - GH.curInt = CMainMenu::create().get(); + GH.curInt = CMainMenu::create(false).get(); } } @@ -708,10 +708,10 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared else { CMM->openCampaignScreen(ourCampaign->campaignSet); - if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), false)) + if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), Point(0, 0))) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), [campaignScoreCalculator, statistic](){ + GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, [campaignScoreCalculator, statistic](){ GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); }); } diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index f280b4d44..b01b71862 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -207,10 +207,10 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & { auto bonusSel = std::make_shared(); lobby->bonusSel = bonusSel; - if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), false)) + if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), Point(0, 0))) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), [bonusSel](){ + GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, [bonusSel](){ if(!CSH->si->campState->getMusic().empty()) CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false); GH.windows().pushWindow(bonusSel); diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index d0790335f..5d1c56a2b 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -59,17 +59,29 @@ #include "../../lib/mapObjects/CGHeroInstance.h" -CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, std::function closeCb) +CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, std::function closeCb) : CWindowObject(BORDERED), closeCb(closeCb) { OBJECT_CONSTRUCTION; addUsedEvents(LCLICK | KEYBOARD); - pos = center(Rect(0, 0, 800, 600)); + if(!rim.empty()) + { + videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(); }); + pos = center(Rect(0, 0, 800, 600)); + } + else + { + videoPlayer = std::make_shared(Point(0, 0), video, true, [this](){ exit(); }); + pos = center(Rect(0, 0, videoPlayer->size().x, videoPlayer->size().y)); + } - videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(); }); - setBackground(rim); + if(showBackground) + backgroundAroundWindow = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y)); + + if(!rim.empty()) + setBackground(rim); } void CampaignRimVideo::exit() @@ -89,6 +101,11 @@ void CampaignRimVideo::keyPressed(EShortcut key) exit(); } +bool CampaignRimVideo::receiveEvent(const Point & position, int eventType) const +{ + return true; // capture click also outside of window +} + std::shared_ptr CBonusSelection::getCampaign() { return CSH->si->campState; diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 176429f8d..3451e7f1c 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -31,20 +31,23 @@ class ISelectionScreenInfo; class ExtraOptionsTab; class VideoWidgetOnce; class CBonusSelection; +class CFilledTexture; class CampaignRimVideo : public CWindowObject { std::shared_ptr videoPlayer; + std::shared_ptr backgroundAroundWindow; std::function closeCb; void exit(); public: - CampaignRimVideo(VideoPath video, ImagePath rim, std::function closeCb); + CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, std::function closeCb); void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; + bool receiveEvent(const Point & position, int eventType) const override; }; /// Campaign screen where you can choose one out of three starting bonuses diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 54cd0d7e0..df9b82493 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -18,6 +18,7 @@ #include "../lobby/CSelectionBase.h" #include "../lobby/CLobbyScreen.h" #include "../media/IMusicPlayer.h" +#include "../media/IVideoPlayer.h" #include "../gui/CursorHandler.h" #include "../windows/GUIClasses.h" #include "../gui/CGuiHandler.h" @@ -117,7 +118,6 @@ void CMenuScreen::show(Canvas & to) void CMenuScreen::activate() { - CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); CIntObject::activate(); } @@ -284,7 +284,7 @@ const JsonNode & CMainMenuConfig::getCampaigns() const return campaignSets; } -CMainMenu::CMainMenu() +CMainMenu::CMainMenu(bool playVideoIntro) { pos.w = GH.screenDimensions().x; pos.h = GH.screenDimensions().y; @@ -292,6 +292,21 @@ CMainMenu::CMainMenu() menu = std::make_shared(CMainMenuConfig::get().getConfig()["window"]); OBJECT_CONSTRUCTION; backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); + + if(playVideoIntro) + { + auto playVideo = [](std::string video, bool rim, std::function cb){ + if(CCS->videoh->open(VideoPath::builtin(video), Point(0, 0))) + GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, [cb](){ cb(); }); + else + cb(); + }; + playVideo("3DOLOGO.SMK", false, [playVideo](){ playVideo("NWCLOGO.SMK", false, [playVideo](){ playVideo("H3INTRO.SMK", true, [](){ + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); + }); }); }); + } + else + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); } CMainMenu::~CMainMenu() @@ -418,10 +433,10 @@ void CMainMenu::openHighScoreScreen() return; } -std::shared_ptr CMainMenu::create() +std::shared_ptr CMainMenu::create(bool playVideoIntro) { if(!CMM) - CMM = std::shared_ptr(new CMainMenu()); + CMM = std::shared_ptr(new CMainMenu(playVideoIntro)); return CMM; } diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index d7e9c5a1e..1b4b263ec 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -142,7 +142,9 @@ class CMainMenu : public CIntObject, public IUpdateable, public std::enable_shar { std::shared_ptr backgroundAroundMenu; - CMainMenu(); //Use CMainMenu::create + std::vector videoPlayList; + + CMainMenu(bool playVideoIntro); //Use CMainMenu::create public: std::shared_ptr menu; @@ -158,7 +160,7 @@ public: static void openHighScoreScreen(); void openCampaignScreen(std::string name); - static std::shared_ptr create(); + static std::shared_ptr create(bool playVideoIntro); static std::shared_ptr createPicture(const JsonNode & config); diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h index 6dc91abaf..fcc191421 100644 --- a/client/media/CEmptyVideoPlayer.h +++ b/client/media/CEmptyVideoPlayer.h @@ -14,18 +14,12 @@ class CEmptyVideoPlayer final : public IVideoPlayer { public: - /// Plays video on top of the screen, returns only after playback is over - bool playIntroVideo(const VideoPath & name) override - { - return false; - }; - void playSpellbookAnimation(const VideoPath & name, const Point & position) override { } /// Load video from specified path - std::unique_ptr open(const VideoPath & name, bool scaleToScreen) override + std::unique_ptr open(const VideoPath & name, const Point & scale) override { return nullptr; }; diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 8a648646f..45c1c1bdf 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -173,13 +173,13 @@ void CVideoInstance::openVideo() openCodec(findVideoStream()); } -void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput) +void CVideoInstance::prepareOutput(Point scale, bool useTextureOutput) { //setup scaling - if(scaleToScreenSize) + if(scale.x > 0 && scale.y > 0) { - dimensions.x = screen->w; - dimensions.y = screen->h; + dimensions.x = scale.x * GH.screenHandler().getScalingFactor(); + dimensions.y = scale.y * GH.screenHandler().getScalingFactor(); } else { @@ -575,7 +575,7 @@ std::pair, si64> CAudioInstance::extractAudio(const Vide return dat; } -bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey) +bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey) { CVideoInstance instance; CAudioInstance audio; @@ -587,7 +587,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po return true; instance.openVideo(); - instance.prepareOutput(scale, true); + instance.prepareOutput(Point(0, 0), true); auto lastTimePoint = boost::chrono::steady_clock::now(); @@ -633,17 +633,12 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po return true; } -bool CVideoPlayer::playIntroVideo(const VideoPath & name) -{ - return openAndPlayVideoImpl(name, Point(0, 0), true, true, true); -} - void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position) { - openAndPlayVideoImpl(name, position * GH.screenHandler().getScalingFactor(), false, false, false); + openAndPlayVideoImpl(name, position * GH.screenHandler().getScalingFactor(), false, false); } -std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool scaleToScreen) +std::unique_ptr CVideoPlayer::open(const VideoPath & name, const Point & scale) { auto result = std::make_unique(); @@ -651,7 +646,7 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, bool return nullptr; result->openVideo(); - result->prepareOutput(scaleToScreen, false); + result->prepareOutput(scale, false); result->loadNextFrame(); // prepare 1st frame return result; diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 3fc6256cd..11bfa844a 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -80,7 +80,7 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream /// video playback current progress, in seconds double frameTime = 0.0; - void prepareOutput(bool scaleToScreenSize, bool useTextureOutput); + void prepareOutput(Point scale, bool useTextureOutput); public: ~CVideoInstance(); @@ -97,13 +97,12 @@ public: class CVideoPlayer final : public IVideoPlayer { - bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey); + bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey); void openVideoFile(CVideoInstance & state, const VideoPath & fname); public: - bool playIntroVideo(const VideoPath & name) final; void playSpellbookAnimation(const VideoPath & name, const Point & position) final; - std::unique_ptr open(const VideoPath & name, bool scaleToScreen) final; + std::unique_ptr open(const VideoPath & name, const Point & scale) final; std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index a867e9b24..4c4c3b5d3 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -38,14 +38,11 @@ public: class IVideoPlayer : boost::noncopyable { public: - /// Plays video on top of the screen, returns only after playback is over, aborts on input event - virtual bool playIntroVideo(const VideoPath & name) = 0; - /// Plays video on top of the screen, returns only after playback is over virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) = 0; /// Load video from specified path. Returns nullptr on failure - virtual std::unique_ptr open(const VideoPath & name, bool scaleToScreen) = 0; + virtual std::unique_ptr open(const VideoPath & name, const Point & scale) = 0; /// Extracts audio data from provided video in wav format virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index 2d68b82ca..a6281de6e 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -28,7 +28,7 @@ VideoWidgetBase::~VideoWidgetBase() = default; void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) { - videoInstance = CCS->videoh->open(fileToPlay, false); + videoInstance = CCS->videoh->open(fileToPlay, Point(0, 0)); if (videoInstance) { pos.w = videoInstance->size().x; @@ -119,6 +119,11 @@ void VideoWidgetBase::tick(uint32_t msPassed) } } +Point VideoWidgetBase::size() +{ + return videoInstance->size(); +} + VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio) : VideoWidgetBase(position, prologue, playAudio) , loopedVideo(looped) diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h index f7c963d51..22bf10457 100644 --- a/client/widgets/VideoWidget.h +++ b/client/widgets/VideoWidget.h @@ -42,6 +42,8 @@ public: void showAll(Canvas & to) override; void tick(uint32_t msPassed) override; + Point size(); + void setPlaybackFinishedCallback(std::function); }; diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index 2b88f6064..282dec7a9 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -59,7 +59,7 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName): background(createBg(imageName, options_ & PLAYER_COLORED)) { if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) - assert(parent == nullptr); //Safe to remove, but windows should not have parent + //assert(parent == nullptr); //Safe to remove, but windows should not have parent if(options & RCLICK_POPUP) CCS->curh->hide(); diff --git a/clientapp/EntryPoint.cpp b/clientapp/EntryPoint.cpp index e32303f07..0da0ff24f 100644 --- a/clientapp/EntryPoint.cpp +++ b/clientapp/EntryPoint.cpp @@ -30,6 +30,7 @@ #include "../client/render/Graphics.h" #include "../client/render/IRenderHandler.h" #include "../client/render/IScreenHandler.h" +#include "../client/lobby/CBonusSelection.h" #include "../client/windows/CMessage.h" #include "../client/windows/InfoWindows.h" @@ -65,7 +66,6 @@ static std::optional criticalInitializationError; #ifndef VCMI_IOS void processCommand(const std::string &message); #endif -void playIntro(); [[noreturn]] static void quitApplication(); static void mainLoop(); @@ -319,13 +319,6 @@ int main(int argc, char * argv[]) init(); #endif - if(!settings["session"]["headless"].Bool()) - { - if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool()) - playIntro(); - GH.screenHandler().clearScreen(); - } - #ifndef VCMI_NO_THREADED_LOAD #ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds { @@ -379,7 +372,8 @@ int main(int argc, char * argv[]) } else { - auto mmenu = CMainMenu::create(); + bool playIntroVideo = !settings["session"]["headless"].Bool() && !vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool(); + auto mmenu = CMainMenu::create(playIntroVideo); GH.curInt = mmenu.get(); } @@ -402,18 +396,6 @@ int main(int argc, char * argv[]) return 0; } -//plays intro, ends when intro is over or button has been pressed (handles events) -void playIntro() -{ - if(!CCS->videoh->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK"))) - return; - - if (!CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK"))) - return; - - CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK")); -} - static void mainLoop() { #ifndef VCMI_UNIX From 8fc3fe3cc30d4412518b96262b5dfaca8d641f4e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:06:33 +0200 Subject: [PATCH 07/20] scaling --- client/CServerHandler.cpp | 4 ++-- client/NetPacksLobbyClient.cpp | 4 ++-- client/lobby/CBonusSelection.cpp | 6 +++--- client/lobby/CBonusSelection.h | 2 +- client/mainmenu/CMainMenu.cpp | 16 ++++++++++------ client/media/CEmptyVideoPlayer.h | 2 +- client/media/CVideoHandler.cpp | 23 ++++++----------------- client/media/CVideoHandler.h | 4 ++-- client/media/IVideoPlayer.h | 2 +- client/widgets/VideoWidget.cpp | 20 +++++++++++++------- client/widgets/VideoWidget.h | 5 +++-- 11 files changed, 44 insertions(+), 44 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 681d3e84c..31e891cba 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -708,10 +708,10 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared else { CMM->openCampaignScreen(ourCampaign->campaignSet); - if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), Point(0, 0))) + if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), 1)) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, [campaignScoreCalculator, statistic](){ + GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, 1, [campaignScoreCalculator, statistic](){ GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); }); } diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index b01b71862..5c7822d97 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -207,10 +207,10 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & { auto bonusSel = std::make_shared(); lobby->bonusSel = bonusSel; - if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), Point(0, 0))) + if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), 1)) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, [bonusSel](){ + GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, 1, [bonusSel](){ if(!CSH->si->campState->getMusic().empty()) CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false); GH.windows().pushWindow(bonusSel); diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 5d1c56a2b..b6c5afc87 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -59,7 +59,7 @@ #include "../../lib/mapObjects/CGHeroInstance.h" -CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, std::function closeCb) +CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb) : CWindowObject(BORDERED), closeCb(closeCb) { OBJECT_CONSTRUCTION; @@ -73,8 +73,8 @@ CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBack } else { - videoPlayer = std::make_shared(Point(0, 0), video, true, [this](){ exit(); }); - pos = center(Rect(0, 0, videoPlayer->size().x, videoPlayer->size().y)); + videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, [this](){ exit(); }); + pos = center(Rect(0, 0, videoPlayer->pos.w, videoPlayer->pos.h)); } if(showBackground) diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 3451e7f1c..13223f321 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -43,7 +43,7 @@ class CampaignRimVideo : public CWindowObject void exit(); public: - CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, std::function closeCb); + CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb); void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index df9b82493..835157d59 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -295,15 +295,19 @@ CMainMenu::CMainMenu(bool playVideoIntro) if(playVideoIntro) { - auto playVideo = [](std::string video, bool rim, std::function cb){ - if(CCS->videoh->open(VideoPath::builtin(video), Point(0, 0))) - GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, [cb](){ cb(); }); + auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function cb){ + if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor)) + GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](){ cb(); }); else cb(); }; - playVideo("3DOLOGO.SMK", false, [playVideo](){ playVideo("NWCLOGO.SMK", false, [playVideo](){ playVideo("H3INTRO.SMK", true, [](){ - CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); - }); }); }); + playVideo("3DOLOGO.SMK", false, 1, [playVideo](){ + playVideo("NWCLOGO.SMK", false, 2, [playVideo](){ + playVideo("H3INTRO.SMK", true, 1, [](){ + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); + }); + }); + }); } else CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); diff --git a/client/media/CEmptyVideoPlayer.h b/client/media/CEmptyVideoPlayer.h index fcc191421..619591c9b 100644 --- a/client/media/CEmptyVideoPlayer.h +++ b/client/media/CEmptyVideoPlayer.h @@ -19,7 +19,7 @@ public: } /// Load video from specified path - std::unique_ptr open(const VideoPath & name, const Point & scale) override + std::unique_ptr open(const VideoPath & name, float scaleFactor) override { return nullptr; }; diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 45c1c1bdf..3ee1cca55 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -173,18 +173,10 @@ void CVideoInstance::openVideo() openCodec(findVideoStream()); } -void CVideoInstance::prepareOutput(Point scale, bool useTextureOutput) +void CVideoInstance::prepareOutput(float scaleFactor, bool useTextureOutput) { //setup scaling - if(scale.x > 0 && scale.y > 0) - { - dimensions.x = scale.x * GH.screenHandler().getScalingFactor(); - dimensions.y = scale.y * GH.screenHandler().getScalingFactor(); - } - else - { - dimensions = Point(getCodecContext()->width, getCodecContext()->height) * GH.screenHandler().getScalingFactor(); - } + dimensions = Point(getCodecContext()->width * scaleFactor, getCodecContext()->height * scaleFactor) * GH.screenHandler().getScalingFactor(); // Allocate a place to put our YUV image on that screen if (useTextureOutput) @@ -352,10 +344,7 @@ FFMpegStream::~FFMpegStream() Point CVideoInstance::size() { - if(!getCurrentFrame()) - throw std::runtime_error("Invalid video frame!"); - - return Point(getCurrentFrame()->width, getCurrentFrame()->height); + return dimensions; } void CVideoInstance::show(const Point & position, Canvas & canvas) @@ -587,7 +576,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po return true; instance.openVideo(); - instance.prepareOutput(Point(0, 0), true); + instance.prepareOutput(1, true); auto lastTimePoint = boost::chrono::steady_clock::now(); @@ -638,7 +627,7 @@ void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & openAndPlayVideoImpl(name, position * GH.screenHandler().getScalingFactor(), false, false); } -std::unique_ptr CVideoPlayer::open(const VideoPath & name, const Point & scale) +std::unique_ptr CVideoPlayer::open(const VideoPath & name, float scaleFactor) { auto result = std::make_unique(); @@ -646,7 +635,7 @@ std::unique_ptr CVideoPlayer::open(const VideoPath & name, const return nullptr; result->openVideo(); - result->prepareOutput(scale, false); + result->prepareOutput(scaleFactor, false); result->loadNextFrame(); // prepare 1st frame return result; diff --git a/client/media/CVideoHandler.h b/client/media/CVideoHandler.h index 11bfa844a..d40582b62 100644 --- a/client/media/CVideoHandler.h +++ b/client/media/CVideoHandler.h @@ -80,7 +80,7 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream /// video playback current progress, in seconds double frameTime = 0.0; - void prepareOutput(Point scale, bool useTextureOutput); + void prepareOutput(float scaleFactor, bool useTextureOutput); public: ~CVideoInstance(); @@ -102,7 +102,7 @@ class CVideoPlayer final : public IVideoPlayer public: void playSpellbookAnimation(const VideoPath & name, const Point & position) final; - std::unique_ptr open(const VideoPath & name, const Point & scale) final; + std::unique_ptr open(const VideoPath & name, float scaleFactor) final; std::pair, si64> getAudio(const VideoPath & videoToOpen) final; }; diff --git a/client/media/IVideoPlayer.h b/client/media/IVideoPlayer.h index 4c4c3b5d3..2c979a088 100644 --- a/client/media/IVideoPlayer.h +++ b/client/media/IVideoPlayer.h @@ -42,7 +42,7 @@ public: virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) = 0; /// Load video from specified path. Returns nullptr on failure - virtual std::unique_ptr open(const VideoPath & name, const Point & scale) = 0; + virtual std::unique_ptr open(const VideoPath & name, float scaleFactor) = 0; /// Extracts audio data from provided video in wav format virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) = 0; diff --git a/client/widgets/VideoWidget.cpp b/client/widgets/VideoWidget.cpp index a6281de6e..0fa6570cf 100644 --- a/client/widgets/VideoWidget.cpp +++ b/client/widgets/VideoWidget.cpp @@ -17,7 +17,12 @@ #include "../render/Canvas.h" VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio) - : playAudio(playAudio) + : VideoWidgetBase(position, video, playAudio, 1.0) +{ +} + +VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor) + : playAudio(playAudio), scaleFactor(scaleFactor) { addUsedEvents(TIME); pos += position; @@ -28,7 +33,7 @@ VideoWidgetBase::~VideoWidgetBase() = default; void VideoWidgetBase::playVideo(const VideoPath & fileToPlay) { - videoInstance = CCS->videoh->open(fileToPlay, Point(0, 0)); + videoInstance = CCS->videoh->open(fileToPlay, scaleFactor); if (videoInstance) { pos.w = videoInstance->size().x; @@ -119,11 +124,6 @@ void VideoWidgetBase::tick(uint32_t msPassed) } } -Point VideoWidgetBase::size() -{ - return videoInstance->size(); -} - VideoWidget::VideoWidget(const Point & position, const VideoPath & prologue, const VideoPath & looped, bool playAudio) : VideoWidgetBase(position, prologue, playAudio) , loopedVideo(looped) @@ -147,6 +147,12 @@ VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video { } +VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, const std::function & callback) + : VideoWidgetBase(position, video, playAudio, scaleFactor) + , callback(callback) +{ +} + void VideoWidgetOnce::onPlaybackFinished() { callback(); diff --git a/client/widgets/VideoWidget.h b/client/widgets/VideoWidget.h index 22bf10457..b0264d58d 100644 --- a/client/widgets/VideoWidget.h +++ b/client/widgets/VideoWidget.h @@ -22,6 +22,7 @@ class VideoWidgetBase : public CIntObject std::pair, si64> audioData = {nullptr, 0}; int audioHandle = -1; bool playAudio = false; + float scaleFactor = 1.0; void loadAudio(const VideoPath & file); void startAudio(); @@ -29,6 +30,7 @@ class VideoWidgetBase : public CIntObject protected: VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio); + VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor); virtual void onPlaybackFinished() = 0; void playVideo(const VideoPath & video); @@ -42,8 +44,6 @@ public: void showAll(Canvas & to) override; void tick(uint32_t msPassed) override; - Point size(); - void setPlaybackFinishedCallback(std::function); }; @@ -64,4 +64,5 @@ class VideoWidgetOnce final: public VideoWidgetBase void onPlaybackFinished() final; public: VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function & callback); + VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, const std::function & callback); }; From 2fbdeb3a5d4022d747b2659b2b1626357a97ae10 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:23:00 +0200 Subject: [PATCH 08/20] skippable video intro --- client/CServerHandler.cpp | 2 +- client/NetPacksLobbyClient.cpp | 2 +- client/lobby/CBonusSelection.cpp | 16 +++++------ client/lobby/CBonusSelection.h | 6 ++-- client/mainmenu/CMainMenu.cpp | 48 ++++++++++++++++++++------------ client/mainmenu/CMainMenu.h | 2 ++ client/windows/CWindowObject.cpp | 2 +- 7 files changed, 46 insertions(+), 32 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 31e891cba..83ce6b70e 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -711,7 +711,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), 1)) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, 1, [campaignScoreCalculator, statistic](){ + GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, 1, [campaignScoreCalculator, statistic](bool skipped){ GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); }); } diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 5c7822d97..88b84b420 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -210,7 +210,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), 1)) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, 1, [bonusSel](){ + GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, 1, [bonusSel](bool skipped){ if(!CSH->si->campState->getMusic().empty()) CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false); GH.windows().pushWindow(bonusSel); diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index b6c5afc87..d293c1f3e 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -59,8 +59,8 @@ #include "../../lib/mapObjects/CGHeroInstance.h" -CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb) - : CWindowObject(BORDERED), closeCb(closeCb) +CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb) + : CWindowObject(BORDERED | NEEDS_ANIMATED_BACKGROUND), closeCb(closeCb) { OBJECT_CONSTRUCTION; @@ -68,12 +68,12 @@ CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBack if(!rim.empty()) { - videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(); }); + videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(false); }); pos = center(Rect(0, 0, 800, 600)); } else { - videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, [this](){ exit(); }); + videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, [this](){ exit(false); }); pos = center(Rect(0, 0, videoPlayer->pos.w, videoPlayer->pos.h)); } @@ -84,21 +84,21 @@ CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBack setBackground(rim); } -void CampaignRimVideo::exit() +void CampaignRimVideo::exit(bool skipped) { close(); if(closeCb) - closeCb(); + closeCb(skipped); } void CampaignRimVideo::clickPressed(const Point & cursorPosition) { - exit(); + exit(true); } void CampaignRimVideo::keyPressed(EShortcut key) { - exit(); + exit(true); } bool CampaignRimVideo::receiveEvent(const Point & position, int eventType) const diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 13223f321..9b29eba94 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -39,11 +39,11 @@ class CampaignRimVideo : public CWindowObject std::shared_ptr videoPlayer; std::shared_ptr backgroundAroundWindow; - std::function closeCb; + std::function closeCb; - void exit(); + void exit(bool skipped); public: - CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb); + CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb); void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 835157d59..17d2ac9d3 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -293,24 +293,7 @@ CMainMenu::CMainMenu(bool playVideoIntro) OBJECT_CONSTRUCTION; backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); - if(playVideoIntro) - { - auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function cb){ - if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor)) - GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](){ cb(); }); - else - cb(); - }; - playVideo("3DOLOGO.SMK", false, 1, [playVideo](){ - playVideo("NWCLOGO.SMK", false, 2, [playVideo](){ - playVideo("H3INTRO.SMK", true, 1, [](){ - CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); - }); - }); - }); - } - else - CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); + playIntroVideos(playVideoIntro); } CMainMenu::~CMainMenu() @@ -319,6 +302,35 @@ CMainMenu::~CMainMenu() GH.curInt = nullptr; } +void CMainMenu::playIntroVideos(bool playVideoIntro) +{ + auto playMusic = [](){ CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); }; + if(playVideoIntro) + { + auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function cb){ + if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor)) + GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](bool skipped){ cb(skipped); }); + else + cb(true); + }; + playVideo("3DOLOGO.SMK", false, 1, [playVideo, playMusic](bool skipped){ + if(!skipped) + playVideo("NWCLOGO.SMK", false, 2, [playVideo, playMusic](bool skipped){ + if(!skipped) + playVideo("H3INTRO.SMK", true, 1, [playMusic](bool skipped){ + playMusic(); + }); + else + playMusic(); + }); + else + playMusic(); + }); + } + else + playMusic(); +} + void CMainMenu::activate() { // check if screen was resized while main menu was inactive - e.g. in gameplay mode diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 1b4b263ec..599efad14 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -146,6 +146,8 @@ class CMainMenu : public CIntObject, public IUpdateable, public std::enable_shar CMainMenu(bool playVideoIntro); //Use CMainMenu::create + void playIntroVideos(bool playVideoIntro); + public: std::shared_ptr menu; diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index 282dec7a9..2b88f6064 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -59,7 +59,7 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName): background(createBg(imageName, options_ & PLAYER_COLORED)) { if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) - //assert(parent == nullptr); //Safe to remove, but windows should not have parent + assert(parent == nullptr); //Safe to remove, but windows should not have parent if(options & RCLICK_POPUP) CCS->curh->hide(); From 21c3cf09606d225ddee6f3f96133d6914413af5a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:35:21 +0200 Subject: [PATCH 09/20] rename and move window class --- client/CServerHandler.cpp | 3 +- client/NetPacksLobbyClient.cpp | 3 +- client/lobby/CBonusSelection.cpp | 48 -------------------------------- client/lobby/CBonusSelection.h | 16 ----------- client/mainmenu/CMainMenu.cpp | 4 +-- client/windows/GUIClasses.cpp | 47 +++++++++++++++++++++++++++++++ client/windows/GUIClasses.h | 16 +++++++++++ 7 files changed, 69 insertions(+), 68 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 83ce6b70e..fa0c42619 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -23,6 +23,7 @@ #include "lobby/CLobbyScreen.h" #include "lobby/CBonusSelection.h" #include "windows/InfoWindows.h" +#include "windows/GUIClasses.h" #include "media/CMusicHandler.h" #include "media/IVideoPlayer.h" @@ -711,7 +712,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), 1)) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, 1, [campaignScoreCalculator, statistic](bool skipped){ + GH.windows().createAndPushWindow(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, 1, [campaignScoreCalculator, statistic](bool skipped){ GH.windows().createAndPushWindow(true, *campaignScoreCalculator, statistic); }); } diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 88b84b420..4689d8d57 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -33,6 +33,7 @@ #include "widgets/TextControls.h" #include "media/CMusicHandler.h" #include "media/IVideoPlayer.h" +#include "windows/GUIClasses.h" #include "../lib/CConfigHandler.h" #include "../lib/texts/CGeneralTextHandler.h" @@ -210,7 +211,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), 1)) { CCS->musich->stopMusic(); - GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, 1, [bonusSel](bool skipped){ + GH.windows().createAndPushWindow(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, 1, [bonusSel](bool skipped){ if(!CSH->si->campState->getMusic().empty()) CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false); GH.windows().pushWindow(bonusSel); diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index d293c1f3e..b4dcc7f8e 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -58,54 +58,6 @@ #include "../../lib/mapObjects/CGHeroInstance.h" - -CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb) - : CWindowObject(BORDERED | NEEDS_ANIMATED_BACKGROUND), closeCb(closeCb) -{ - OBJECT_CONSTRUCTION; - - addUsedEvents(LCLICK | KEYBOARD); - - if(!rim.empty()) - { - videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(false); }); - pos = center(Rect(0, 0, 800, 600)); - } - else - { - videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, [this](){ exit(false); }); - pos = center(Rect(0, 0, videoPlayer->pos.w, videoPlayer->pos.h)); - } - - if(showBackground) - backgroundAroundWindow = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y)); - - if(!rim.empty()) - setBackground(rim); -} - -void CampaignRimVideo::exit(bool skipped) -{ - close(); - if(closeCb) - closeCb(skipped); -} - -void CampaignRimVideo::clickPressed(const Point & cursorPosition) -{ - exit(true); -} - -void CampaignRimVideo::keyPressed(EShortcut key) -{ - exit(true); -} - -bool CampaignRimVideo::receiveEvent(const Point & position, int eventType) const -{ - return true; // capture click also outside of window -} - std::shared_ptr CBonusSelection::getCampaign() { return CSH->si->campState; diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index 9b29eba94..eaed973db 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -34,22 +34,6 @@ class CBonusSelection; class CFilledTexture; -class CampaignRimVideo : public CWindowObject -{ - std::shared_ptr videoPlayer; - std::shared_ptr backgroundAroundWindow; - - std::function closeCb; - - void exit(bool skipped); -public: - CampaignRimVideo(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb); - - void clickPressed(const Point & cursorPosition) override; - void keyPressed(EShortcut key) override; - bool receiveEvent(const Point & position, int eventType) const override; -}; - /// Campaign screen where you can choose one out of three starting bonuses class CBonusSelection : public CWindowObject { diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 17d2ac9d3..94702b41f 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -309,11 +309,11 @@ void CMainMenu::playIntroVideos(bool playVideoIntro) { auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function cb){ if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor)) - GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](bool skipped){ cb(skipped); }); + GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](bool skipped){ cb(skipped); }); else cb(true); }; - playVideo("3DOLOGO.SMK", false, 1, [playVideo, playMusic](bool skipped){ + playVideo("3DOLOGO.SMK", false, 1.25, [playVideo, playMusic](bool skipped){ if(!skipped) playVideo("NWCLOGO.SMK", false, 2, [playVideo, playMusic](bool skipped){ if(!skipped) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 8c2bd87d2..857cd8415 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1607,3 +1607,50 @@ void CObjectListWindow::keyPressed(EShortcut key) list->scrollTo(sel); changeSelection(sel); } + +VideoWindow::VideoWindow(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb) + : CWindowObject(BORDERED | NEEDS_ANIMATED_BACKGROUND), closeCb(closeCb) +{ + OBJECT_CONSTRUCTION; + + addUsedEvents(LCLICK | KEYBOARD); + + if(!rim.empty()) + { + videoPlayer = std::make_shared(Point(80, 186), video, true, [this](){ exit(false); }); + pos = center(Rect(0, 0, 800, 600)); + } + else + { + videoPlayer = std::make_shared(Point(0, 0), video, true, scaleFactor, [this](){ exit(false); }); + pos = center(Rect(0, 0, videoPlayer->pos.w, videoPlayer->pos.h)); + } + + if(showBackground) + backgroundAroundWindow = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y)); + + if(!rim.empty()) + setBackground(rim); +} + +void VideoWindow::exit(bool skipped) +{ + close(); + if(closeCb) + closeCb(skipped); +} + +void VideoWindow::clickPressed(const Point & cursorPosition) +{ + exit(true); +} + +void VideoWindow::keyPressed(EShortcut key) +{ + exit(true); +} + +bool VideoWindow::receiveEvent(const Point & position, int eventType) const +{ + return true; // capture click also outside of window +} diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 20d9f55e5..e9ccb89b2 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -43,6 +43,7 @@ class CAnimImage; class CFilledTexture; class IImage; class VideoWidget; +class VideoWidgetOnce; enum class EUserEvent; @@ -501,3 +502,18 @@ public: CThievesGuildWindow(const CGObjectInstance * _owner); }; +class VideoWindow : public CWindowObject +{ + std::shared_ptr videoPlayer; + std::shared_ptr backgroundAroundWindow; + + std::function closeCb; + + void exit(bool skipped); +public: + VideoWindow(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb); + + void clickPressed(const Point & cursorPosition) override; + void keyPressed(EShortcut key) override; + bool receiveEvent(const Point & position, int eventType) const override; +}; From 16ae854f17a623aeae46061e89bf915a6b5de557 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:49:57 +0200 Subject: [PATCH 10/20] fix shadow --- client/windows/GUIClasses.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 857cd8415..b0b5e9351 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1631,6 +1631,8 @@ VideoWindow::VideoWindow(VideoPath video, ImagePath rim, bool showBackground, fl if(!rim.empty()) setBackground(rim); + + updateShadow(); } void VideoWindow::exit(bool skipped) From e963550431f4f6defa502402eaf6163027af3d23 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Sep 2024 00:03:25 +0200 Subject: [PATCH 11/20] fix and no shadow --- client/lobby/CBonusSelection.h | 1 - client/media/CVideoHandler.cpp | 2 +- client/windows/GUIClasses.cpp | 4 +--- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/client/lobby/CBonusSelection.h b/client/lobby/CBonusSelection.h index eaed973db..b9b341c60 100644 --- a/client/lobby/CBonusSelection.h +++ b/client/lobby/CBonusSelection.h @@ -31,7 +31,6 @@ class ISelectionScreenInfo; class ExtraOptionsTab; class VideoWidgetOnce; class CBonusSelection; -class CFilledTexture; /// Campaign screen where you can choose one out of three starting bonuses diff --git a/client/media/CVideoHandler.cpp b/client/media/CVideoHandler.cpp index 3ee1cca55..51e17ff10 100644 --- a/client/media/CVideoHandler.cpp +++ b/client/media/CVideoHandler.cpp @@ -344,7 +344,7 @@ FFMpegStream::~FFMpegStream() Point CVideoInstance::size() { - return dimensions; + return dimensions / GH.screenHandler().getScalingFactor(); } void CVideoInstance::show(const Point & position, Canvas & canvas) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index b0b5e9351..6353a1595 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1609,7 +1609,7 @@ void CObjectListWindow::keyPressed(EShortcut key) } VideoWindow::VideoWindow(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function closeCb) - : CWindowObject(BORDERED | NEEDS_ANIMATED_BACKGROUND), closeCb(closeCb) + : CWindowObject(BORDERED | SHADOW_DISABLED | NEEDS_ANIMATED_BACKGROUND), closeCb(closeCb) { OBJECT_CONSTRUCTION; @@ -1631,8 +1631,6 @@ VideoWindow::VideoWindow(VideoPath video, ImagePath rim, bool showBackground, fl if(!rim.empty()) setBackground(rim); - - updateShadow(); } void VideoWindow::exit(bool skipped) From 0aaafc4c8a4a7832464f9e3bd981dabb3e6fe00b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 14 Sep 2024 14:49:52 +0200 Subject: [PATCH 12/20] cast without skip --- config/schemas/spell.json | 4 ++++ docs/modders/Entities_Format/Spell_Format.md | 3 +++ include/vcmi/spells/Spell.h | 1 + lib/battle/CUnitState.cpp | 7 ++++++- lib/battle/CUnitState.h | 1 + lib/networkPacks/NetPacksLib.cpp | 1 + lib/spells/CSpellHandler.cpp | 6 ++++++ lib/spells/CSpellHandler.h | 2 ++ server/battles/BattleFlowProcessor.cpp | 13 +++++++++++++ test/mock/mock_spells_Spell.h | 1 + 10 files changed, 38 insertions(+), 1 deletion(-) diff --git a/config/schemas/spell.json b/config/schemas/spell.json index dff3bc2df..d4b00487b 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -171,6 +171,10 @@ "type" : "boolean", "description" : "If used as creature spell, unit can cast this spell on itself" }, + "canCastWithoutSkip" : { + "type" : "boolean", + "description" : "If used the creature will not skip the turn after casting a spell." + }, "gainChance" : { "type" : "object", "description" : "Chance for this spell to appear in Mage Guild of a specific faction", diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index 5e17e817e..41461b606 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -64,6 +64,9 @@ // If true, then creature capable of casting this spell can cast this spell on itself // If false, then creature can only cast this spell on other units "canCastOnSelf" : false, + + // If true the creature will not skip the turn after casting a spell + "canCastWithoutSkip": false, // If true, spell won't be available on a map without water "onlyOnWaterMap" : true, diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index 1822639e1..6b40bf258 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -45,6 +45,7 @@ public: virtual bool hasSchool(SpellSchool school) const = 0; virtual bool canCastOnSelf() const = 0; + virtual bool canCastWithoutSkip() const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index de2704058..ad3754260 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -330,6 +330,7 @@ CUnitState::CUnitState(): drainedMana(false), fear(false), hadMorale(false), + usedSpell(SpellID::NONE), ghost(false), ghostPending(false), movedThisRound(false), @@ -362,6 +363,7 @@ CUnitState & CUnitState::operator=(const CUnitState & other) drainedMana = other.drainedMana; fear = other.fear; hadMorale = other.hadMorale; + usedSpell = other.usedSpell; ghost = other.ghost; ghostPending = other.ghostPending; movedThisRound = other.movedThisRound; @@ -532,7 +534,7 @@ bool CUnitState::hasClone() const bool CUnitState::canCast() const { - return casts.canUse(1);//do not check specific cast abilities here + return casts.canUse(1) && usedSpell == SpellID::NONE;//do not check specific cast abilities here } bool CUnitState::isCaster() const @@ -748,6 +750,7 @@ void CUnitState::serializeJson(JsonSerializeFormat & handler) handler.serializeBool("drainedMana", drainedMana); handler.serializeBool("fear", fear); handler.serializeBool("hadMorale", hadMorale); + handler.serializeInt("usedSpell", usedSpell); handler.serializeBool("ghost", ghost); handler.serializeBool("ghostPending", ghostPending); handler.serializeBool("moved", movedThisRound); @@ -782,6 +785,7 @@ void CUnitState::reset() drainedMana = false; fear = false; hadMorale = false; + usedSpell = SpellID::NONE; ghost = false; ghostPending = false; movedThisRound = false; @@ -864,6 +868,7 @@ void CUnitState::afterNewRound() waitedThisTurn = false; movedThisRound = false; hadMorale = false; + usedSpell = SpellID::NONE; fear = false; drainedMana = false; counterAttacks.reset(); diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index 02422e2e1..1ae759115 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -141,6 +141,7 @@ public: bool drainedMana; bool fear; bool hadMorale; + SpellID usedSpell; bool ghost; bool ghostPending; bool movedThisRound; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index bccbbef61..6d2431297 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -2209,6 +2209,7 @@ void StartAction::applyGs(CGameState *gs) st->waiting = false; st->defendingAnim = false; st->movedThisRound = true; + st->usedSpell = ba.actionType == EActionType::MONSTER_SPELL ? ba.spell : SpellID::NONE; break; } } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 05d639f62..4e3db1a5c 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -298,6 +298,11 @@ bool CSpell::canCastOnSelf() const return castOnSelf; } +bool CSpell::canCastWithoutSkip() const +{ + return castWithoutSkip; +} + const std::string & CSpell::getIconImmune() const { return iconImmune; @@ -779,6 +784,7 @@ std::shared_ptr CSpellHandler::loadFromJson(const std::string & scope, c } spell->castOnSelf = json["canCastOnSelf"].Bool(); + spell->castWithoutSkip = json["canCastWithoutSkip"].Bool(); spell->level = static_cast(json["level"].Integer()); spell->power = static_cast(json["power"].Integer()); diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 32b91f3fb..c16e10ceb 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -167,6 +167,7 @@ public: bool hasSchool(SpellSchool school) const override; bool canCastOnSelf() const override; + bool canCastWithoutSkip() const override; /** * Calls cb for each school this spell belongs to @@ -296,6 +297,7 @@ private: bool combat; //is this spell combat (true) or adventure (false) bool creatureAbility; //if true, only creatures can use this spell bool castOnSelf; // if set, creature caster can cast this spell on itself + bool castWithoutSkip; // if set the creature will not skip the turn after casting a spell si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative std::unique_ptr mechanics;//(!) do not serialize diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index c8beebb63..c1b29bdcc 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -565,6 +565,19 @@ void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const if(battle.battleGetTacticDist() != 0) return; + // creature will not skip the turn after casting a spell if spell uses canCastWithoutSkip + if(ba.actionType == EActionType::MONSTER_SPELL) + { + assert(activeStack != nullptr); + assert(actedStack != nullptr); + + if(actedStack->usedSpell != SpellID::NONE && SpellID(actedStack->usedSpell).toSpell()->canCastWithoutSkip()) + { + setActiveStack(battle, actedStack); + return; + } + } + if (ba.isUnitAction()) { assert(activeStack != nullptr); diff --git a/test/mock/mock_spells_Spell.h b/test/mock/mock_spells_Spell.h index de03559eb..2d0ae31d1 100644 --- a/test/mock/mock_spells_Spell.h +++ b/test/mock/mock_spells_Spell.h @@ -47,6 +47,7 @@ public: MOCK_CONST_METHOD0(isSpecial, bool()); MOCK_CONST_METHOD0(isMagical, bool()); MOCK_CONST_METHOD0(canCastOnSelf, bool()); + MOCK_CONST_METHOD0(canCastWithoutSkip, bool()); MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool)); MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &)); MOCK_CONST_METHOD0(getCastSound, const std::string &()); From bda1adbdfd3866ab24a9ed0f4a9ff41f08081062 Mon Sep 17 00:00:00 2001 From: K Date: Wed, 18 Sep 2024 16:41:41 +0200 Subject: [PATCH 13/20] miniscule bonus system cleanup update comments, remove one never used method, make another method private --- lib/bonuses/CBonusSystemNode.cpp | 7 ------- lib/bonuses/CBonusSystemNode.h | 3 +-- lib/bonuses/IBonusBearer.h | 4 +--- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/bonuses/CBonusSystemNode.cpp b/lib/bonuses/CBonusSystemNode.cpp index 2fca57dff..7b944eefb 100644 --- a/lib/bonuses/CBonusSystemNode.cpp +++ b/lib/bonuses/CBonusSystemNode.cpp @@ -625,13 +625,6 @@ void CBonusSystemNode::limitBonuses(const BonusList &allBonuses, BonusList &out) } } -TBonusListPtr CBonusSystemNode::limitBonuses(const BonusList &allBonuses) const -{ - auto ret = std::make_shared(); - limitBonuses(allBonuses, *ret); - return ret; -} - void CBonusSystemNode::treeHasChanged() { treeChanged++; diff --git a/lib/bonuses/CBonusSystemNode.h b/lib/bonuses/CBonusSystemNode.h index e5b320af3..460957f8b 100644 --- a/lib/bonuses/CBonusSystemNode.h +++ b/lib/bonuses/CBonusSystemNode.h @@ -55,6 +55,7 @@ private: void getAllBonusesRec(BonusList &out, const CSelector & selector) const; TConstBonusListPtr getAllBonusesWithoutCaching(const CSelector &selector, const CSelector &limit) const; std::shared_ptr getUpdatedBonus(const std::shared_ptr & b, const TUpdaterPtr & updater) const; + void limitBonuses(const BonusList &allBonuses, BonusList &out) const; //out will bo populed with bonuses that are not limited here void getRedParents(TCNodes &out) const; //retrieves list of red parent nodes (nodes bonuses propagate from) void getRedAncestors(TCNodes &out) const; @@ -84,8 +85,6 @@ public: explicit CBonusSystemNode(ENodeTypes NodeType); virtual ~CBonusSystemNode(); - void limitBonuses(const BonusList &allBonuses, BonusList &out) const; //out will bo populed with bonuses that are not limited here - TBonusListPtr limitBonuses(const BonusList &allBonuses) const; //same as above, returns out by val for convenience TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = "") const override; void getParents(TCNodes &out) const; //retrieves list of parent nodes (nodes to inherit bonuses from), diff --git a/lib/bonuses/IBonusBearer.h b/lib/bonuses/IBonusBearer.h index b37dbfddb..b272edea4 100644 --- a/lib/bonuses/IBonusBearer.h +++ b/lib/bonuses/IBonusBearer.h @@ -17,9 +17,7 @@ class DLL_LINKAGE IBonusBearer { public: //new bonusing node interface - // * selector is predicate that tests if HeroBonus matches our criteria - // * root is node on which call was made (nullptr will be replaced with this) - //interface + // * selector is predicate that tests if Bonus matches our criteria IBonusBearer() = default; virtual ~IBonusBearer() = default; virtual TConstBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const std::string &cachingStr = "") const = 0; From 735af83294bacca1fdbb59482d615a457eaac90d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:24:27 +0200 Subject: [PATCH 14/20] castSpellThisTurn to bool var --- lib/battle/CUnitState.cpp | 12 ++++++------ lib/battle/CUnitState.h | 2 +- lib/networkPacks/NetPacksLib.cpp | 2 +- server/battles/BattleFlowProcessor.cpp | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index ad3754260..131fc2a23 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -330,7 +330,7 @@ CUnitState::CUnitState(): drainedMana(false), fear(false), hadMorale(false), - usedSpell(SpellID::NONE), + castSpellThisTurn(false), ghost(false), ghostPending(false), movedThisRound(false), @@ -363,7 +363,7 @@ CUnitState & CUnitState::operator=(const CUnitState & other) drainedMana = other.drainedMana; fear = other.fear; hadMorale = other.hadMorale; - usedSpell = other.usedSpell; + castSpellThisTurn = other.castSpellThisTurn; ghost = other.ghost; ghostPending = other.ghostPending; movedThisRound = other.movedThisRound; @@ -534,7 +534,7 @@ bool CUnitState::hasClone() const bool CUnitState::canCast() const { - return casts.canUse(1) && usedSpell == SpellID::NONE;//do not check specific cast abilities here + return casts.canUse(1) && !castSpellThisTurn;//do not check specific cast abilities here } bool CUnitState::isCaster() const @@ -750,7 +750,7 @@ void CUnitState::serializeJson(JsonSerializeFormat & handler) handler.serializeBool("drainedMana", drainedMana); handler.serializeBool("fear", fear); handler.serializeBool("hadMorale", hadMorale); - handler.serializeInt("usedSpell", usedSpell); + handler.serializeBool("castSpellThisTurn", castSpellThisTurn); handler.serializeBool("ghost", ghost); handler.serializeBool("ghostPending", ghostPending); handler.serializeBool("moved", movedThisRound); @@ -785,7 +785,7 @@ void CUnitState::reset() drainedMana = false; fear = false; hadMorale = false; - usedSpell = SpellID::NONE; + castSpellThisTurn = false; ghost = false; ghostPending = false; movedThisRound = false; @@ -868,7 +868,7 @@ void CUnitState::afterNewRound() waitedThisTurn = false; movedThisRound = false; hadMorale = false; - usedSpell = SpellID::NONE; + castSpellThisTurn = false; fear = false; drainedMana = false; counterAttacks.reset(); diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index 1ae759115..b5451ba89 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -141,7 +141,7 @@ public: bool drainedMana; bool fear; bool hadMorale; - SpellID usedSpell; + bool castSpellThisTurn; bool ghost; bool ghostPending; bool movedThisRound; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 6d2431297..43309a8c5 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -2209,7 +2209,7 @@ void StartAction::applyGs(CGameState *gs) st->waiting = false; st->defendingAnim = false; st->movedThisRound = true; - st->usedSpell = ba.actionType == EActionType::MONSTER_SPELL ? ba.spell : SpellID::NONE; + st->castSpellThisTurn = ba.actionType == EActionType::MONSTER_SPELL; break; } } diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index c1b29bdcc..ce70d21fd 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -571,7 +571,7 @@ void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const assert(activeStack != nullptr); assert(actedStack != nullptr); - if(actedStack->usedSpell != SpellID::NONE && SpellID(actedStack->usedSpell).toSpell()->canCastWithoutSkip()) + if(actedStack->castSpellThisTurn && SpellID(ba.spell).toSpell()->canCastWithoutSkip()) { setActiveStack(battle, actedStack); return; From a43475042fd371e2e44aa9661757bf0bbcede21e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 18 Sep 2024 22:10:25 +0200 Subject: [PATCH 15/20] code review --- client/CServerHandler.cpp | 5 ++- client/mainmenu/CHighScoreScreen.cpp | 2 + client/mainmenu/CMainMenu.cpp | 58 ++++++++++++++-------------- client/mainmenu/CMainMenu.h | 8 ++-- clientapp/EntryPoint.cpp | 9 ++++- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index fa0c42619..373bbec3e 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -662,10 +662,13 @@ void CServerHandler::endGameplay() { GH.curInt = CMM.get(); CMM->enable(); + CMM->playMusic(); } else { - GH.curInt = CMainMenu::create(false).get(); + auto mainMenu = CMainMenu::create(); + GH.curInt = mainMenu.get(); + mainMenu->playMusic(); } } diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 6676dcb1c..953f2ce2f 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -12,6 +12,7 @@ #include "CHighScoreScreen.h" #include "CStatisticScreen.h" +#include "CMainMenu.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../gui/Shortcut.h" @@ -170,6 +171,7 @@ void CHighScoreScreen::buttonResetClick() void CHighScoreScreen::buttonExitClick() { close(); + CMM->playMusic(); } CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 94702b41f..d19275185 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -284,7 +284,7 @@ const JsonNode & CMainMenuConfig::getCampaigns() const return campaignSets; } -CMainMenu::CMainMenu(bool playVideoIntro) +CMainMenu::CMainMenu() { pos.w = GH.screenDimensions().x; pos.h = GH.screenDimensions().y; @@ -292,8 +292,6 @@ CMainMenu::CMainMenu(bool playVideoIntro) menu = std::make_shared(CMainMenuConfig::get().getConfig()["window"]); OBJECT_CONSTRUCTION; backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); - - playIntroVideos(playVideoIntro); } CMainMenu::~CMainMenu() @@ -302,33 +300,33 @@ CMainMenu::~CMainMenu() GH.curInt = nullptr; } -void CMainMenu::playIntroVideos(bool playVideoIntro) +void CMainMenu::playIntroVideos() { - auto playMusic = [](){ CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); }; - if(playVideoIntro) - { - auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function cb){ - if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor)) - GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](bool skipped){ cb(skipped); }); - else - cb(true); - }; - playVideo("3DOLOGO.SMK", false, 1.25, [playVideo, playMusic](bool skipped){ - if(!skipped) - playVideo("NWCLOGO.SMK", false, 2, [playVideo, playMusic](bool skipped){ - if(!skipped) - playVideo("H3INTRO.SMK", true, 1, [playMusic](bool skipped){ - playMusic(); - }); - else + auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function cb){ + if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor)) + GH.windows().createAndPushWindow(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](bool skipped){ cb(skipped); }); + else + cb(true); + }; + + playVideo("3DOLOGO.SMK", false, 1.25, [playVideo, this](bool skipped){ + if(!skipped) + playVideo("NWCLOGO.SMK", false, 2, [playVideo, this](bool skipped){ + if(!skipped) + playVideo("H3INTRO.SMK", true, 1, [this](bool skipped){ playMusic(); - }); - else - playMusic(); - }); - } - else - playMusic(); + }); + else + playMusic(); + }); + else + playMusic(); + }); +} + +void CMainMenu::playMusic() +{ + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); } void CMainMenu::activate() @@ -449,10 +447,10 @@ void CMainMenu::openHighScoreScreen() return; } -std::shared_ptr CMainMenu::create(bool playVideoIntro) +std::shared_ptr CMainMenu::create() { if(!CMM) - CMM = std::shared_ptr(new CMainMenu(playVideoIntro)); + CMM = std::shared_ptr(new CMainMenu()); return CMM; } diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 599efad14..82b684a43 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -144,9 +144,7 @@ class CMainMenu : public CIntObject, public IUpdateable, public std::enable_shar std::vector videoPlayList; - CMainMenu(bool playVideoIntro); //Use CMainMenu::create - - void playIntroVideos(bool playVideoIntro); + CMainMenu(); //Use CMainMenu::create public: std::shared_ptr menu; @@ -162,10 +160,12 @@ public: static void openHighScoreScreen(); void openCampaignScreen(std::string name); - static std::shared_ptr create(bool playVideoIntro); + static std::shared_ptr create(); static std::shared_ptr createPicture(const JsonNode & config); + void playIntroVideos(); + void playMusic(); }; /// Simple window to enter the server's address. diff --git a/clientapp/EntryPoint.cpp b/clientapp/EntryPoint.cpp index 0da0ff24f..8d0ffa278 100644 --- a/clientapp/EntryPoint.cpp +++ b/clientapp/EntryPoint.cpp @@ -372,9 +372,14 @@ int main(int argc, char * argv[]) } else { - bool playIntroVideo = !settings["session"]["headless"].Bool() && !vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool(); - auto mmenu = CMainMenu::create(playIntroVideo); + auto mmenu = CMainMenu::create(); GH.curInt = mmenu.get(); + + bool playIntroVideo = !settings["session"]["headless"].Bool() && !vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool(); + if(playIntroVideo) + mmenu->playIntroVideos(); + else + mmenu->playMusic(); } std::vector names; From 6b4220c510bdaf61d4f7388d05880141173085b1 Mon Sep 17 00:00:00 2001 From: MichalZr6 Date: Wed, 4 Sep 2024 00:13:00 +0200 Subject: [PATCH 16/20] Add update paths code to netpack visiting methods --- client/NetPacksClient.cpp | 98 ++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index aadcdcc54..afd8f96f5 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -25,6 +25,7 @@ #include "CMT.h" #include "GameChatHandler.h" #include "CServerHandler.h" +#include "PlayerLocalState.h" #include "../CCallback.h" #include "../lib/filesystem/Filesystem.h" @@ -101,7 +102,7 @@ void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & bat { assert(cl.gameState()->getBattle(battleID)); - if (!cl.gameState()->getBattle(battleID)) + if(!cl.gameState()->getBattle(battleID)) { logGlobal->error("Attempt to call battle interface without ongoing battle!"); return; @@ -161,7 +162,7 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) if(settings["session"]["headless"].Bool()) return; - for (auto window : GH.windows().findWindows()) + for(auto window : GH.windows().findWindows()) window->heroManaPointsChanged(h); } @@ -230,6 +231,15 @@ void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack) { dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); cl.invalidatePaths(); //it is possible to remove last non-native unit for current terrain and lose movement penalty + + if(LOCPLINT) + { + for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if(hero->id == pack.army) + LOCPLINT->localState->verifyPath(hero); + } + } } void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) @@ -237,15 +247,32 @@ void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); if(pack.srcArmy != pack.dstArmy) + { cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + if(LOCPLINT) + { + for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if(hero->id == pack.dstArmy) + LOCPLINT->localState->verifyPath(hero); + } + } + } } void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack) { dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); - if(gs.getHero(pack.army)) + auto hero = gs.getHero(pack.army); + if(hero) + { cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + if(LOCPLINT) + { + LOCPLINT->localState->verifyPath(hero); + } + } } void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) @@ -253,7 +280,17 @@ void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); if(pack.srcArmy != pack.dstArmy) + { cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + if(LOCPLINT) + { + for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if(hero->id == pack.dstArmy || hero->id == pack.srcArmy) + LOCPLINT->localState->verifyPath(hero); + } + } + } } void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack) @@ -266,7 +303,18 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy); if(pack.moves[0].srcArmy != destArmy) + { cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains + if(LOCPLINT) + { + // update all paths + for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if(hero->id == destArmy || hero->id == pack.moves[0].srcArmy) + LOCPLINT->localState->verifyPath(hero); + } + } + } } } @@ -313,6 +361,15 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::artifactMoved, srcLoc, dstLoc); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings + + if(LOCPLINT) + { + for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if(hero->id == pack.srcArtHolder || hero->id == pack.dstArtHolder) + LOCPLINT->localState->verifyPath(hero); + } + } } }; @@ -343,6 +400,15 @@ void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings + if(LOCPLINT) + { + for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if(hero->id == pack.al.artHolder) + LOCPLINT->localState->verifyPath(hero); + } + } + } void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) @@ -350,6 +416,14 @@ void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings + if(LOCPLINT) + { + for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if(hero->id == pack.al.artHolder) + LOCPLINT->localState->verifyPath(hero); + } + } } void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) @@ -363,7 +437,7 @@ void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack) { cl.invalidatePaths(); - if (pack.newWeekNotification) + if(pack.newWeekNotification) { const auto & newWeek = *pack.newWeekNotification; @@ -380,7 +454,7 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) case GiveBonus::ETarget::OBJECT: { const CGHeroInstance *h = gs.getHero(pack.id.as()); - if (h) + if(h) callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); } break; @@ -419,7 +493,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) bool lastHumanEndsGame = CSH->howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool(); - if (lastHumanEndsGame) + if(lastHumanEndsGame) { assert(adventureInt); if(adventureInt) @@ -446,9 +520,9 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface { cl.initPlayerInterfaces(); - for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) { - if (cl.gameState()->isPlayerMakingTurn(player)) + if(cl.gameState()->isPlayerMakingTurn(player)) { callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE); @@ -482,7 +556,7 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) case GiveBonus::ETarget::OBJECT: { const CGHeroInstance *h = gs.getHero(pack.whoID.as()); - if (h) + if(h) callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); } break; @@ -701,7 +775,7 @@ void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & } // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER && CGI->mh) + if(pack.what == ObjProperty::OWNER && CGI->mh) { auto object = gs.getObjInstance(pack.id); CGI->mh->onObjectInstantRemove(object, object->getOwner()); @@ -718,7 +792,7 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) } // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER && CGI->mh) + if(pack.what == ObjProperty::OWNER && CGI->mh) { auto object = gs.getObjInstance(pack.id); CGI->mh->onObjectInstantAdd(object, object->getOwner()); @@ -807,7 +881,7 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); PlayerColor playerToCall; //pack.player that will move activated stack - if (activated->hasBonusOfType(BonusType::HYPNOTIZED)) + if(activated->hasBonusOfType(BonusType::HYPNOTIZED)) { playerToCall = gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color == activated->unitOwner() ? gs.getBattle(pack.battleID)->getSide(BattleSide::DEFENDER).color From e13e72d093fe604d2ecc9fb7462f5bcc313fae85 Mon Sep 17 00:00:00 2001 From: MichalZr6 Date: Thu, 19 Sep 2024 08:34:53 +0200 Subject: [PATCH 17/20] Moved code from NetpacksClient.cpp to Client.cpp Created new function CClient::updatePath that will invalidate paths and update given hero displayed path --- client/Client.cpp | 1174 +++++++++++++++++++------------------ client/Client.h | 4 +- client/NetPacksClient.cpp | 92 +-- 3 files changed, 609 insertions(+), 661 deletions(-) diff --git a/client/Client.cpp b/client/Client.cpp index 43c708ea7..f058e7da3 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -1,580 +1,594 @@ -/* - * Client.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 "Global.h" -#include "Client.h" - -#include "CGameInfo.h" -#include "CPlayerInterface.h" -#include "CServerHandler.h" -#include "ClientNetPackVisitors.h" -#include "adventureMap/AdventureMapInterface.h" -#include "battle/BattleInterface.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "mapView/mapHandler.h" - -#include "../CCallback.h" -#include "../lib/CConfigHandler.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CPlayerState.h" -#include "../lib/CThreadHelper.h" -#include "../lib/VCMIDirs.h" -#include "../lib/UnlockGuard.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/serializer/Connection.h" -#include "../lib/mapping/CMapService.h" -#include "../lib/pathfinder/CGPathNode.h" -#include "../lib/filesystem/Filesystem.h" - -#include -#include - -#if SCRIPTING_ENABLED -#include "../lib/ScriptHandler.h" -#endif - -#ifdef VCMI_ANDROID -#include "lib/CAndroidVMHelper.h" -#endif - -ThreadSafeVector CClient::waitingRequest; - -CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_) - : player(player_), - cl(cl_), - mainCallback(mainCallback_) -{ - -} - -const Services * CPlayerEnvironment::services() const -{ - return VLC; -} - -vstd::CLoggerBase * CPlayerEnvironment::logger() const -{ - return logGlobal; -} - -events::EventBus * CPlayerEnvironment::eventBus() const -{ - return cl->eventBus();//always get actual value -} - -const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const -{ - return mainCallback->getBattle(battleID).get(); -} - -const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const -{ - return mainCallback.get(); -} - -CClient::CClient() -{ - waitingRequest.clear(); - gs = nullptr; -} - -CClient::~CClient() = default; - -const Services * CClient::services() const -{ - return VLC; //todo: this should be CGI -} - -const CClient::BattleCb * CClient::battle(const BattleID & battleID) const -{ - return nullptr; //todo? -} - -const CClient::GameCb * CClient::game() const -{ - return this; -} - -vstd::CLoggerBase * CClient::logger() const -{ - return logGlobal; -} - -events::EventBus * CClient::eventBus() const -{ - return clientEventBus.get(); -} - -void CClient::newGame(CGameState * initializedGameState) -{ - CSH->th->update(); - CMapService mapService; - assert(initializedGameState); - gs = initializedGameState; - gs->preInit(VLC, this); - logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); - if(!initializedGameState) - { - Load::ProgressAccumulator progressTracking; - gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); - } - logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); - - initMapHandler(); - reinitScripting(); - initPlayerEnvironments(); - initPlayerInterfaces(); -} - -void CClient::loadGame(CGameState * initializedGameState) -{ - logNetwork->info("Loading procedure started!"); - - logNetwork->info("Game state was transferred over network, loading."); - gs = initializedGameState; - - gs->preInit(VLC, this); - gs->updateOnLoad(CSH->si.get()); - logNetwork->info("Game loaded, initialize interfaces."); - - initMapHandler(); - - reinitScripting(); - - initPlayerEnvironments(); - initPlayerInterfaces(); -} - -void CClient::save(const std::string & fname) -{ - if(!gs->currentBattles.empty()) - { - logNetwork->error("Game cannot be saved during battle!"); - return; - } - - SaveGame save_game(fname); - sendRequest(&save_game, PlayerColor::NEUTRAL); -} - -void CClient::endNetwork() -{ - if (CGI->mh) - CGI->mh->endNetwork(); - - if (CPlayerInterface::battleInt) - CPlayerInterface::battleInt->endNetwork(); - - for(auto & i : playerint) - { - auto interface = std::dynamic_pointer_cast(i.second); - if (interface) - interface->endNetwork(); - } -} - -void CClient::endGame() -{ -#if SCRIPTING_ENABLED - clientScripts.reset(); -#endif - - //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) - for(auto & i : playerint) - i.second->finish(); - - GH.curInt = nullptr; - { - logNetwork->info("Ending current game!"); - removeGUI(); - - CGI->mh.reset(); - vstd::clear_pointer(gs); - - logNetwork->info("Deleted mapHandler and gameState."); - } - - CPlayerInterface::battleInt.reset(); - playerint.clear(); - battleints.clear(); - battleCallbacks.clear(); - playerEnvironments.clear(); - logNetwork->info("Deleted playerInts."); - logNetwork->info("Client stopped."); -} - -void CClient::initMapHandler() -{ - // TODO: CMapHandler initialization can probably go somewhere else - // It's can't be before initialization of interfaces - // During loading CPlayerInterface from serialized state it's depend on MH - if(!settings["session"]["headless"].Bool()) - { - CGI->mh = std::make_shared(gs->map); - logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); - } - - pathCache.clear(); -} - -void CClient::initPlayerEnvironments() -{ - playerEnvironments.clear(); - - auto allPlayers = CSH->getAllClientPlayers(CSH->logicConnection->connectionID); - bool hasHumanPlayer = false; - for(auto & color : allPlayers) - { - logNetwork->info("Preparing environment for player %s", color.toString()); - playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); - - if(!hasHumanPlayer && gs->players[color].isHuman()) - hasHumanPlayer = true; - } - - if(!hasHumanPlayer && !settings["session"]["headless"].Bool()) - { - Settings session = settings.write["session"]; - session["spectate"].Bool() = true; - session["spectate-skip-battle-result"].Bool() = true; - session["spectate-ignore-hero"].Bool() = true; - } - - if(settings["session"]["spectate"].Bool()) - { - playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); - } -} - -void CClient::initPlayerInterfaces() -{ - for(auto & playerInfo : gs->scenarioOps->playerInfos) - { - PlayerColor color = playerInfo.first; - if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), color)) - continue; - - if(!vstd::contains(playerint, color)) - { - logNetwork->info("Preparing interface for player %s", color.toString()); - if(playerInfo.second.isControlledByAI() || settings["session"]["onlyai"].Bool()) - { - bool alliedToHuman = false; - for(auto & allyInfo : gs->scenarioOps->playerInfos) - if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) - alliedToHuman = true; - - auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); - logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); - installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); - } - else - { - logNetwork->info("Player %s will be lead by human", color.toString()); - installNewPlayerInterface(std::make_shared(color), color); - } - } - } - - if(settings["session"]["spectate"].Bool()) - { - installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); - } - - if(CSH->getAllClientPlayers(CSH->logicConnection->connectionID).count(PlayerColor::NEUTRAL)) - installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); - - logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); -} - -std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) const -{ - if(ps.name.size()) - { - const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); - if(boost::filesystem::exists(aiPath)) - return ps.name; - } - - return aiNameForPlayer(battleAI, alliedToHuman); -} - -std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) const -{ - const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; - std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); - std::string goodBattleAI = settings["server"]["neutralAI"].String(); - std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; - std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; - - //TODO what about human players - if(battleints.size() >= sensibleAILimit) - return badAI; - - return goodAI; -} - -void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) -{ - playerint[color] = gameInterface; - - logGlobal->trace("\tInitializing the interface for player %s", color.toString()); - auto cb = std::make_shared(gs, color, this); - battleCallbacks[color] = cb; - gameInterface->initGameInterface(playerEnvironments.at(color), cb); - - installNewBattleInterface(gameInterface, color, battlecb); -} - -void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) -{ - battleints[color] = battleInterface; - - if(needCallback) - { - logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); - auto cbc = std::make_shared(color, this); - battleCallbacks[color] = cbc; - battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); - } -} - -void CClient::handlePack(CPackForClient * pack) -{ - ApplyClientNetPackVisitor afterVisitor(*this, *gameState()); - ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState()); - - pack->visit(beforeVisitor); - logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name()); - { - boost::unique_lock lock(CGameState::mutex); - gs->apply(pack); - } - logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); - pack->visit(afterVisitor); - logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name()); - - delete pack; -} - -int CClient::sendRequest(const CPackForServer * request, PlayerColor player) -{ - static ui32 requestCounter = 1; - - ui32 requestID = requestCounter++; - logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); - - waitingRequest.pushBack(requestID); - request->requestID = requestID; - request->player = player; - CSH->logicConnection->sendPack(request); - if(vstd::contains(playerint, player)) - playerint[player]->requestSent(request, requestID); - - return requestID; -} - -void CClient::battleStarted(const BattleInfo * info) -{ - std::shared_ptr att; - std::shared_ptr def; - const auto & leftSide = info->getSide(BattleSide::LEFT_SIDE); - const auto & rightSide = info->getSide(BattleSide::RIGHT_SIDE); - - for(auto & battleCb : battleCallbacks) - { - if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color) - battleCb.second->onBattleStarted(info); - } - - //If quick combat is not, do not prepare interfaces for battleint - auto callBattleStart = [&](PlayerColor color, BattleSide side) - { - if(vstd::contains(battleints, color)) - battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); - }; - - callBattleStart(leftSide.color, BattleSide::LEFT_SIDE); - callBattleStart(rightSide.color, BattleSide::RIGHT_SIDE); - callBattleStart(PlayerColor::UNFLAGGABLE, BattleSide::RIGHT_SIDE); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, BattleSide::RIGHT_SIDE); - - if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) - att = std::dynamic_pointer_cast(playerint[leftSide.color]); - - if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) - def = std::dynamic_pointer_cast(playerint[rightSide.color]); - - //Remove player interfaces for auto battle (quickCombat option) - if((att && att->isAutoFightOn) || (def && def->isAutoFightOn)) - { - auto endTacticPhaseIfEligible = [info](const CPlayerInterface * interface) - { - if (interface->cb->getBattle(info->battleID)->battleGetTacticDist()) - { - auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID); - - if(interface->playerID == info->getSide(info->tacticsSide).color) - { - auto action = BattleAction::makeEndOFTacticPhase(side); - interface->cb->battleMakeTacticAction(info->battleID, action); - } - } - }; - - if(att && att->isAutoFightOn) - endTacticPhaseIfEligible(att.get()); - else // def && def->isAutoFightOn - endTacticPhaseIfEligible(def.get()); - - att.reset(); - def.reset(); - } - - if(!settings["session"]["headless"].Bool()) - { - if(att || def) - { - CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); - } - else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - { - //TODO: This certainly need improvement - auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); - spectratorInt->cb->onBattleStarted(info); - CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); - } - } - - if(info->tacticDistance) - { - auto tacticianColor = info->getSide(info->tacticsSide).color; - - if (vstd::contains(battleints, tacticianColor)) - battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); - } -} - -void CClient::battleFinished(const BattleID & battleID) -{ - for(auto side : { BattleSide::ATTACKER, BattleSide::DEFENDER }) - { - if(battleCallbacks.count(gs->getBattle(battleID)->getSide(side).color)) - battleCallbacks[gs->getBattle(battleID)->getSide(side).color]->onBattleEnded(battleID); - } - - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); -} - -void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) -{ - if (battleints.count(color) == 0) - return; // not our combat in MP - - auto battleint = battleints.at(color); - - if (!battleint->human) - { - // we want to avoid locking gamestate and causing UI to freeze while AI is making turn - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); - } - else - { - battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); - } -} - -void CClient::invalidatePaths() -{ - boost::unique_lock pathLock(pathCacheMutex); - pathCache.clear(); -} - -vstd::RNG & CClient::getRandomGenerator() -{ - // Client should use CRandomGenerator::getDefault() for UI logic - // Gamestate should never call this method on client! - throw std::runtime_error("Illegal access to random number generator from client code!"); -} - -std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) -{ - assert(h); - boost::unique_lock pathLock(pathCacheMutex); - - auto iter = pathCache.find(h); - - if(iter == std::end(pathCache)) - { - auto paths = std::make_shared(getMapSize(), h); - - gs->calculatePaths(h, *paths.get()); - - pathCache[h] = paths; - return paths; - } - else - { - return iter->second; - } -} - -#if SCRIPTING_ENABLED -scripting::Pool * CClient::getGlobalContextPool() const -{ - return clientScripts.get(); -} -#endif - -void CClient::reinitScripting() -{ - clientEventBus = std::make_unique(); -#if SCRIPTING_ENABLED - clientScripts.reset(new scripting::PoolImpl(this)); -#endif -} - -void CClient::removeGUI() const -{ - // CClient::endGame - GH.curInt = nullptr; - GH.windows().clear(); - adventureInt.reset(); - logGlobal->info("Removed GUI."); - - LOCPLINT = nullptr; -} - -#ifdef VCMI_ANDROID -extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) -{ - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - - logGlobal->info("Received emergency save game request"); - if(!LOCPLINT || !LOCPLINT->cb) - { - logGlobal->info("... but no active player interface found!"); - return false; - } - - if (!CSH || !CSH->logicConnection) - { - logGlobal->info("... but no active connection found!"); - return false; - } - - LOCPLINT->cb->save("Saves/_Android_Autosave"); - return true; -} -#endif +/* + * Client.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 "Global.h" +#include "Client.h" + +#include "CGameInfo.h" +#include "CPlayerInterface.h" +#include "PlayerLocalState.h" +#include "CServerHandler.h" +#include "ClientNetPackVisitors.h" +#include "adventureMap/AdventureMapInterface.h" +#include "battle/BattleInterface.h" +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "mapView/mapHandler.h" + +#include "../CCallback.h" +#include "../lib/CConfigHandler.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/CPlayerState.h" +#include "../lib/CThreadHelper.h" +#include "../lib/VCMIDirs.h" +#include "../lib/UnlockGuard.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/serializer/Connection.h" +#include "../lib/mapping/CMapService.h" +#include "../lib/pathfinder/CGPathNode.h" +#include "../lib/filesystem/Filesystem.h" + +#include +#include + +#if SCRIPTING_ENABLED +#include "../lib/ScriptHandler.h" +#endif + +#ifdef VCMI_ANDROID +#include "lib/CAndroidVMHelper.h" +#endif + +ThreadSafeVector CClient::waitingRequest; + +CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_) + : player(player_), + cl(cl_), + mainCallback(mainCallback_) +{ + +} + +const Services * CPlayerEnvironment::services() const +{ + return VLC; +} + +vstd::CLoggerBase * CPlayerEnvironment::logger() const +{ + return logGlobal; +} + +events::EventBus * CPlayerEnvironment::eventBus() const +{ + return cl->eventBus();//always get actual value +} + +const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const +{ + return mainCallback->getBattle(battleID).get(); +} + +const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const +{ + return mainCallback.get(); +} + +CClient::CClient() +{ + waitingRequest.clear(); + gs = nullptr; +} + +CClient::~CClient() = default; + +const Services * CClient::services() const +{ + return VLC; //todo: this should be CGI +} + +const CClient::BattleCb * CClient::battle(const BattleID & battleID) const +{ + return nullptr; //todo? +} + +const CClient::GameCb * CClient::game() const +{ + return this; +} + +vstd::CLoggerBase * CClient::logger() const +{ + return logGlobal; +} + +events::EventBus * CClient::eventBus() const +{ + return clientEventBus.get(); +} + +void CClient::newGame(CGameState * initializedGameState) +{ + CSH->th->update(); + CMapService mapService; + assert(initializedGameState); + gs = initializedGameState; + gs->preInit(VLC, this); + logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); + if(!initializedGameState) + { + Load::ProgressAccumulator progressTracking; + gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); + } + logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); + + initMapHandler(); + reinitScripting(); + initPlayerEnvironments(); + initPlayerInterfaces(); +} + +void CClient::loadGame(CGameState * initializedGameState) +{ + logNetwork->info("Loading procedure started!"); + + logNetwork->info("Game state was transferred over network, loading."); + gs = initializedGameState; + + gs->preInit(VLC, this); + gs->updateOnLoad(CSH->si.get()); + logNetwork->info("Game loaded, initialize interfaces."); + + initMapHandler(); + + reinitScripting(); + + initPlayerEnvironments(); + initPlayerInterfaces(); +} + +void CClient::save(const std::string & fname) +{ + if(!gs->currentBattles.empty()) + { + logNetwork->error("Game cannot be saved during battle!"); + return; + } + + SaveGame save_game(fname); + sendRequest(&save_game, PlayerColor::NEUTRAL); +} + +void CClient::endNetwork() +{ + if (CGI->mh) + CGI->mh->endNetwork(); + + if (CPlayerInterface::battleInt) + CPlayerInterface::battleInt->endNetwork(); + + for(auto & i : playerint) + { + auto interface = std::dynamic_pointer_cast(i.second); + if (interface) + interface->endNetwork(); + } +} + +void CClient::endGame() +{ +#if SCRIPTING_ENABLED + clientScripts.reset(); +#endif + + //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) + for(auto & i : playerint) + i.second->finish(); + + GH.curInt = nullptr; + { + logNetwork->info("Ending current game!"); + removeGUI(); + + CGI->mh.reset(); + vstd::clear_pointer(gs); + + logNetwork->info("Deleted mapHandler and gameState."); + } + + CPlayerInterface::battleInt.reset(); + playerint.clear(); + battleints.clear(); + battleCallbacks.clear(); + playerEnvironments.clear(); + logNetwork->info("Deleted playerInts."); + logNetwork->info("Client stopped."); +} + +void CClient::initMapHandler() +{ + // TODO: CMapHandler initialization can probably go somewhere else + // It's can't be before initialization of interfaces + // During loading CPlayerInterface from serialized state it's depend on MH + if(!settings["session"]["headless"].Bool()) + { + CGI->mh = std::make_shared(gs->map); + logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); + } + + pathCache.clear(); +} + +void CClient::initPlayerEnvironments() +{ + playerEnvironments.clear(); + + auto allPlayers = CSH->getAllClientPlayers(CSH->logicConnection->connectionID); + bool hasHumanPlayer = false; + for(auto & color : allPlayers) + { + logNetwork->info("Preparing environment for player %s", color.toString()); + playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); + + if(!hasHumanPlayer && gs->players[color].isHuman()) + hasHumanPlayer = true; + } + + if(!hasHumanPlayer && !settings["session"]["headless"].Bool()) + { + Settings session = settings.write["session"]; + session["spectate"].Bool() = true; + session["spectate-skip-battle-result"].Bool() = true; + session["spectate-ignore-hero"].Bool() = true; + } + + if(settings["session"]["spectate"].Bool()) + { + playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); + } +} + +void CClient::initPlayerInterfaces() +{ + for(auto & playerInfo : gs->scenarioOps->playerInfos) + { + PlayerColor color = playerInfo.first; + if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), color)) + continue; + + if(!vstd::contains(playerint, color)) + { + logNetwork->info("Preparing interface for player %s", color.toString()); + if(playerInfo.second.isControlledByAI() || settings["session"]["onlyai"].Bool()) + { + bool alliedToHuman = false; + for(auto & allyInfo : gs->scenarioOps->playerInfos) + if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) + alliedToHuman = true; + + auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); + logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); + installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); + } + else + { + logNetwork->info("Player %s will be lead by human", color.toString()); + installNewPlayerInterface(std::make_shared(color), color); + } + } + } + + if(settings["session"]["spectate"].Bool()) + { + installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); + } + + if(CSH->getAllClientPlayers(CSH->logicConnection->connectionID).count(PlayerColor::NEUTRAL)) + installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); + + logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); +} + +std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) const +{ + if(ps.name.size()) + { + const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); + if(boost::filesystem::exists(aiPath)) + return ps.name; + } + + return aiNameForPlayer(battleAI, alliedToHuman); +} + +std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) const +{ + const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; + std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); + std::string goodBattleAI = settings["server"]["neutralAI"].String(); + std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; + std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; + + //TODO what about human players + if(battleints.size() >= sensibleAILimit) + return badAI; + + return goodAI; +} + +void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) +{ + playerint[color] = gameInterface; + + logGlobal->trace("\tInitializing the interface for player %s", color.toString()); + auto cb = std::make_shared(gs, color, this); + battleCallbacks[color] = cb; + gameInterface->initGameInterface(playerEnvironments.at(color), cb); + + installNewBattleInterface(gameInterface, color, battlecb); +} + +void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) +{ + battleints[color] = battleInterface; + + if(needCallback) + { + logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); + auto cbc = std::make_shared(color, this); + battleCallbacks[color] = cbc; + battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); + } +} + +void CClient::handlePack(CPackForClient * pack) +{ + ApplyClientNetPackVisitor afterVisitor(*this, *gameState()); + ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState()); + + pack->visit(beforeVisitor); + logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name()); + { + boost::unique_lock lock(CGameState::mutex); + gs->apply(pack); + } + logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); + pack->visit(afterVisitor); + logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name()); + + delete pack; +} + +int CClient::sendRequest(const CPackForServer * request, PlayerColor player) +{ + static ui32 requestCounter = 1; + + ui32 requestID = requestCounter++; + logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); + + waitingRequest.pushBack(requestID); + request->requestID = requestID; + request->player = player; + CSH->logicConnection->sendPack(request); + if(vstd::contains(playerint, player)) + playerint[player]->requestSent(request, requestID); + + return requestID; +} + +void CClient::battleStarted(const BattleInfo * info) +{ + std::shared_ptr att; + std::shared_ptr def; + const auto & leftSide = info->getSide(BattleSide::LEFT_SIDE); + const auto & rightSide = info->getSide(BattleSide::RIGHT_SIDE); + + for(auto & battleCb : battleCallbacks) + { + if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color) + battleCb.second->onBattleStarted(info); + } + + //If quick combat is not, do not prepare interfaces for battleint + auto callBattleStart = [&](PlayerColor color, BattleSide side) + { + if(vstd::contains(battleints, color)) + battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); + }; + + callBattleStart(leftSide.color, BattleSide::LEFT_SIDE); + callBattleStart(rightSide.color, BattleSide::RIGHT_SIDE); + callBattleStart(PlayerColor::UNFLAGGABLE, BattleSide::RIGHT_SIDE); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + callBattleStart(PlayerColor::SPECTATOR, BattleSide::RIGHT_SIDE); + + if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) + att = std::dynamic_pointer_cast(playerint[leftSide.color]); + + if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) + def = std::dynamic_pointer_cast(playerint[rightSide.color]); + + //Remove player interfaces for auto battle (quickCombat option) + if((att && att->isAutoFightOn) || (def && def->isAutoFightOn)) + { + auto endTacticPhaseIfEligible = [info](const CPlayerInterface * interface) + { + if (interface->cb->getBattle(info->battleID)->battleGetTacticDist()) + { + auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID); + + if(interface->playerID == info->getSide(info->tacticsSide).color) + { + auto action = BattleAction::makeEndOFTacticPhase(side); + interface->cb->battleMakeTacticAction(info->battleID, action); + } + } + }; + + if(att && att->isAutoFightOn) + endTacticPhaseIfEligible(att.get()); + else // def && def->isAutoFightOn + endTacticPhaseIfEligible(def.get()); + + att.reset(); + def.reset(); + } + + if(!settings["session"]["headless"].Bool()) + { + if(att || def) + { + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); + } + else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + { + //TODO: This certainly need improvement + auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); + spectratorInt->cb->onBattleStarted(info); + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); + } + } + + if(info->tacticDistance) + { + auto tacticianColor = info->getSide(info->tacticsSide).color; + + if (vstd::contains(battleints, tacticianColor)) + battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); + } +} + +void CClient::battleFinished(const BattleID & battleID) +{ + for(auto side : { BattleSide::ATTACKER, BattleSide::DEFENDER }) + { + if(battleCallbacks.count(gs->getBattle(battleID)->getSide(side).color)) + battleCallbacks[gs->getBattle(battleID)->getSide(side).color]->onBattleEnded(battleID); + } + + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); +} + +void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) +{ + if (battleints.count(color) == 0) + return; // not our combat in MP + + auto battleint = battleints.at(color); + + if (!battleint->human) + { + // we want to avoid locking gamestate and causing UI to freeze while AI is making turn + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } + else + { + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } +} + +void CClient::updatePath(const ObjectInstanceID & id) +{ + invalidatePaths(); + auto hero = getHero(id); + updatePath(hero); +} + +void CClient::updatePath(const CGHeroInstance * hero) +{ + if(LOCPLINT && hero) + LOCPLINT->localState->verifyPath(hero); +} + +void CClient::invalidatePaths() +{ + boost::unique_lock pathLock(pathCacheMutex); + pathCache.clear(); +} + +vstd::RNG & CClient::getRandomGenerator() +{ + // Client should use CRandomGenerator::getDefault() for UI logic + // Gamestate should never call this method on client! + throw std::runtime_error("Illegal access to random number generator from client code!"); +} + +std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) +{ + assert(h); + boost::unique_lock pathLock(pathCacheMutex); + + auto iter = pathCache.find(h); + + if(iter == std::end(pathCache)) + { + auto paths = std::make_shared(getMapSize(), h); + + gs->calculatePaths(h, *paths.get()); + + pathCache[h] = paths; + return paths; + } + else + { + return iter->second; + } +} + +#if SCRIPTING_ENABLED +scripting::Pool * CClient::getGlobalContextPool() const +{ + return clientScripts.get(); +} +#endif + +void CClient::reinitScripting() +{ + clientEventBus = std::make_unique(); +#if SCRIPTING_ENABLED + clientScripts.reset(new scripting::PoolImpl(this)); +#endif +} + +void CClient::removeGUI() const +{ + // CClient::endGame + GH.curInt = nullptr; + GH.windows().clear(); + adventureInt.reset(); + logGlobal->info("Removed GUI."); + + LOCPLINT = nullptr; +} + +#ifdef VCMI_ANDROID +extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + logGlobal->info("Received emergency save game request"); + if(!LOCPLINT || !LOCPLINT->cb) + { + logGlobal->info("... but no active player interface found!"); + return false; + } + + if (!CSH || !CSH->logicConnection) + { + logGlobal->info("... but no active connection found!"); + return false; + } + + LOCPLINT->cb->save("Saves/_Android_Autosave"); + return true; +} +#endif diff --git a/client/Client.h b/client/Client.h index c5b763a49..d63f70578 100644 --- a/client/Client.h +++ b/client/Client.h @@ -150,7 +150,9 @@ public: void battleFinished(const BattleID & battleID); void startPlayerBattleAction(const BattleID & battleID, PlayerColor color); - void invalidatePaths(); + void invalidatePaths(); // clears this->pathCache() + void updatePath(const ObjectInstanceID & heroID); // invalidatePaths and update displayed hero path + void updatePath(const CGHeroInstance * hero); std::shared_ptr getPathsInfo(const CGHeroInstance * h); friend class CCallback; //handling players actions diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index afd8f96f5..4175b9e6d 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -25,7 +25,6 @@ #include "CMT.h" #include "GameChatHandler.h" #include "CServerHandler.h" -#include "PlayerLocalState.h" #include "../CCallback.h" #include "../lib/filesystem/Filesystem.h" @@ -169,7 +168,7 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack) { const CGHeroInstance *h = cl.getHero(pack.hid); - cl.invalidatePaths(); + cl.updatePath(h); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h); } @@ -230,16 +229,7 @@ void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack) void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack) { dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); - cl.invalidatePaths(); //it is possible to remove last non-native unit for current terrain and lose movement penalty - - if(LOCPLINT) - { - for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if(hero->id == pack.army) - LOCPLINT->localState->verifyPath(hero); - } - } + cl.updatePath(pack.army); //it is possible to remove last non-native unit for current terrain and lose movement penalty } void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) @@ -247,32 +237,14 @@ void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); if(pack.srcArmy != pack.dstArmy) - { - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains - if(LOCPLINT) - { - for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if(hero->id == pack.dstArmy) - LOCPLINT->localState->verifyPath(hero); - } - } - } + cl.updatePath(pack.dstArmy); // adding/removing units may change terrain type penalty based on creature native terrains } void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack) { dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); - auto hero = gs.getHero(pack.army); - if(hero) - { - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains - if(LOCPLINT) - { - LOCPLINT->localState->verifyPath(hero); - } - } + cl.updatePath(pack.army); // adding/removing units may change terrain type penalty based on creature native terrains } void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) @@ -281,15 +253,8 @@ void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) if(pack.srcArmy != pack.dstArmy) { - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains - if(LOCPLINT) - { - for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if(hero->id == pack.dstArmy || hero->id == pack.srcArmy) - LOCPLINT->localState->verifyPath(hero); - } - } + cl.updatePath(pack.srcArmy); // adding/removing units may change terrain type penalty based on creature native terrains + cl.updatePath(pack.dstArmy); } } @@ -304,16 +269,8 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p if(pack.moves[0].srcArmy != destArmy) { - cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains - if(LOCPLINT) - { - // update all paths - for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if(hero->id == destArmy || hero->id == pack.moves[0].srcArmy) - LOCPLINT->localState->verifyPath(hero); - } - } + cl.updatePath(destArmy); // adding/removing units may change terrain type penalty based on creature native terrains + cl.updatePath(pack.moves[0].srcArmy); } } } @@ -360,16 +317,8 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) if(pack.interfaceOwner != dstOwner) callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::artifactMoved, srcLoc, dstLoc); - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings - - if(LOCPLINT) - { - for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if(hero->id == pack.srcArtHolder || hero->id == pack.dstArtHolder) - LOCPLINT->localState->verifyPath(hero); - } - } + cl.updatePath(pack.srcArtHolder); // hero might have equipped/unequipped Angel Wings + cl.updatePath(pack.dstArtHolder); } }; @@ -399,31 +348,14 @@ void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) { callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al); - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings - if(LOCPLINT) - { - for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if(hero->id == pack.al.artHolder) - LOCPLINT->localState->verifyPath(hero); - } - } - + cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings } void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) { callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al); - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings - if(LOCPLINT) - { - for(const auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if(hero->id == pack.al.artHolder) - LOCPLINT->localState->verifyPath(hero); - } - } + cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings } void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) From 7fa965e78b9f84feec710500891f9b06fe719753 Mon Sep 17 00:00:00 2001 From: MichalZr6 Date: Thu, 19 Sep 2024 09:20:36 +0200 Subject: [PATCH 18/20] Fix unintentionally broken line endings to LF in Client.cpp --- client/Client.cpp | 1188 ++++++++++++++++++++++----------------------- 1 file changed, 594 insertions(+), 594 deletions(-) diff --git a/client/Client.cpp b/client/Client.cpp index f058e7da3..2d57375ca 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -1,594 +1,594 @@ -/* - * Client.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 "Global.h" -#include "Client.h" - -#include "CGameInfo.h" -#include "CPlayerInterface.h" -#include "PlayerLocalState.h" -#include "CServerHandler.h" -#include "ClientNetPackVisitors.h" -#include "adventureMap/AdventureMapInterface.h" -#include "battle/BattleInterface.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "mapView/mapHandler.h" - -#include "../CCallback.h" -#include "../lib/CConfigHandler.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CPlayerState.h" -#include "../lib/CThreadHelper.h" -#include "../lib/VCMIDirs.h" -#include "../lib/UnlockGuard.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/serializer/Connection.h" -#include "../lib/mapping/CMapService.h" -#include "../lib/pathfinder/CGPathNode.h" -#include "../lib/filesystem/Filesystem.h" - -#include -#include - -#if SCRIPTING_ENABLED -#include "../lib/ScriptHandler.h" -#endif - -#ifdef VCMI_ANDROID -#include "lib/CAndroidVMHelper.h" -#endif - -ThreadSafeVector CClient::waitingRequest; - -CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_) - : player(player_), - cl(cl_), - mainCallback(mainCallback_) -{ - -} - -const Services * CPlayerEnvironment::services() const -{ - return VLC; -} - -vstd::CLoggerBase * CPlayerEnvironment::logger() const -{ - return logGlobal; -} - -events::EventBus * CPlayerEnvironment::eventBus() const -{ - return cl->eventBus();//always get actual value -} - -const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const -{ - return mainCallback->getBattle(battleID).get(); -} - -const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const -{ - return mainCallback.get(); -} - -CClient::CClient() -{ - waitingRequest.clear(); - gs = nullptr; -} - -CClient::~CClient() = default; - -const Services * CClient::services() const -{ - return VLC; //todo: this should be CGI -} - -const CClient::BattleCb * CClient::battle(const BattleID & battleID) const -{ - return nullptr; //todo? -} - -const CClient::GameCb * CClient::game() const -{ - return this; -} - -vstd::CLoggerBase * CClient::logger() const -{ - return logGlobal; -} - -events::EventBus * CClient::eventBus() const -{ - return clientEventBus.get(); -} - -void CClient::newGame(CGameState * initializedGameState) -{ - CSH->th->update(); - CMapService mapService; - assert(initializedGameState); - gs = initializedGameState; - gs->preInit(VLC, this); - logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); - if(!initializedGameState) - { - Load::ProgressAccumulator progressTracking; - gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); - } - logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); - - initMapHandler(); - reinitScripting(); - initPlayerEnvironments(); - initPlayerInterfaces(); -} - -void CClient::loadGame(CGameState * initializedGameState) -{ - logNetwork->info("Loading procedure started!"); - - logNetwork->info("Game state was transferred over network, loading."); - gs = initializedGameState; - - gs->preInit(VLC, this); - gs->updateOnLoad(CSH->si.get()); - logNetwork->info("Game loaded, initialize interfaces."); - - initMapHandler(); - - reinitScripting(); - - initPlayerEnvironments(); - initPlayerInterfaces(); -} - -void CClient::save(const std::string & fname) -{ - if(!gs->currentBattles.empty()) - { - logNetwork->error("Game cannot be saved during battle!"); - return; - } - - SaveGame save_game(fname); - sendRequest(&save_game, PlayerColor::NEUTRAL); -} - -void CClient::endNetwork() -{ - if (CGI->mh) - CGI->mh->endNetwork(); - - if (CPlayerInterface::battleInt) - CPlayerInterface::battleInt->endNetwork(); - - for(auto & i : playerint) - { - auto interface = std::dynamic_pointer_cast(i.second); - if (interface) - interface->endNetwork(); - } -} - -void CClient::endGame() -{ -#if SCRIPTING_ENABLED - clientScripts.reset(); -#endif - - //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) - for(auto & i : playerint) - i.second->finish(); - - GH.curInt = nullptr; - { - logNetwork->info("Ending current game!"); - removeGUI(); - - CGI->mh.reset(); - vstd::clear_pointer(gs); - - logNetwork->info("Deleted mapHandler and gameState."); - } - - CPlayerInterface::battleInt.reset(); - playerint.clear(); - battleints.clear(); - battleCallbacks.clear(); - playerEnvironments.clear(); - logNetwork->info("Deleted playerInts."); - logNetwork->info("Client stopped."); -} - -void CClient::initMapHandler() -{ - // TODO: CMapHandler initialization can probably go somewhere else - // It's can't be before initialization of interfaces - // During loading CPlayerInterface from serialized state it's depend on MH - if(!settings["session"]["headless"].Bool()) - { - CGI->mh = std::make_shared(gs->map); - logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); - } - - pathCache.clear(); -} - -void CClient::initPlayerEnvironments() -{ - playerEnvironments.clear(); - - auto allPlayers = CSH->getAllClientPlayers(CSH->logicConnection->connectionID); - bool hasHumanPlayer = false; - for(auto & color : allPlayers) - { - logNetwork->info("Preparing environment for player %s", color.toString()); - playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); - - if(!hasHumanPlayer && gs->players[color].isHuman()) - hasHumanPlayer = true; - } - - if(!hasHumanPlayer && !settings["session"]["headless"].Bool()) - { - Settings session = settings.write["session"]; - session["spectate"].Bool() = true; - session["spectate-skip-battle-result"].Bool() = true; - session["spectate-ignore-hero"].Bool() = true; - } - - if(settings["session"]["spectate"].Bool()) - { - playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); - } -} - -void CClient::initPlayerInterfaces() -{ - for(auto & playerInfo : gs->scenarioOps->playerInfos) - { - PlayerColor color = playerInfo.first; - if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), color)) - continue; - - if(!vstd::contains(playerint, color)) - { - logNetwork->info("Preparing interface for player %s", color.toString()); - if(playerInfo.second.isControlledByAI() || settings["session"]["onlyai"].Bool()) - { - bool alliedToHuman = false; - for(auto & allyInfo : gs->scenarioOps->playerInfos) - if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) - alliedToHuman = true; - - auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); - logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); - installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); - } - else - { - logNetwork->info("Player %s will be lead by human", color.toString()); - installNewPlayerInterface(std::make_shared(color), color); - } - } - } - - if(settings["session"]["spectate"].Bool()) - { - installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); - } - - if(CSH->getAllClientPlayers(CSH->logicConnection->connectionID).count(PlayerColor::NEUTRAL)) - installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); - - logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); -} - -std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) const -{ - if(ps.name.size()) - { - const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); - if(boost::filesystem::exists(aiPath)) - return ps.name; - } - - return aiNameForPlayer(battleAI, alliedToHuman); -} - -std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) const -{ - const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; - std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); - std::string goodBattleAI = settings["server"]["neutralAI"].String(); - std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; - std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; - - //TODO what about human players - if(battleints.size() >= sensibleAILimit) - return badAI; - - return goodAI; -} - -void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) -{ - playerint[color] = gameInterface; - - logGlobal->trace("\tInitializing the interface for player %s", color.toString()); - auto cb = std::make_shared(gs, color, this); - battleCallbacks[color] = cb; - gameInterface->initGameInterface(playerEnvironments.at(color), cb); - - installNewBattleInterface(gameInterface, color, battlecb); -} - -void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) -{ - battleints[color] = battleInterface; - - if(needCallback) - { - logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); - auto cbc = std::make_shared(color, this); - battleCallbacks[color] = cbc; - battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); - } -} - -void CClient::handlePack(CPackForClient * pack) -{ - ApplyClientNetPackVisitor afterVisitor(*this, *gameState()); - ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState()); - - pack->visit(beforeVisitor); - logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name()); - { - boost::unique_lock lock(CGameState::mutex); - gs->apply(pack); - } - logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); - pack->visit(afterVisitor); - logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name()); - - delete pack; -} - -int CClient::sendRequest(const CPackForServer * request, PlayerColor player) -{ - static ui32 requestCounter = 1; - - ui32 requestID = requestCounter++; - logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); - - waitingRequest.pushBack(requestID); - request->requestID = requestID; - request->player = player; - CSH->logicConnection->sendPack(request); - if(vstd::contains(playerint, player)) - playerint[player]->requestSent(request, requestID); - - return requestID; -} - -void CClient::battleStarted(const BattleInfo * info) -{ - std::shared_ptr att; - std::shared_ptr def; - const auto & leftSide = info->getSide(BattleSide::LEFT_SIDE); - const auto & rightSide = info->getSide(BattleSide::RIGHT_SIDE); - - for(auto & battleCb : battleCallbacks) - { - if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color) - battleCb.second->onBattleStarted(info); - } - - //If quick combat is not, do not prepare interfaces for battleint - auto callBattleStart = [&](PlayerColor color, BattleSide side) - { - if(vstd::contains(battleints, color)) - battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); - }; - - callBattleStart(leftSide.color, BattleSide::LEFT_SIDE); - callBattleStart(rightSide.color, BattleSide::RIGHT_SIDE); - callBattleStart(PlayerColor::UNFLAGGABLE, BattleSide::RIGHT_SIDE); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, BattleSide::RIGHT_SIDE); - - if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) - att = std::dynamic_pointer_cast(playerint[leftSide.color]); - - if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) - def = std::dynamic_pointer_cast(playerint[rightSide.color]); - - //Remove player interfaces for auto battle (quickCombat option) - if((att && att->isAutoFightOn) || (def && def->isAutoFightOn)) - { - auto endTacticPhaseIfEligible = [info](const CPlayerInterface * interface) - { - if (interface->cb->getBattle(info->battleID)->battleGetTacticDist()) - { - auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID); - - if(interface->playerID == info->getSide(info->tacticsSide).color) - { - auto action = BattleAction::makeEndOFTacticPhase(side); - interface->cb->battleMakeTacticAction(info->battleID, action); - } - } - }; - - if(att && att->isAutoFightOn) - endTacticPhaseIfEligible(att.get()); - else // def && def->isAutoFightOn - endTacticPhaseIfEligible(def.get()); - - att.reset(); - def.reset(); - } - - if(!settings["session"]["headless"].Bool()) - { - if(att || def) - { - CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); - } - else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - { - //TODO: This certainly need improvement - auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); - spectratorInt->cb->onBattleStarted(info); - CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); - } - } - - if(info->tacticDistance) - { - auto tacticianColor = info->getSide(info->tacticsSide).color; - - if (vstd::contains(battleints, tacticianColor)) - battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); - } -} - -void CClient::battleFinished(const BattleID & battleID) -{ - for(auto side : { BattleSide::ATTACKER, BattleSide::DEFENDER }) - { - if(battleCallbacks.count(gs->getBattle(battleID)->getSide(side).color)) - battleCallbacks[gs->getBattle(battleID)->getSide(side).color]->onBattleEnded(battleID); - } - - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); -} - -void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) -{ - if (battleints.count(color) == 0) - return; // not our combat in MP - - auto battleint = battleints.at(color); - - if (!battleint->human) - { - // we want to avoid locking gamestate and causing UI to freeze while AI is making turn - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); - } - else - { - battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); - } -} - -void CClient::updatePath(const ObjectInstanceID & id) -{ - invalidatePaths(); - auto hero = getHero(id); - updatePath(hero); -} - -void CClient::updatePath(const CGHeroInstance * hero) -{ - if(LOCPLINT && hero) - LOCPLINT->localState->verifyPath(hero); -} - -void CClient::invalidatePaths() -{ - boost::unique_lock pathLock(pathCacheMutex); - pathCache.clear(); -} - -vstd::RNG & CClient::getRandomGenerator() -{ - // Client should use CRandomGenerator::getDefault() for UI logic - // Gamestate should never call this method on client! - throw std::runtime_error("Illegal access to random number generator from client code!"); -} - -std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) -{ - assert(h); - boost::unique_lock pathLock(pathCacheMutex); - - auto iter = pathCache.find(h); - - if(iter == std::end(pathCache)) - { - auto paths = std::make_shared(getMapSize(), h); - - gs->calculatePaths(h, *paths.get()); - - pathCache[h] = paths; - return paths; - } - else - { - return iter->second; - } -} - -#if SCRIPTING_ENABLED -scripting::Pool * CClient::getGlobalContextPool() const -{ - return clientScripts.get(); -} -#endif - -void CClient::reinitScripting() -{ - clientEventBus = std::make_unique(); -#if SCRIPTING_ENABLED - clientScripts.reset(new scripting::PoolImpl(this)); -#endif -} - -void CClient::removeGUI() const -{ - // CClient::endGame - GH.curInt = nullptr; - GH.windows().clear(); - adventureInt.reset(); - logGlobal->info("Removed GUI."); - - LOCPLINT = nullptr; -} - -#ifdef VCMI_ANDROID -extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) -{ - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - - logGlobal->info("Received emergency save game request"); - if(!LOCPLINT || !LOCPLINT->cb) - { - logGlobal->info("... but no active player interface found!"); - return false; - } - - if (!CSH || !CSH->logicConnection) - { - logGlobal->info("... but no active connection found!"); - return false; - } - - LOCPLINT->cb->save("Saves/_Android_Autosave"); - return true; -} -#endif +/* + * Client.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 "Global.h" +#include "Client.h" + +#include "CGameInfo.h" +#include "CPlayerInterface.h" +#include "PlayerLocalState.h" +#include "CServerHandler.h" +#include "ClientNetPackVisitors.h" +#include "adventureMap/AdventureMapInterface.h" +#include "battle/BattleInterface.h" +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "mapView/mapHandler.h" + +#include "../CCallback.h" +#include "../lib/CConfigHandler.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/CPlayerState.h" +#include "../lib/CThreadHelper.h" +#include "../lib/VCMIDirs.h" +#include "../lib/UnlockGuard.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/serializer/Connection.h" +#include "../lib/mapping/CMapService.h" +#include "../lib/pathfinder/CGPathNode.h" +#include "../lib/filesystem/Filesystem.h" + +#include +#include + +#if SCRIPTING_ENABLED +#include "../lib/ScriptHandler.h" +#endif + +#ifdef VCMI_ANDROID +#include "lib/CAndroidVMHelper.h" +#endif + +ThreadSafeVector CClient::waitingRequest; + +CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_) + : player(player_), + cl(cl_), + mainCallback(mainCallback_) +{ + +} + +const Services * CPlayerEnvironment::services() const +{ + return VLC; +} + +vstd::CLoggerBase * CPlayerEnvironment::logger() const +{ + return logGlobal; +} + +events::EventBus * CPlayerEnvironment::eventBus() const +{ + return cl->eventBus();//always get actual value +} + +const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const +{ + return mainCallback->getBattle(battleID).get(); +} + +const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const +{ + return mainCallback.get(); +} + +CClient::CClient() +{ + waitingRequest.clear(); + gs = nullptr; +} + +CClient::~CClient() = default; + +const Services * CClient::services() const +{ + return VLC; //todo: this should be CGI +} + +const CClient::BattleCb * CClient::battle(const BattleID & battleID) const +{ + return nullptr; //todo? +} + +const CClient::GameCb * CClient::game() const +{ + return this; +} + +vstd::CLoggerBase * CClient::logger() const +{ + return logGlobal; +} + +events::EventBus * CClient::eventBus() const +{ + return clientEventBus.get(); +} + +void CClient::newGame(CGameState * initializedGameState) +{ + CSH->th->update(); + CMapService mapService; + assert(initializedGameState); + gs = initializedGameState; + gs->preInit(VLC, this); + logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); + if(!initializedGameState) + { + Load::ProgressAccumulator progressTracking; + gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); + } + logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); + + initMapHandler(); + reinitScripting(); + initPlayerEnvironments(); + initPlayerInterfaces(); +} + +void CClient::loadGame(CGameState * initializedGameState) +{ + logNetwork->info("Loading procedure started!"); + + logNetwork->info("Game state was transferred over network, loading."); + gs = initializedGameState; + + gs->preInit(VLC, this); + gs->updateOnLoad(CSH->si.get()); + logNetwork->info("Game loaded, initialize interfaces."); + + initMapHandler(); + + reinitScripting(); + + initPlayerEnvironments(); + initPlayerInterfaces(); +} + +void CClient::save(const std::string & fname) +{ + if(!gs->currentBattles.empty()) + { + logNetwork->error("Game cannot be saved during battle!"); + return; + } + + SaveGame save_game(fname); + sendRequest(&save_game, PlayerColor::NEUTRAL); +} + +void CClient::endNetwork() +{ + if (CGI->mh) + CGI->mh->endNetwork(); + + if (CPlayerInterface::battleInt) + CPlayerInterface::battleInt->endNetwork(); + + for(auto & i : playerint) + { + auto interface = std::dynamic_pointer_cast(i.second); + if (interface) + interface->endNetwork(); + } +} + +void CClient::endGame() +{ +#if SCRIPTING_ENABLED + clientScripts.reset(); +#endif + + //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) + for(auto & i : playerint) + i.second->finish(); + + GH.curInt = nullptr; + { + logNetwork->info("Ending current game!"); + removeGUI(); + + CGI->mh.reset(); + vstd::clear_pointer(gs); + + logNetwork->info("Deleted mapHandler and gameState."); + } + + CPlayerInterface::battleInt.reset(); + playerint.clear(); + battleints.clear(); + battleCallbacks.clear(); + playerEnvironments.clear(); + logNetwork->info("Deleted playerInts."); + logNetwork->info("Client stopped."); +} + +void CClient::initMapHandler() +{ + // TODO: CMapHandler initialization can probably go somewhere else + // It's can't be before initialization of interfaces + // During loading CPlayerInterface from serialized state it's depend on MH + if(!settings["session"]["headless"].Bool()) + { + CGI->mh = std::make_shared(gs->map); + logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); + } + + pathCache.clear(); +} + +void CClient::initPlayerEnvironments() +{ + playerEnvironments.clear(); + + auto allPlayers = CSH->getAllClientPlayers(CSH->logicConnection->connectionID); + bool hasHumanPlayer = false; + for(auto & color : allPlayers) + { + logNetwork->info("Preparing environment for player %s", color.toString()); + playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); + + if(!hasHumanPlayer && gs->players[color].isHuman()) + hasHumanPlayer = true; + } + + if(!hasHumanPlayer && !settings["session"]["headless"].Bool()) + { + Settings session = settings.write["session"]; + session["spectate"].Bool() = true; + session["spectate-skip-battle-result"].Bool() = true; + session["spectate-ignore-hero"].Bool() = true; + } + + if(settings["session"]["spectate"].Bool()) + { + playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); + } +} + +void CClient::initPlayerInterfaces() +{ + for(auto & playerInfo : gs->scenarioOps->playerInfos) + { + PlayerColor color = playerInfo.first; + if(!vstd::contains(CSH->getAllClientPlayers(CSH->logicConnection->connectionID), color)) + continue; + + if(!vstd::contains(playerint, color)) + { + logNetwork->info("Preparing interface for player %s", color.toString()); + if(playerInfo.second.isControlledByAI() || settings["session"]["onlyai"].Bool()) + { + bool alliedToHuman = false; + for(auto & allyInfo : gs->scenarioOps->playerInfos) + if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) + alliedToHuman = true; + + auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); + logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); + installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); + } + else + { + logNetwork->info("Player %s will be lead by human", color.toString()); + installNewPlayerInterface(std::make_shared(color), color); + } + } + } + + if(settings["session"]["spectate"].Bool()) + { + installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); + } + + if(CSH->getAllClientPlayers(CSH->logicConnection->connectionID).count(PlayerColor::NEUTRAL)) + installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); + + logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); +} + +std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) const +{ + if(ps.name.size()) + { + const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); + if(boost::filesystem::exists(aiPath)) + return ps.name; + } + + return aiNameForPlayer(battleAI, alliedToHuman); +} + +std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) const +{ + const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; + std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); + std::string goodBattleAI = settings["server"]["neutralAI"].String(); + std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; + std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; + + //TODO what about human players + if(battleints.size() >= sensibleAILimit) + return badAI; + + return goodAI; +} + +void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) +{ + playerint[color] = gameInterface; + + logGlobal->trace("\tInitializing the interface for player %s", color.toString()); + auto cb = std::make_shared(gs, color, this); + battleCallbacks[color] = cb; + gameInterface->initGameInterface(playerEnvironments.at(color), cb); + + installNewBattleInterface(gameInterface, color, battlecb); +} + +void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) +{ + battleints[color] = battleInterface; + + if(needCallback) + { + logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); + auto cbc = std::make_shared(color, this); + battleCallbacks[color] = cbc; + battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); + } +} + +void CClient::handlePack(CPackForClient * pack) +{ + ApplyClientNetPackVisitor afterVisitor(*this, *gameState()); + ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState()); + + pack->visit(beforeVisitor); + logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name()); + { + boost::unique_lock lock(CGameState::mutex); + gs->apply(pack); + } + logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); + pack->visit(afterVisitor); + logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name()); + + delete pack; +} + +int CClient::sendRequest(const CPackForServer * request, PlayerColor player) +{ + static ui32 requestCounter = 1; + + ui32 requestID = requestCounter++; + logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); + + waitingRequest.pushBack(requestID); + request->requestID = requestID; + request->player = player; + CSH->logicConnection->sendPack(request); + if(vstd::contains(playerint, player)) + playerint[player]->requestSent(request, requestID); + + return requestID; +} + +void CClient::battleStarted(const BattleInfo * info) +{ + std::shared_ptr att; + std::shared_ptr def; + const auto & leftSide = info->getSide(BattleSide::LEFT_SIDE); + const auto & rightSide = info->getSide(BattleSide::RIGHT_SIDE); + + for(auto & battleCb : battleCallbacks) + { + if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color) + battleCb.second->onBattleStarted(info); + } + + //If quick combat is not, do not prepare interfaces for battleint + auto callBattleStart = [&](PlayerColor color, BattleSide side) + { + if(vstd::contains(battleints, color)) + battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); + }; + + callBattleStart(leftSide.color, BattleSide::LEFT_SIDE); + callBattleStart(rightSide.color, BattleSide::RIGHT_SIDE); + callBattleStart(PlayerColor::UNFLAGGABLE, BattleSide::RIGHT_SIDE); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + callBattleStart(PlayerColor::SPECTATOR, BattleSide::RIGHT_SIDE); + + if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) + att = std::dynamic_pointer_cast(playerint[leftSide.color]); + + if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) + def = std::dynamic_pointer_cast(playerint[rightSide.color]); + + //Remove player interfaces for auto battle (quickCombat option) + if((att && att->isAutoFightOn) || (def && def->isAutoFightOn)) + { + auto endTacticPhaseIfEligible = [info](const CPlayerInterface * interface) + { + if (interface->cb->getBattle(info->battleID)->battleGetTacticDist()) + { + auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID); + + if(interface->playerID == info->getSide(info->tacticsSide).color) + { + auto action = BattleAction::makeEndOFTacticPhase(side); + interface->cb->battleMakeTacticAction(info->battleID, action); + } + } + }; + + if(att && att->isAutoFightOn) + endTacticPhaseIfEligible(att.get()); + else // def && def->isAutoFightOn + endTacticPhaseIfEligible(def.get()); + + att.reset(); + def.reset(); + } + + if(!settings["session"]["headless"].Bool()) + { + if(att || def) + { + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); + } + else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + { + //TODO: This certainly need improvement + auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); + spectratorInt->cb->onBattleStarted(info); + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); + } + } + + if(info->tacticDistance) + { + auto tacticianColor = info->getSide(info->tacticsSide).color; + + if (vstd::contains(battleints, tacticianColor)) + battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); + } +} + +void CClient::battleFinished(const BattleID & battleID) +{ + for(auto side : { BattleSide::ATTACKER, BattleSide::DEFENDER }) + { + if(battleCallbacks.count(gs->getBattle(battleID)->getSide(side).color)) + battleCallbacks[gs->getBattle(battleID)->getSide(side).color]->onBattleEnded(battleID); + } + + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); +} + +void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) +{ + if (battleints.count(color) == 0) + return; // not our combat in MP + + auto battleint = battleints.at(color); + + if (!battleint->human) + { + // we want to avoid locking gamestate and causing UI to freeze while AI is making turn + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } + else + { + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } +} + +void CClient::updatePath(const ObjectInstanceID & id) +{ + invalidatePaths(); + auto hero = getHero(id); + updatePath(hero); +} + +void CClient::updatePath(const CGHeroInstance * hero) +{ + if(LOCPLINT && hero) + LOCPLINT->localState->verifyPath(hero); +} + +void CClient::invalidatePaths() +{ + boost::unique_lock pathLock(pathCacheMutex); + pathCache.clear(); +} + +vstd::RNG & CClient::getRandomGenerator() +{ + // Client should use CRandomGenerator::getDefault() for UI logic + // Gamestate should never call this method on client! + throw std::runtime_error("Illegal access to random number generator from client code!"); +} + +std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) +{ + assert(h); + boost::unique_lock pathLock(pathCacheMutex); + + auto iter = pathCache.find(h); + + if(iter == std::end(pathCache)) + { + auto paths = std::make_shared(getMapSize(), h); + + gs->calculatePaths(h, *paths.get()); + + pathCache[h] = paths; + return paths; + } + else + { + return iter->second; + } +} + +#if SCRIPTING_ENABLED +scripting::Pool * CClient::getGlobalContextPool() const +{ + return clientScripts.get(); +} +#endif + +void CClient::reinitScripting() +{ + clientEventBus = std::make_unique(); +#if SCRIPTING_ENABLED + clientScripts.reset(new scripting::PoolImpl(this)); +#endif +} + +void CClient::removeGUI() const +{ + // CClient::endGame + GH.curInt = nullptr; + GH.windows().clear(); + adventureInt.reset(); + logGlobal->info("Removed GUI."); + + LOCPLINT = nullptr; +} + +#ifdef VCMI_ANDROID +extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) +{ + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + logGlobal->info("Received emergency save game request"); + if(!LOCPLINT || !LOCPLINT->cb) + { + logGlobal->info("... but no active player interface found!"); + return false; + } + + if (!CSH || !CSH->logicConnection) + { + logGlobal->info("... but no active connection found!"); + return false; + } + + LOCPLINT->cb->save("Saves/_Android_Autosave"); + return true; +} +#endif From 5f86f5504bba0f53e6f1667ff27bcb59b3186def Mon Sep 17 00:00:00 2001 From: MichalZr6 Date: Thu, 19 Sep 2024 09:51:18 +0200 Subject: [PATCH 19/20] Fix #3286 --- client/NetPacksClient.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 4175b9e6d..cda16c4b3 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -297,6 +297,7 @@ void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) void ApplyClientNetPackVisitor::visitEraseArtifact(BulkEraseArtifacts & pack) { + cl.updatePath(pack.artHolder); for(const auto & slotErase : pack.posPack) callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase)); } From a66651cdbbb19b74a7cc7e32a5e67f7af0ace7ee Mon Sep 17 00:00:00 2001 From: MichalZr6 Date: Thu, 19 Sep 2024 11:31:08 +0200 Subject: [PATCH 20/20] Trigger workflow